Ivan Shishkin, Birch Grove

Groupe fondamental

Concept history

A revision trail for this concept page.

5 revisions

Revision 4772

9/13/2026, 11:00:59 AM · Ancient Tree

Updated text

Compare with revision 47711 changed line
1##### Intuition
2
3Le groupe fondamental est une tentative de formaliser la notion de « trou ».
4Par exemple, un [[tore|tore]] possède un trou. Comment peut-on rendre cette observation mathématique ?
5
6L'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```jsxgraph
9<!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 <div
172 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 <input
185 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 rouge
196 </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 <input
206 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 vert
217 </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 : rotation
224 </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: 0
254 };
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 t
271 ];
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: true
454 });
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: true
495 });
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.red
520 ? 'Pause du rouge'
521 : 'Animer le rouge';
522
523 greenButton.textContent = animations.green
524 ? 'Pause du vert'
525 : state.shrink > 0.999
526 ? '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 === 1
568 ? '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 : 3200
595 };
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.top
627 ];
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.unitY
635 ];
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.pointerId
696 };
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.5
764 ? background
765 : index === 0 ? red : green
766 });
767 });
768
769 redHandle.setAttribute({
770 fillColor: red,
771 strokeColor: background
772 });
773
774 greenHandle.setAttribute({
775 fillColor: green,
776 strokeColor: background
777 });
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: true
799 });
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
820La déformation des lacets se fait mathématiquement avec l'[[homotopie-de-chemins|homotopie]].
821Ainsi, 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 formelle
823
824Le 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##### Intuition
2
3Le groupe fondamental est une tentative de formaliser la notion de « trou ».
4Par exemple, un [[tore|tore]] possède un trou. Comment peut-on rendre cette observation mathématique ?
5
6L'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
8La déformation des lacets se fait mathématiquent avec l'[[homotopie-de-chemins|homotopie]].
8```jsxgraph
9<!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 <div
172 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 <input
185 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 rouge
196 </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 <input
206 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 vert
217 </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 : rotation
224 </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: 0
254 };
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 t
271 ];
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: true
454 });
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: true
495 });
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.red
520 ? 'Pause du rouge'
521 : 'Animer le rouge';
522
523 greenButton.textContent = animations.green
524 ? 'Pause du vert'
525 : state.shrink > 0.999
526 ? '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 === 1
568 ? '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 : 3200
595 };
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.top
627 ];
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.unitY
635 ];
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.pointerId
696 };
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.5
764 ? background
765 : index === 0 ? red : green
766 });
767 });
768
769 redHandle.setAttribute({
770 fillColor: red,
771 strokeColor: background
772 });
773
774 greenHandle.setAttribute({
775 fillColor: green,
776 strokeColor: background
777 });
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: true
799 });
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
820La déformation des lacets se fait mathématiquement avec l'[[homotopie-de-chemins|homotopie]].
821
10##### Définition formelle
11
12Le 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##### Intuition
2
3Le groupe fondamental est une tentative de formaliser la notion de « trou ».
4Par exemple, un [[tore|tore]] possède un trou. Comment peut-on rendre cette observation mathématique ?
5
6L'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
8La déformation des lacets se fait mathématiquent avec l'[[homotopie-de-chemins|homotopie]].
9
10##### Définition formelle
11
1Le 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
1Le 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
1Le 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}).$$