
Groupe fondamental
Concept history
A revision trail for this concept page.
Revision 4772
9/13/2026, 11:00:59 AM · Ancient Tree
Updated text
Compare with revision 47711 changed line
1
##### Intuition2
3
Le groupe fondamental est une tentative de formaliser la notion de « trou ».4
Par exemple, un [[tore|tore]] possède un trou. Comment peut-on rendre cette observation mathématique ?5
6
L'idée est de considérer des [[lacet-dun-espace|lacets]] et de les déformer. On peut les imaginer comme des élastiques : s'il est impossible de contracter un lacet en un seul point, c'est qu'il est retenu par un obstacle : autrement dit, un trou.7
8
```jsxgraph9
<!doctype html>10
<html lang="fr">11
<head>12
<meta charset="utf-8">13
<meta name="viewport" content="width=device-width, initial-scale=1">14
<title>Tore et lacets — JSXGraph</title>15
16
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsxgraph@1.13.1/distrib/jsxgraph.css">17
18
<style>19
:root {20
color-scheme: light dark;21
--background: light-dark(#ffffff, #181818);22
--foreground: light-dark(#202020, #eeeeee);23
--muted-foreground: light-dark(#626262, #b4b4b4);24
--red: light-dark(#da4048, #ff7378);25
--green: light-dark(#17864c, #62cf91);26
--button-background: light-dark(#f0f0f0, #343434);27
}28
29
* { box-sizing: border-box; }30
31
body {32
margin: 0;33
padding: 24px;34
font: 15px/1.5 system-ui, sans-serif;35
background: var(--background);36
color: var(--foreground);37
}38
39
#tore-lacets {40
width: 100%;41
max-width: 900px;42
margin: 0 auto;43
container-type: inline-size;44
}45
46
.viz-row {47
display: flex;48
flex-wrap: wrap;49
align-items: center;50
}51
52
.btn {53
display: inline-flex;54
align-items: center;55
justify-content: center;56
padding: 8px 14px;57
border: 0;58
border-radius: 8px;59
cursor: pointer;60
font: inherit;61
color: var(--foreground);62
background: var(--button-background);63
}64
65
.btn:hover { filter: brightness(0.94); }66
67
.form-range {68
display: block;69
width: 100%;70
margin: 12px 0 16px;71
cursor: pointer;72
}73
74
#tore-red-position { accent-color: var(--red); }75
#tore-green-shrink { accent-color: var(--green); }76
.tabular-nums { font-variant-numeric: tabular-nums; }77
.text-small { font-size: 12px; }78
.text-muted { color: var(--muted-foreground); }79
80
.sr-only {81
position: absolute;82
width: 1px;83
height: 1px;84
padding: 0;85
margin: -1px;86
overflow: hidden;87
clip: rect(0, 0, 0, 0);88
white-space: nowrap;89
border: 0;90
}91
92
.torus-legend {93
justify-content: center;94
gap: 12px 24px;95
}96
97
.torus-legend span {98
display: inline-flex;99
align-items: center;100
gap: 8px;101
}102
103
.torus-key {104
display: inline-block;105
width: 22px;106
height: 4px;107
border-radius: 3px;108
}109
110
.torus-red { background: var(--red); }111
.torus-green { background: var(--green); }112
113
#tore-board {114
width: 100%;115
height: clamp(260px, 62cqw, 440px);116
border: 0;117
position: relative;118
background: transparent;119
touch-action: none;120
cursor: grab;121
overflow: hidden;122
}123
124
.torus-controls {125
display: grid;126
grid-template-columns: 1fr 1fr;127
gap: 24px;128
margin-top: 8px;129
}130
131
.torus-control-heading {132
display: flex;133
align-items: baseline;134
justify-content: space-between;135
gap: 8px;136
}137
138
.torus-footer {139
justify-content: space-between;140
gap: 8px 20px;141
margin-top: 16px;142
}143
144
.JXGtext { color: var(--foreground); }145
146
@container (max-width: 460px) {147
.torus-controls {148
grid-template-columns: 1fr;149
gap: 16px;150
}151
}152
153
@media (pointer: coarse) {154
.btn { min-height: 44px; }155
.form-range { min-height: 28px; }156
}157
158
@media (max-width: 480px) {159
body { padding: 16px; }160
}161
</style>162
</head>163
164
<body>165
<div id="tore-lacets">166
<div class="viz-row torus-legend">167
<span><i class="torus-key torus-red"></i>Rouge · non contractile</span>168
<span><i class="torus-key torus-green"></i>Vert · contractile</span>169
</div>170
171
<div172
id="tore-board"173
role="img"174
aria-label="Tore en trois dimensions avec deux lacets et des poignées manipulables. Commandes équivalentes sous la figure."175
></div>176
177
<div class="torus-controls">178
<div>179
<div class="torus-control-heading">180
<label for="tore-red-position">Position du rouge</label>181
<output id="tore-red-value" class="tabular-nums" aria-live="off">325°</output>182
</div>183
184
<input185
class="form-range"186
id="tore-red-position"187
type="range"188
min="0"189
max="360"190
step="1"191
value="325"192
>193
194
<button type="button" class="btn" id="tore-red-play">195
Animer le rouge196
</button>197
</div>198
199
<div>200
<div class="torus-control-heading">201
<label for="tore-green-shrink">Contraction du vert</label>202
<output id="tore-green-value" class="tabular-nums" aria-live="off">0 %</output>203
</div>204
205
<input206
class="form-range"207
id="tore-green-shrink"208
type="range"209
min="0"210
max="100"211
step="1"212
value="0"213
>214
215
<button type="button" class="btn" id="tore-green-play">216
Contracter le vert217
</button>218
</div>219
</div>220
221
<div class="viz-row torus-footer">222
<span class="text-small text-muted">223
Poignées : lacets · Glisser le fond : rotation224
</span>225
</div>226
227
<div class="sr-only" id="tore-status" aria-live="polite"></div>228
<div id="tore-error" role="alert" hidden></div>229
</div>230
231
<script src="https://cdn.jsdelivr.net/npm/jsxgraph@1.13.1/distrib/jsxgraphcore.js"></script>232
233
<script>234
(() => {235
'use strict';236
237
const root = document.getElementById('tore-lacets');238
const $ = id => root.querySelector('#' + id);239
240
if (!window.JXG) {241
$('tore-error').hidden = false;242
$('tore-error').textContent =243
'Le chargement de JSXGraph a échoué. Rechargez la figure avec une connexion internet.';244
return;245
}246
247
const TAU = 2 * Math.PI;248
const R = 2.45;249
const r = 0.92;250
251
const state = {252
red: 325 * Math.PI / 180,253
shrink: 0254
};255
256
const greenU = 2.6;257
const greenV = 1.7;258
259
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));260
const wrap = x => ((x % TAU) + TAU) % TAU;261
262
const torus = (u, v) => [263
(R + r * Math.cos(v)) * Math.cos(u),264
(R + r * Math.cos(v)) * Math.sin(u),265
r * Math.sin(v)266
];267
268
const redUV = t => [269
state.red + 0.2 * Math.sin(2 * t),270
t271
];272
273
const greenUV = t => [274
greenU + 0.4 * (1 - state.shrink) * Math.cos(t),275
greenV + 0.82 * (1 - state.shrink) * Math.sin(t)276
];277
278
const at = (uv, t) => torus(...uv(t));279
280
function color(token) {281
const probe = document.createElement('span');282
probe.style.color = 'var(' + token + ')';283
root.appendChild(probe);284
const value = getComputedStyle(probe).color;285
probe.remove();286
return value;287
}288
289
let red = color('--red');290
let green = color('--green');291
let background = color('--background');292
293
const board = JXG.JSXGraph.initBoard('tore-board', {294
boundingbox: [-4.5, 3.15, 4.5, -3.15],295
keepaspectratio: true,296
axis: false,297
grid: false,298
showCopyright: false,299
showNavigation: false,300
pan: { enabled: false },301
zoom: { enabled: false },302
resize: { enabled: true },303
keyboard: { enabled: false },304
renderer: 'svg'305
});306
307
board.suspendUpdate();308
309
const view = board.create('view3d', [310
[-3.7, -3.7],311
[7.4, 7.4],312
[[-3.7, 3.7], [-3.7, 3.7], [-3.7, 3.7]]313
], {314
projection: 'parallel',315
axesPosition: 'none',316
xPlaneRear: { visible: false },317
yPlaneRear: { visible: false },318
zPlaneRear: { visible: false },319
xPlaneFront: { visible: false },320
yPlaneFront: { visible: false },321
zPlaneFront: { visible: false },322
depthOrder: { enabled: true },323
verticalDrag: { enabled: false },324
az: {325
slider: { visible: false, start: 0.85 },326
pointer: { enabled: true, button: 0, key: 'none' }327
},328
el: {329
slider: { visible: false, start: 0.64 },330
pointer: { enabled: true, button: 0, key: 'none' }331
},332
bank: {333
slider: { visible: false },334
pointer: { enabled: false }335
},336
trackball: { enabled: false }337
});338
339
const isDark = () => {340
const numbers = color('--background').match(/[\d.]+/g);341
return numbers &&342
(Number(numbers[0]) + Number(numbers[1]) + Number(numbers[2])) / 3 < 128;343
};344
345
let dark = isDark();346
347
view.create('parametricsurface3d', [348
torus,349
[0, TAU],350
[0, TAU]351
], {352
type: 'shader',353
tiling: 'rectangle',354
stepsU: 64,355
stepsV: 32,356
polyhedron: {357
strokeWidth: 0,358
fillOpacity: 1,359
highlight: false,360
shader: {361
enabled: true,362
fixed: false,363
type: 'angle',364
hue: 0,365
saturation: 0,366
minLightness: () => dark ? 24 : 63,367
maxLightness: () => dark ? 56 : 94,368
light: { type: 1, dir: 0 }369
}370
}371
});372
373
const project = p => view.project3DTo2D(p).slice(1, 3);374
375
function visible(p, eye) {376
let distance = 0.016;377
378
for (let i = 0; i < 90 && distance < 9; i++) {379
const x = p[0] + distance * eye[0];380
const y = p[1] + distance * eye[1];381
const z = p[2] + distance * eye[2];382
383
const sdf = Math.hypot(Math.hypot(x, y) - R, z) - r;384
if (sdf < -0.0005) return false;385
386
distance += Math.max(0.016, sdf * 0.85);387
}388
389
return true;390
}391
392
let curveCacheKey = '';393
let curveCache;394
395
function sampledCurves() {396
const eye = view.matrix3DRot[3].slice(1);397
const key = [398
state.red,399
state.shrink,400
...view.matrix3DRot.flat()401
].join(',');402
403
if (key === curveCacheKey) return curveCache;404
405
curveCacheKey = key;406
407
curveCache = [redUV, greenUV].map(uv => {408
const front = [[], []];409
const back = [[], []];410
let previous = null;411
412
for (let i = 0; i <= 320; i++) {413
const p = at(uv, TAU * i / 320);414
const xy = project(p);415
const isVisible = visible(p, eye);416
417
const target = isVisible ? front : back;418
const other = isVisible ? back : front;419
420
if (previous && previous.visible !== isVisible) {421
target[0].push(previous.xy[0]);422
target[1].push(previous.xy[1]);423
other[0].push(xy[0]);424
other[1].push(xy[1]);425
}426
427
target[0].push(xy[0]);428
target[1].push(xy[1]);429
other[0].push(NaN);430
other[1].push(NaN);431
432
previous = { xy, visible: isVisible };433
}434
435
return { front, back };436
});437
438
return curveCache;439
}440
441
const strokes = [];442
443
function makeStroke(index, side, strokeColor, width, opacity, layer, dash = 0) {444
const curve = board.create('curve', [[], []], {445
strokeColor,446
strokeWidth: width,447
strokeOpacity: opacity,448
dash,449
fixed: true,450
highlight: false,451
layer,452
doAdvancedPlot: false,453
element3D: true454
});455
456
curve.updateDataArray = function () {457
const data = sampledCurves()[index][side];458
this.dataX = data[0];459
this.dataY = data[1];460
};461
462
strokes.push({ curve, index, side, width });463
return curve;464
}465
466
makeStroke(0, 'back', red, 2, 0.55, 14, 2);467
makeStroke(1, 'back', green, 2, 0.55, 14, 2);468
469
makeStroke(0, 'front', background, 7.5, 0.95, 15);470
makeStroke(1, 'front', background, 7.5, 0.95, 15);471
472
makeStroke(0, 'front', red, 4.5, 1, 16);473
makeStroke(1, 'front', green, 4.5, 1, 16);474
475
const redHandleWorld = () => at(redUV, Math.PI / 2);476
const greenHandleWorld = () => at(greenUV, Math.PI / 2);477
478
function handle(world, fillColor, face) {479
return board.create('point', [480
() => project(world())[0],481
() => project(world())[1]482
], {483
name: '',484
withLabel: false,485
face,486
size: 7,487
strokeWidth: 3,488
strokeColor: background,489
fillColor,490
fixed: true,491
highlight: false,492
showInfobox: false,493
layer: 19,494
element3D: true495
});496
}497
498
const redHandle = handle(redHandleWorld, red, 'o');499
const greenHandle = handle(greenHandleWorld, green, '[]');500
501
board.unsuspendUpdate();502
503
const redInput = $('tore-red-position');504
const greenInput = $('tore-green-shrink');505
const redButton = $('tore-red-play');506
const greenButton = $('tore-green-play');507
508
const animations = { red: null, green: null };509
let frame = 0;510
let destroyed = false;511
512
function updateControls() {513
redInput.value = Math.round(wrap(state.red) * 180 / Math.PI);514
greenInput.value = Math.round(state.shrink * 100);515
516
$('tore-red-value').textContent = redInput.value + '°';517
$('tore-green-value').textContent = greenInput.value + ' %';518
519
redButton.textContent = animations.red520
? 'Pause du rouge'521
: 'Animer le rouge';522
523
greenButton.textContent = animations.green524
? 'Pause du vert'525
: state.shrink > 0.999526
? 'Déployer le vert'527
: 'Contracter le vert';528
}529
530
function announce(message) {531
$('tore-status').textContent = message;532
}533
534
function stop(which) {535
animations[which] = null;536
updateControls();537
}538
539
function render() {540
if (destroyed) return;541
updateControls();542
board.update();543
}544
545
function tick(now) {546
frame = 0;547
548
for (const which of ['red', 'green']) {549
const a = animations[which];550
if (!a) continue;551
552
const t = clamp((now - a.start) / a.duration, 0, 1);553
const ease = t * t * (3 - 2 * t);554
555
if (which === 'red') {556
state.red = a.from + TAU * ease;557
} else {558
state.shrink = a.from + (a.to - a.from) * ease;559
}560
561
if (t === 1) {562
animations[which] = null;563
564
announce(565
which === 'red'566
? 'Le lacet rouge a fait un tour et reste non contractile.'567
: state.shrink === 1568
? 'Le lacet vert est contracté en un point.'569
: 'Le lacet vert est déployé.'570
);571
}572
}573
574
render();575
576
if (animations.red || animations.green) {577
frame = requestAnimationFrame(tick);578
}579
}580
581
function animate(which) {582
if (animations[which]) {583
stop(which);584
return;585
}586
587
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;588
const from = which === 'red' ? state.red : state.shrink;589
590
animations[which] = {591
from,592
to: state.shrink > 0.999 ? 0 : 1,593
start: performance.now(),594
duration: reduced ? 500 : which === 'red' ? 7000 : 3200595
};596
597
updateControls();598
599
if (!frame) frame = requestAnimationFrame(tick);600
}601
602
redButton.addEventListener('click', () => animate('red'));603
greenButton.addEventListener('click', () => animate('green'));604
605
redInput.addEventListener('input', () => {606
const value = Number(redInput.value);607
stop('red');608
state.red = value * Math.PI / 180;609
render();610
});611
612
greenInput.addEventListener('input', () => {613
const value = Number(greenInput.value);614
stop('green');615
state.shrink = value / 100;616
render();617
});618
619
const host = $('tore-board');620
let drag = null;621
622
function clientToBoard(event) {623
const rect = host.getBoundingClientRect();624
return [625
event.clientX - rect.left,626
event.clientY - rect.top627
];628
}629
630
function toPixels(p) {631
const xy = project(p);632
return [633
board.origin.scrCoords[1] + xy[0] * board.unitX,634
board.origin.scrCoords[2] - xy[1] * board.unitY635
];636
}637
638
function distance2(a, b) {639
return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;640
}641
642
function nearestParameter(xy, which) {643
const range = which === 'red' ? TAU : 1;644
645
const world = t => which === 'red'646
? torus(t, Math.PI / 2)647
: torus(greenU, greenV + 0.82 * (1 - t));648
649
let best = which === 'red' ? wrap(state.red) : state.shrink;650
let bestDistance = Infinity;651
652
for (let i = 0; i <= 240; i++) {653
const value = range * i / 240;654
const d = distance2(toPixels(world(value)), xy);655
656
if (d < bestDistance) {657
bestDistance = d;658
best = value;659
}660
}661
662
let step = range / 240;663
664
for (let j = 0; j < 10; j++) {665
for (const candidate of [best - step, best + step]) {666
const value = which === 'red'667
? wrap(candidate)668
: clamp(candidate, 0, 1);669
670
const d = distance2(toPixels(world(value)), xy);671
672
if (d < bestDistance) {673
bestDistance = d;674
best = value;675
}676
}677
678
step /= 2;679
}680
681
return best;682
}683
684
host.addEventListener('pointerdown', event => {685
if (event.button !== 0) return;686
687
const xy = clientToBoard(event);688
const dr = distance2(xy, toPixels(redHandleWorld()));689
const dg = distance2(xy, toPixels(greenHandleWorld()));690
691
if (Math.min(dr, dg) > 26 * 26) return;692
693
drag = {694
which: dr < dg ? 'red' : 'green',695
id: event.pointerId696
};697
698
stop(drag.which);699
host.setPointerCapture(event.pointerId);700
host.style.cursor = 'grabbing';701
702
event.preventDefault();703
event.stopImmediatePropagation();704
}, true);705
706
host.addEventListener('pointermove', event => {707
if (!drag) {708
const xy = clientToBoard(event);709
710
host.style.cursor = Math.min(711
distance2(xy, toPixels(redHandleWorld())),712
distance2(xy, toPixels(greenHandleWorld()))713
) < 26 * 26 ? 'pointer' : 'grab';714
715
return;716
}717
718
if (event.pointerId !== drag.id) return;719
720
const value = nearestParameter(clientToBoard(event), drag.which);721
722
if (drag.which === 'red') {723
state.red = value;724
} else {725
state.shrink = value;726
}727
728
render();729
event.preventDefault();730
event.stopImmediatePropagation();731
}, true);732
733
function endDrag(event) {734
if (!drag || event.pointerId !== drag.id) return;735
736
if (host.hasPointerCapture(event.pointerId)) {737
host.releasePointerCapture(event.pointerId);738
}739
740
drag = null;741
host.style.cursor = 'grab';742
event.stopImmediatePropagation();743
}744
745
host.addEventListener('pointerup', endDrag, true);746
host.addEventListener('pointercancel', endDrag, true);747
748
host.addEventListener('lostpointercapture', () => {749
drag = null;750
host.style.cursor = 'grab';751
});752
753
function themeChanged() {754
board.suspendUpdate();755
756
red = color('--red');757
green = color('--green');758
background = color('--background');759
dark = isDark();760
761
strokes.forEach(({ curve, index, width }) => {762
curve.setAttribute({763
strokeColor: width === 7.5764
? background765
: index === 0 ? red : green766
});767
});768
769
redHandle.setAttribute({770
fillColor: red,771
strokeColor: background772
});773
774
greenHandle.setAttribute({775
fillColor: green,776
strokeColor: background777
});778
779
board.unsuspendUpdate();780
}781
782
const themeObserver = new MutationObserver(themeChanged);783
784
themeObserver.observe(document.documentElement, {785
attributes: true,786
attributeFilter: ['class', 'style', 'data-theme']787
});788
789
const appearance = matchMedia('(prefers-color-scheme: dark)');790
appearance.addEventListener('change', themeChanged);791
792
const lifetime = new MutationObserver(() => {793
if (!root.isConnected) destroy();794
});795
796
lifetime.observe(document.body, {797
childList: true,798
subtree: true799
});800
801
function destroy() {802
if (destroyed) return;803
804
destroyed = true;805
cancelAnimationFrame(frame);806
themeObserver.disconnect();807
lifetime.disconnect();808
appearance.removeEventListener('change', themeChanged);809
JXG.JSXGraph.freeBoard(board);810
}811
812
window.addEventListener('pagehide', destroy, { once: true });813
updateControls();814
})();815
</script>816
</body>817
</html>818
```819
820
La déformation des lacets se fait mathématiquement avec l'[[homotopie-de-chemins|homotopie]].821
Ainsi, le groupe fondamental cherche à déterminer quels lacets sont fondamentalement différents les uns des autres, à homotopie près. Ces différentes classes de lacets permettent alors de détecter certains trous.821
822
##### Définition formelle823
824
Le groupe fondamental d'un [[Topological space|espace topologique]] $X$ en un point $x_{0}$ est le [[Group|groupe]] des classes d'homotopie de [[Lacet d'un espace|lacets]] de $X$ basés en $x_{0}$, où l'opération de groupe est induite par la concaténation sur les lacets. Il est dénoté par 825
$$\pi_{1}(X,x_{0}).$$Revision 4771
9/13/2026, 10:52:18 AM · Ancient Tree
Updated text
Compare with revision 4748814 changed lines
1
##### Intuition2
3
Le groupe fondamental est une tentative de formaliser la notion de « trou ».4
Par exemple, un [[tore|tore]] possède un trou. Comment peut-on rendre cette observation mathématique ?5
6
L'idée est de considérer des [[lacet-dun-espace|lacets]] et de les déformer. On peut les imaginer comme des élastiques : s'il est impossible de contracter un lacet en un seul point, c'est qu'il est retenu par un obstacle : autrement dit, un trou.7
8
La déformation des lacets se fait mathématiquent avec l'[[homotopie-de-chemins|homotopie]].8
```jsxgraph9
<!doctype html>10
<html lang="fr">11
<head>12
<meta charset="utf-8">13
<meta name="viewport" content="width=device-width, initial-scale=1">14
<title>Tore et lacets — JSXGraph</title>9
16
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsxgraph@1.13.1/distrib/jsxgraph.css">17
18
<style>19
:root {20
color-scheme: light dark;21
--background: light-dark(#ffffff, #181818);22
--foreground: light-dark(#202020, #eeeeee);23
--muted-foreground: light-dark(#626262, #b4b4b4);24
--red: light-dark(#da4048, #ff7378);25
--green: light-dark(#17864c, #62cf91);26
--button-background: light-dark(#f0f0f0, #343434);27
}28
29
* { box-sizing: border-box; }30
31
body {32
margin: 0;33
padding: 24px;34
font: 15px/1.5 system-ui, sans-serif;35
background: var(--background);36
color: var(--foreground);37
}38
39
#tore-lacets {40
width: 100%;41
max-width: 900px;42
margin: 0 auto;43
container-type: inline-size;44
}45
46
.viz-row {47
display: flex;48
flex-wrap: wrap;49
align-items: center;50
}51
52
.btn {53
display: inline-flex;54
align-items: center;55
justify-content: center;56
padding: 8px 14px;57
border: 0;58
border-radius: 8px;59
cursor: pointer;60
font: inherit;61
color: var(--foreground);62
background: var(--button-background);63
}64
65
.btn:hover { filter: brightness(0.94); }66
67
.form-range {68
display: block;69
width: 100%;70
margin: 12px 0 16px;71
cursor: pointer;72
}73
74
#tore-red-position { accent-color: var(--red); }75
#tore-green-shrink { accent-color: var(--green); }76
.tabular-nums { font-variant-numeric: tabular-nums; }77
.text-small { font-size: 12px; }78
.text-muted { color: var(--muted-foreground); }79
80
.sr-only {81
position: absolute;82
width: 1px;83
height: 1px;84
padding: 0;85
margin: -1px;86
overflow: hidden;87
clip: rect(0, 0, 0, 0);88
white-space: nowrap;89
border: 0;90
}91
92
.torus-legend {93
justify-content: center;94
gap: 12px 24px;95
}96
97
.torus-legend span {98
display: inline-flex;99
align-items: center;100
gap: 8px;101
}102
103
.torus-key {104
display: inline-block;105
width: 22px;106
height: 4px;107
border-radius: 3px;108
}109
110
.torus-red { background: var(--red); }111
.torus-green { background: var(--green); }112
113
#tore-board {114
width: 100%;115
height: clamp(260px, 62cqw, 440px);116
border: 0;117
position: relative;118
background: transparent;119
touch-action: none;120
cursor: grab;121
overflow: hidden;122
}123
124
.torus-controls {125
display: grid;126
grid-template-columns: 1fr 1fr;127
gap: 24px;128
margin-top: 8px;129
}130
131
.torus-control-heading {132
display: flex;133
align-items: baseline;134
justify-content: space-between;135
gap: 8px;136
}137
138
.torus-footer {139
justify-content: space-between;140
gap: 8px 20px;141
margin-top: 16px;142
}143
144
.JXGtext { color: var(--foreground); }145
146
@container (max-width: 460px) {147
.torus-controls {148
grid-template-columns: 1fr;149
gap: 16px;150
}151
}152
153
@media (pointer: coarse) {154
.btn { min-height: 44px; }155
.form-range { min-height: 28px; }156
}157
158
@media (max-width: 480px) {159
body { padding: 16px; }160
}161
</style>162
</head>163
164
<body>165
<div id="tore-lacets">166
<div class="viz-row torus-legend">167
<span><i class="torus-key torus-red"></i>Rouge · non contractile</span>168
<span><i class="torus-key torus-green"></i>Vert · contractile</span>169
</div>170
171
<div172
id="tore-board"173
role="img"174
aria-label="Tore en trois dimensions avec deux lacets et des poignées manipulables. Commandes équivalentes sous la figure."175
></div>176
177
<div class="torus-controls">178
<div>179
<div class="torus-control-heading">180
<label for="tore-red-position">Position du rouge</label>181
<output id="tore-red-value" class="tabular-nums" aria-live="off">325°</output>182
</div>183
184
<input185
class="form-range"186
id="tore-red-position"187
type="range"188
min="0"189
max="360"190
step="1"191
value="325"192
>193
194
<button type="button" class="btn" id="tore-red-play">195
Animer le rouge196
</button>197
</div>198
199
<div>200
<div class="torus-control-heading">201
<label for="tore-green-shrink">Contraction du vert</label>202
<output id="tore-green-value" class="tabular-nums" aria-live="off">0 %</output>203
</div>204
205
<input206
class="form-range"207
id="tore-green-shrink"208
type="range"209
min="0"210
max="100"211
step="1"212
value="0"213
>214
215
<button type="button" class="btn" id="tore-green-play">216
Contracter le vert217
</button>218
</div>219
</div>220
221
<div class="viz-row torus-footer">222
<span class="text-small text-muted">223
Poignées : lacets · Glisser le fond : rotation224
</span>225
</div>226
227
<div class="sr-only" id="tore-status" aria-live="polite"></div>228
<div id="tore-error" role="alert" hidden></div>229
</div>230
231
<script src="https://cdn.jsdelivr.net/npm/jsxgraph@1.13.1/distrib/jsxgraphcore.js"></script>232
233
<script>234
(() => {235
'use strict';236
237
const root = document.getElementById('tore-lacets');238
const $ = id => root.querySelector('#' + id);239
240
if (!window.JXG) {241
$('tore-error').hidden = false;242
$('tore-error').textContent =243
'Le chargement de JSXGraph a échoué. Rechargez la figure avec une connexion internet.';244
return;245
}246
247
const TAU = 2 * Math.PI;248
const R = 2.45;249
const r = 0.92;250
251
const state = {252
red: 325 * Math.PI / 180,253
shrink: 0254
};255
256
const greenU = 2.6;257
const greenV = 1.7;258
259
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));260
const wrap = x => ((x % TAU) + TAU) % TAU;261
262
const torus = (u, v) => [263
(R + r * Math.cos(v)) * Math.cos(u),264
(R + r * Math.cos(v)) * Math.sin(u),265
r * Math.sin(v)266
];267
268
const redUV = t => [269
state.red + 0.2 * Math.sin(2 * t),270
t271
];272
273
const greenUV = t => [274
greenU + 0.4 * (1 - state.shrink) * Math.cos(t),275
greenV + 0.82 * (1 - state.shrink) * Math.sin(t)276
];277
278
const at = (uv, t) => torus(...uv(t));279
280
function color(token) {281
const probe = document.createElement('span');282
probe.style.color = 'var(' + token + ')';283
root.appendChild(probe);284
const value = getComputedStyle(probe).color;285
probe.remove();286
return value;287
}288
289
let red = color('--red');290
let green = color('--green');291
let background = color('--background');292
293
const board = JXG.JSXGraph.initBoard('tore-board', {294
boundingbox: [-4.5, 3.15, 4.5, -3.15],295
keepaspectratio: true,296
axis: false,297
grid: false,298
showCopyright: false,299
showNavigation: false,300
pan: { enabled: false },301
zoom: { enabled: false },302
resize: { enabled: true },303
keyboard: { enabled: false },304
renderer: 'svg'305
});306
307
board.suspendUpdate();308
309
const view = board.create('view3d', [310
[-3.7, -3.7],311
[7.4, 7.4],312
[[-3.7, 3.7], [-3.7, 3.7], [-3.7, 3.7]]313
], {314
projection: 'parallel',315
axesPosition: 'none',316
xPlaneRear: { visible: false },317
yPlaneRear: { visible: false },318
zPlaneRear: { visible: false },319
xPlaneFront: { visible: false },320
yPlaneFront: { visible: false },321
zPlaneFront: { visible: false },322
depthOrder: { enabled: true },323
verticalDrag: { enabled: false },324
az: {325
slider: { visible: false, start: 0.85 },326
pointer: { enabled: true, button: 0, key: 'none' }327
},328
el: {329
slider: { visible: false, start: 0.64 },330
pointer: { enabled: true, button: 0, key: 'none' }331
},332
bank: {333
slider: { visible: false },334
pointer: { enabled: false }335
},336
trackball: { enabled: false }337
});338
339
const isDark = () => {340
const numbers = color('--background').match(/[\d.]+/g);341
return numbers &&342
(Number(numbers[0]) + Number(numbers[1]) + Number(numbers[2])) / 3 < 128;343
};344
345
let dark = isDark();346
347
view.create('parametricsurface3d', [348
torus,349
[0, TAU],350
[0, TAU]351
], {352
type: 'shader',353
tiling: 'rectangle',354
stepsU: 64,355
stepsV: 32,356
polyhedron: {357
strokeWidth: 0,358
fillOpacity: 1,359
highlight: false,360
shader: {361
enabled: true,362
fixed: false,363
type: 'angle',364
hue: 0,365
saturation: 0,366
minLightness: () => dark ? 24 : 63,367
maxLightness: () => dark ? 56 : 94,368
light: { type: 1, dir: 0 }369
}370
}371
});372
373
const project = p => view.project3DTo2D(p).slice(1, 3);374
375
function visible(p, eye) {376
let distance = 0.016;377
378
for (let i = 0; i < 90 && distance < 9; i++) {379
const x = p[0] + distance * eye[0];380
const y = p[1] + distance * eye[1];381
const z = p[2] + distance * eye[2];382
383
const sdf = Math.hypot(Math.hypot(x, y) - R, z) - r;384
if (sdf < -0.0005) return false;385
386
distance += Math.max(0.016, sdf * 0.85);387
}388
389
return true;390
}391
392
let curveCacheKey = '';393
let curveCache;394
395
function sampledCurves() {396
const eye = view.matrix3DRot[3].slice(1);397
const key = [398
state.red,399
state.shrink,400
...view.matrix3DRot.flat()401
].join(',');402
403
if (key === curveCacheKey) return curveCache;404
405
curveCacheKey = key;406
407
curveCache = [redUV, greenUV].map(uv => {408
const front = [[], []];409
const back = [[], []];410
let previous = null;411
412
for (let i = 0; i <= 320; i++) {413
const p = at(uv, TAU * i / 320);414
const xy = project(p);415
const isVisible = visible(p, eye);416
417
const target = isVisible ? front : back;418
const other = isVisible ? back : front;419
420
if (previous && previous.visible !== isVisible) {421
target[0].push(previous.xy[0]);422
target[1].push(previous.xy[1]);423
other[0].push(xy[0]);424
other[1].push(xy[1]);425
}426
427
target[0].push(xy[0]);428
target[1].push(xy[1]);429
other[0].push(NaN);430
other[1].push(NaN);431
432
previous = { xy, visible: isVisible };433
}434
435
return { front, back };436
});437
438
return curveCache;439
}440
441
const strokes = [];442
443
function makeStroke(index, side, strokeColor, width, opacity, layer, dash = 0) {444
const curve = board.create('curve', [[], []], {445
strokeColor,446
strokeWidth: width,447
strokeOpacity: opacity,448
dash,449
fixed: true,450
highlight: false,451
layer,452
doAdvancedPlot: false,453
element3D: true454
});455
456
curve.updateDataArray = function () {457
const data = sampledCurves()[index][side];458
this.dataX = data[0];459
this.dataY = data[1];460
};461
462
strokes.push({ curve, index, side, width });463
return curve;464
}465
466
makeStroke(0, 'back', red, 2, 0.55, 14, 2);467
makeStroke(1, 'back', green, 2, 0.55, 14, 2);468
469
makeStroke(0, 'front', background, 7.5, 0.95, 15);470
makeStroke(1, 'front', background, 7.5, 0.95, 15);471
472
makeStroke(0, 'front', red, 4.5, 1, 16);473
makeStroke(1, 'front', green, 4.5, 1, 16);474
475
const redHandleWorld = () => at(redUV, Math.PI / 2);476
const greenHandleWorld = () => at(greenUV, Math.PI / 2);477
478
function handle(world, fillColor, face) {479
return board.create('point', [480
() => project(world())[0],481
() => project(world())[1]482
], {483
name: '',484
withLabel: false,485
face,486
size: 7,487
strokeWidth: 3,488
strokeColor: background,489
fillColor,490
fixed: true,491
highlight: false,492
showInfobox: false,493
layer: 19,494
element3D: true495
});496
}497
498
const redHandle = handle(redHandleWorld, red, 'o');499
const greenHandle = handle(greenHandleWorld, green, '[]');500
501
board.unsuspendUpdate();502
503
const redInput = $('tore-red-position');504
const greenInput = $('tore-green-shrink');505
const redButton = $('tore-red-play');506
const greenButton = $('tore-green-play');507
508
const animations = { red: null, green: null };509
let frame = 0;510
let destroyed = false;511
512
function updateControls() {513
redInput.value = Math.round(wrap(state.red) * 180 / Math.PI);514
greenInput.value = Math.round(state.shrink * 100);515
516
$('tore-red-value').textContent = redInput.value + '°';517
$('tore-green-value').textContent = greenInput.value + ' %';518
519
redButton.textContent = animations.red520
? 'Pause du rouge'521
: 'Animer le rouge';522
523
greenButton.textContent = animations.green524
? 'Pause du vert'525
: state.shrink > 0.999526
? 'Déployer le vert'527
: 'Contracter le vert';528
}529
530
function announce(message) {531
$('tore-status').textContent = message;532
}533
534
function stop(which) {535
animations[which] = null;536
updateControls();537
}538
539
function render() {540
if (destroyed) return;541
updateControls();542
board.update();543
}544
545
function tick(now) {546
frame = 0;547
548
for (const which of ['red', 'green']) {549
const a = animations[which];550
if (!a) continue;551
552
const t = clamp((now - a.start) / a.duration, 0, 1);553
const ease = t * t * (3 - 2 * t);554
555
if (which === 'red') {556
state.red = a.from + TAU * ease;557
} else {558
state.shrink = a.from + (a.to - a.from) * ease;559
}560
561
if (t === 1) {562
animations[which] = null;563
564
announce(565
which === 'red'566
? 'Le lacet rouge a fait un tour et reste non contractile.'567
: state.shrink === 1568
? 'Le lacet vert est contracté en un point.'569
: 'Le lacet vert est déployé.'570
);571
}572
}573
574
render();575
576
if (animations.red || animations.green) {577
frame = requestAnimationFrame(tick);578
}579
}580
581
function animate(which) {582
if (animations[which]) {583
stop(which);584
return;585
}586
587
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;588
const from = which === 'red' ? state.red : state.shrink;589
590
animations[which] = {591
from,592
to: state.shrink > 0.999 ? 0 : 1,593
start: performance.now(),594
duration: reduced ? 500 : which === 'red' ? 7000 : 3200595
};596
597
updateControls();598
599
if (!frame) frame = requestAnimationFrame(tick);600
}601
602
redButton.addEventListener('click', () => animate('red'));603
greenButton.addEventListener('click', () => animate('green'));604
605
redInput.addEventListener('input', () => {606
const value = Number(redInput.value);607
stop('red');608
state.red = value * Math.PI / 180;609
render();610
});611
612
greenInput.addEventListener('input', () => {613
const value = Number(greenInput.value);614
stop('green');615
state.shrink = value / 100;616
render();617
});618
619
const host = $('tore-board');620
let drag = null;621
622
function clientToBoard(event) {623
const rect = host.getBoundingClientRect();624
return [625
event.clientX - rect.left,626
event.clientY - rect.top627
];628
}629
630
function toPixels(p) {631
const xy = project(p);632
return [633
board.origin.scrCoords[1] + xy[0] * board.unitX,634
board.origin.scrCoords[2] - xy[1] * board.unitY635
];636
}637
638
function distance2(a, b) {639
return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;640
}641
642
function nearestParameter(xy, which) {643
const range = which === 'red' ? TAU : 1;644
645
const world = t => which === 'red'646
? torus(t, Math.PI / 2)647
: torus(greenU, greenV + 0.82 * (1 - t));648
649
let best = which === 'red' ? wrap(state.red) : state.shrink;650
let bestDistance = Infinity;651
652
for (let i = 0; i <= 240; i++) {653
const value = range * i / 240;654
const d = distance2(toPixels(world(value)), xy);655
656
if (d < bestDistance) {657
bestDistance = d;658
best = value;659
}660
}661
662
let step = range / 240;663
664
for (let j = 0; j < 10; j++) {665
for (const candidate of [best - step, best + step]) {666
const value = which === 'red'667
? wrap(candidate)668
: clamp(candidate, 0, 1);669
670
const d = distance2(toPixels(world(value)), xy);671
672
if (d < bestDistance) {673
bestDistance = d;674
best = value;675
}676
}677
678
step /= 2;679
}680
681
return best;682
}683
684
host.addEventListener('pointerdown', event => {685
if (event.button !== 0) return;686
687
const xy = clientToBoard(event);688
const dr = distance2(xy, toPixels(redHandleWorld()));689
const dg = distance2(xy, toPixels(greenHandleWorld()));690
691
if (Math.min(dr, dg) > 26 * 26) return;692
693
drag = {694
which: dr < dg ? 'red' : 'green',695
id: event.pointerId696
};697
698
stop(drag.which);699
host.setPointerCapture(event.pointerId);700
host.style.cursor = 'grabbing';701
702
event.preventDefault();703
event.stopImmediatePropagation();704
}, true);705
706
host.addEventListener('pointermove', event => {707
if (!drag) {708
const xy = clientToBoard(event);709
710
host.style.cursor = Math.min(711
distance2(xy, toPixels(redHandleWorld())),712
distance2(xy, toPixels(greenHandleWorld()))713
) < 26 * 26 ? 'pointer' : 'grab';714
715
return;716
}717
718
if (event.pointerId !== drag.id) return;719
720
const value = nearestParameter(clientToBoard(event), drag.which);721
722
if (drag.which === 'red') {723
state.red = value;724
} else {725
state.shrink = value;726
}727
728
render();729
event.preventDefault();730
event.stopImmediatePropagation();731
}, true);732
733
function endDrag(event) {734
if (!drag || event.pointerId !== drag.id) return;735
736
if (host.hasPointerCapture(event.pointerId)) {737
host.releasePointerCapture(event.pointerId);738
}739
740
drag = null;741
host.style.cursor = 'grab';742
event.stopImmediatePropagation();743
}744
745
host.addEventListener('pointerup', endDrag, true);746
host.addEventListener('pointercancel', endDrag, true);747
748
host.addEventListener('lostpointercapture', () => {749
drag = null;750
host.style.cursor = 'grab';751
});752
753
function themeChanged() {754
board.suspendUpdate();755
756
red = color('--red');757
green = color('--green');758
background = color('--background');759
dark = isDark();760
761
strokes.forEach(({ curve, index, width }) => {762
curve.setAttribute({763
strokeColor: width === 7.5764
? background765
: index === 0 ? red : green766
});767
});768
769
redHandle.setAttribute({770
fillColor: red,771
strokeColor: background772
});773
774
greenHandle.setAttribute({775
fillColor: green,776
strokeColor: background777
});778
779
board.unsuspendUpdate();780
}781
782
const themeObserver = new MutationObserver(themeChanged);783
784
themeObserver.observe(document.documentElement, {785
attributes: true,786
attributeFilter: ['class', 'style', 'data-theme']787
});788
789
const appearance = matchMedia('(prefers-color-scheme: dark)');790
appearance.addEventListener('change', themeChanged);791
792
const lifetime = new MutationObserver(() => {793
if (!root.isConnected) destroy();794
});795
796
lifetime.observe(document.body, {797
childList: true,798
subtree: true799
});800
801
function destroy() {802
if (destroyed) return;803
804
destroyed = true;805
cancelAnimationFrame(frame);806
themeObserver.disconnect();807
lifetime.disconnect();808
appearance.removeEventListener('change', themeChanged);809
JXG.JSXGraph.freeBoard(board);810
}811
812
window.addEventListener('pagehide', destroy, { once: true });813
updateControls();814
})();815
</script>816
</body>817
</html>818
```819
820
La déformation des lacets se fait mathématiquement avec l'[[homotopie-de-chemins|homotopie]].821
10
##### Définition formelle11
12
Le groupe fondamental d'un [[Topological space|espace topologique]] $X$ en un point $x_{0}$ est le [[Group|groupe]] des classes d'homotopie de [[Lacet d'un espace|lacets]] de $X$ basés en $x_{0}$, où l'opération de groupe est induite par la concaténation sur les lacets. Il est dénoté par 13
$$\pi_{1}(X,x_{0}).$$Revision 4748
9/13/2026, 8:02:24 AM · Ancient Tree
Updated text and domain
domainAlgebraic topologyAlgebraic topology
Compare with revision 194711 changed lines
1
##### Intuition2
3
Le groupe fondamental est une tentative de formaliser la notion de « trou ».4
Par exemple, un [[tore|tore]] possède un trou. Comment peut-on rendre cette observation mathématique ?5
6
L'idée est de considérer des [[lacet-dun-espace|lacets]] et de les déformer. On peut les imaginer comme des élastiques : s'il est impossible de contracter un lacet en un seul point, c'est qu'il est retenu par un obstacle : autrement dit, un trou.7
8
La déformation des lacets se fait mathématiquent avec l'[[homotopie-de-chemins|homotopie]].9
10
##### Définition formelle11
1
Le groupe fondamental d'un [[Topological space|espace topologique]] $X$ en un point $x_{0}$ est le [[Group|groupe]] des classes d'homotopie de [[Lacet d'un espace|lacets]] de $X$ basés en $x_{0}$, où l'opération de groupe est induite par la concaténation sur les lacets. Il est dénoté par 2
$$\pi_{1}(X,x_{0}).$$Revision 1947
8/21/2026, 7:44:40 PM · Ancient Tree
Updated text
Compare with revision 14692 changed lines
1
Le groupe fondamental d'un [[Topological space|espace topologique]] $X$ en un point $x_{0}$ est le [[Group|groupe]] des classes d'homotopie de [[loop|lacets]] de $X$ basés en $x_{0}$, où l'opération de groupe est induite par la concaténation sur les lacets. Il est dénoté par 1
Le groupe fondamental d'un [[Topological space|espace topologique]] $X$ en un point $x_{0}$ est le [[Group|groupe]] des classes d'homotopie de [[Lacet d'un espace|lacets]] de $X$ basés en $x_{0}$, où l'opération de groupe est induite par la concaténation sur les lacets. Il est dénoté par 2
$$\pi_{1}(X,x_{0}).$$Revision 1469
8/16/2026, 10:32:43 AM · Ancient Tree
Concept created
Le groupe fondamental d'un [[Topological space|espace topologique]] $X$ en un point $x_{0}$ est le [[Group|groupe]] des classes d'homotopie de [[loop|lacets]] de $X$ basés en $x_{0}$, où l'opération de groupe est induite par la concaténation sur les lacets. Il est dénoté par
$$\pi_{1}(X,x_{0}).$$