Ivan Shishkin, Birch Grove

Variété différentiable orientable

Concept history

A revision trail for this concept page.

4 revisions

Revision 7818

10/26/2026, 11:49:13 AM · Ancient Tree

Updated text

Compare with revision 78172 changed lines
1##### Intuition
2
3Une [[variete|variété]] est orientable si l'on peut choisir un « sens positif » de manière cohérente sur toute la variété, sans que ce sens s'inverse lorsqu'on se déplace.
4Sur une surface, on peut imaginer choisir un sens de rotation : celui des aiguilles d'une montre ou son inverse.
5```jsxgraph
6{
7<script src="https://cdn.jsdelivr.net/npm/jsxgraph@1.10.1/distrib/jsxgraphcore.js"></script>
8
9<div id="jxgbox" style="
10 width:100%;
11 aspect-ratio:1180 / 480;
12 border:0;
13 background:transparent;
14 overflow:hidden;
15 touch-action:none;
16"></div>
17
18<script>
19(function () {
20 const board = JXG.JSXGraph.initBoard('jxgbox', {
21 boundingbox: [-5.9, 2.7, 5.9, -2.1],
22 keepaspectratio: true,
23 axis: false,
24 grid: false,
25 showNavigation: false,
26 showCopyright: false,
27 pan: { enabled: false },
28 zoom: { enabled: false, wheel: false },
29 keyboard: { enabled: false },
30 renderer: 'svg'
31 });
32
33 const TAU = 2 * Math.PI;
34 const R = 2, W = 0.62, A = 0.34;
35 const BLACK = '#202124';
36 const BLUE = '#2f6fd6';
37 const GRAY = '#8b9097';
38
39 const sy = Math.sin(0.13), cy = Math.cos(0.13);
40 const se = Math.sin(0.64), ce = Math.cos(0.64);
41
42 const clamp = (x, a, b) => Math.max(a, Math.min(b, x));
43 const sub = (a, b) => a.map((x, i) => x - b[i]);
44 const cross = (a, b) => [
45 a[1]*b[2] - a[2]*b[1],
46 a[2]*b[0] - a[0]*b[2],
47 a[0]*b[1] - a[1]*b[0]
48 ];
49 const unit = a => a.map(x => x / Math.hypot(...a));
50 const light = unit([-0.4, -0.5, 1]);
51
52 // k = 0 : cylindre ; k = 1 : Möbius.
53 function point(k, u, v) {
54 const t = u - Math.PI/2;
55 const a = k*u/2;
56 const r = R + v*Math.sin(a);
57
58 return [
59 r*Math.cos(t),
60 r*Math.sin(t),
61 v*Math.cos(a)
62 ];
63 }
64
65 function project(k, p) {
66 const [x, y, z] = p;
67
68 return [
69 (k ? 3 : -3) + x*cy + y*sy,
70 -x*sy*se + y*cy*se + z*ce,
71 x*sy*ce - y*cy*ce + z*se
72 ];
73 }
74
75 const at = (k, u, v) => project(k, point(k, u, v));
76
77 const path = ps => 'M' + ps.map(p =>
78 ((p[0]+5.9)*100).toFixed(2) + ',' +
79 ((2.1-p[1])*100).toFixed(2)
80 ).join('L');
81
82 // Surfaces vectorielles fixes et contours noirs.
83 function surface(k) {
84 const pieces = [];
85 const du = TAU/180;
86
87 for (let i = 0; i < 180; i++) {
88 const u = i*du, m = u+du/2, e = 0.0001;
89 const n = unit(cross(
90 sub(point(k,m+e,0), point(k,m-e,0)),
91 sub(point(k,m,e), point(k,m,-e))
92 ));
93
94 const shade = 0.15 + 0.08 * (
95 1 - Math.abs(n.reduce((s,x,j) => s+x*light[j], 0))
96 );
97 const c = Math.round(255*(1-shade) + 32*shade);
98
99 pieces.push({
100 ps: [
101 at(k,u,-W), at(k,u+du,-W),
102 at(k,u+du,W), at(k,u,W)
103 ],
104 color: `rgb(${c},${c},${c})`
105 });
106
107 for (const v of [-W,W]) {
108 pieces.push({
109 ps: [at(k,u,v), at(k,u+du,v)],
110 edge: true
111 });
112 }
113 }
114
115 pieces.forEach(p => {
116 p.depth = p.ps.reduce((s,q) => s+q[2], 0) /
117 p.ps.length + (p.edge ? 0.012 : 0);
118 });
119 pieces.sort((a,b) => a.depth-b.depth);
120
121 const faces = pieces.filter(p => !p.edge);
122
123 const filter = `
124 <filter id="rim${k}"
125 x="-5%" y="-5%" width="110%" height="110%"
126 color-interpolation-filters="sRGB">
127 <feComponentTransfer in="SourceAlpha" result="solid">
128 <feFuncA type="linear" slope="2"/>
129 </feComponentTransfer>
130 <feMorphology in="solid" operator="dilate"
131 radius="0.95" result="outer"/>
132 <feMorphology in="solid" operator="erode"
133 radius="0.75" result="inner"/>
134 <feComposite in="outer" in2="inner"
135 operator="out" result="outline"/>
136 <feFlood flood-color="${BLACK}" result="ink"/>
137 <feComposite in="ink" in2="outline" operator="in"/>
138 </filter>`;
139
140 const mesh = pieces.map(p => p.edge
141 ? `<path d="${path(p.ps)}" fill="none"
142 stroke="${BLACK}" stroke-width="1.7"
143 stroke-linecap="round"/>`
144 : `<path d="${path(p.ps)}Z" fill="${p.color}"
145 stroke="${p.color}" stroke-width="1"/>`
146 ).join('');
147
148 const outline = faces.map(p =>
149 `<path d="${path(p.ps)}Z" fill="black"
150 stroke="black" stroke-width="1"/>`
151 ).join('');
152
153 const route = Array.from(
154 {length:161}, (_,i) => at(k,i*TAU/160,0)
155 );
156
157 return `
158 <defs>${filter}</defs>
159 ${mesh}
160 <g filter="url(#rim${k})">${outline}</g>
161 <path d="${path(route)}" fill="none"
162 stroke="${GRAY}" stroke-width="1"
163 stroke-dasharray="3 6" opacity="0.38"/>`;
164 }
165
166 board.suspendUpdate();
167
168 const svg = `
169 <svg xmlns="http://www.w3.org/2000/svg"
170 width="1180" height="420" viewBox="0 0 1180 420">
171 ${surface(0)}
172 ${surface(1)}
173 </svg>`;
174
175 board.create('image', [
176 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg),
177 [-5.9,-2.1],
178 [11.8,4.2]
179 ], {
180 fixed: true,
181 highlight: false,
182 layer: 0
183 });
184
185 const slider = board.create('slider', [
186 [-5.35,2.35], [5.35,2.35], [0,0,1]
187 ], {
188 name: '',
189 withLabel: false,
190 withTicks: false,
191 snapWidth: 0.005,
192 size: 5,
193 strokeColor: BLACK,
194 fillColor: BLACK,
195 highlight: false,
196 baseline: {
197 strokeColor: '#d2d5da',
198 strokeWidth: 3,
199 highlight: false,
200 fixed: true
201 },
202 highline: {
203 strokeColor: BLACK,
204 strokeWidth: 3,
205 highlight: false
206 },
207 point1: { visible:false, fixed:true },
208 point2: { visible:false, fixed:true }
209 });
210
211 // Transport continu du cercle dans les coordonnées du ruban.
212 function arrow(k, u, reference) {
213 const ps = Array.from({length:81}, (_,i) => {
214 const a = 0.42 + (TAU-0.84)*i/80;
215 return at(k, u+A*Math.cos(a)/R, A*Math.sin(a));
216 });
217
218 // Pointe calculée en pixels pour rester bien visible.
219 const screen = ps.map(p => [
220 p[0]*board.unitX,
221 p[1]*board.unitY
222 ]);
223
224 const tip = screen[80], prev = screen[79];
225 const dx = tip[0]-prev[0], dy = tip[1]-prev[1];
226 const d = Math.hypot(dx,dy) || 1;
227 const tx = dx/d, ty = dy/d;
228
229 const len = clamp(
230 board.unitX*0.17,
231 reference ? 5 : 6,
232 reference ? 9 : 11
233 );
234
235 const head = [
236 tip,
237 [
238 tip[0]-len*tx-len*0.58*ty,
239 tip[1]-len*ty+len*0.58*tx
240 ],
241 [
242 tip[0]-len*tx+len*0.58*ty,
243 tip[1]-len*ty-len*0.58*tx
244 ],
245 tip
246 ];
247
248 const body = screen.slice();
249 let remaining = len*0.72;
250
251 while (body.length > 2) {
252 const b = body[body.length-1];
253 const a = body[body.length-2];
254 const distance = Math.hypot(b[0]-a[0], b[1]-a[1]);
255
256 if (distance > remaining) {
257 body[body.length-1] = b.map((x,j) =>
258 x + (a[j]-x)*remaining/distance
259 );
260 break;
261 }
262
263 remaining -= distance;
264 body.pop();
265 }
266
267 const coords = points => points.map(p => [
268 p[0]/board.unitX,
269 p[1]/board.unitY
270 ]);
271
272 return { body:coords(body), head:coords(head) };
273 }
274
275 function marker(k, reference) {
276 const color = reference ? GRAY : BLUE;
277
278 for (const part of ['body','head']) {
279 const curve = board.create('curve', [[0],[0]], {
280 fixed: true,
281 highlight: false,
282 withLabel: false,
283 layer: reference ? 5 : 6,
284 visible: () => !reference || slider.Value() >= 0.015,
285 strokeColor: color,
286 fillColor: color,
287 strokeWidth: part === 'head'
288 ? 0 : (reference ? 1.5 : 2.4),
289 fillOpacity: part === 'head'
290 ? (reference ? 0.75 : 1) : 0,
291 strokeOpacity: reference ? 0.65 : 1,
292 dash: reference && part === 'body' ? 2 : 0
293 });
294
295 curve.updateDataArray = function () {
296 const points = arrow(
297 k,
298 reference ? 0 : slider.Value()*TAU,
299 reference
300 )[part];
301
302 this.dataX = points.map(p => p[0]);
303 this.dataY = points.map(p => p[1]);
304 };
305 }
306 }
307
308 for (const k of [0,1]) {
309 marker(k,true);
310 marker(k,false);
311 }
312
313 board.unsuspendUpdate();
314})();
315</script>
316}
317```
318*Tirer le curseur pour visualiser : un [[cylindre|cylindre]] est une [[surface|surface]] orientable. Mais un [[ruban de Möbius|ruban de Möbius]], lorsqu'on effectue un tour complet en transportant cette orientation locale, on revient au point de départ avec le sens opposé. Le ruban de Möbius n'est donc pas orientable.*
319
320##### Définition formelle
321
322Soit $M$ une [[variete_differentiable|variété différentiable]] de dimension $n$.
323
324On dit que $M$ est orientable s'il existe un atlas orienté $(U_i,\varphi_i)_{i\in I}$, c'est-à-dire que, pour tous $i,j\in I$, les [[Fonction de transition|fonctions de transition]] sont de déterminant strictement positif :
324On dit que $M$ est orientable s'il existe un [[atlas-differentiable|atlas]] orienté $(U_i,\varphi_i)_{i\in I}$, c'est-à-dire que, pour tous $i,j\in I$, les [[Fonction de transition|fonctions de transition]] sont de déterminant strictement positif :
325$$
326\det D(\varphi_j\circ\varphi_i^{-1})>0
327$$
328en tout point de $\varphi_i(U_i\cap U_j)$.

Revision 7817

10/26/2026, 11:48:41 AM · Ancient Tree

Updated text

Compare with revision 78162 changed lines
1##### Intuition
2
3Une [[variete|variété]] est orientable si l'on peut choisir un « sens positif » de manière cohérente sur toute la variété, sans que ce sens s'inverse lorsqu'on se déplace.
4Sur une surface, on peut imaginer choisir un sens de rotation : celui des aiguilles d'une montre ou son inverse.
5```jsxgraph
6{
7<script src="https://cdn.jsdelivr.net/npm/jsxgraph@1.10.1/distrib/jsxgraphcore.js"></script>
8
9<div id="jxgbox" style="
10 width:100%;
11 aspect-ratio:1180 / 480;
12 border:0;
13 background:transparent;
14 overflow:hidden;
15 touch-action:none;
16"></div>
17
18<script>
19(function () {
20 const board = JXG.JSXGraph.initBoard('jxgbox', {
21 boundingbox: [-5.9, 2.7, 5.9, -2.1],
22 keepaspectratio: true,
23 axis: false,
24 grid: false,
25 showNavigation: false,
26 showCopyright: false,
27 pan: { enabled: false },
28 zoom: { enabled: false, wheel: false },
29 keyboard: { enabled: false },
30 renderer: 'svg'
31 });
32
33 const TAU = 2 * Math.PI;
34 const R = 2, W = 0.62, A = 0.34;
35 const BLACK = '#202124';
36 const BLUE = '#2f6fd6';
37 const GRAY = '#8b9097';
38
39 const sy = Math.sin(0.13), cy = Math.cos(0.13);
40 const se = Math.sin(0.64), ce = Math.cos(0.64);
41
42 const clamp = (x, a, b) => Math.max(a, Math.min(b, x));
43 const sub = (a, b) => a.map((x, i) => x - b[i]);
44 const cross = (a, b) => [
45 a[1]*b[2] - a[2]*b[1],
46 a[2]*b[0] - a[0]*b[2],
47 a[0]*b[1] - a[1]*b[0]
48 ];
49 const unit = a => a.map(x => x / Math.hypot(...a));
50 const light = unit([-0.4, -0.5, 1]);
51
52 // k = 0 : cylindre ; k = 1 : Möbius.
53 function point(k, u, v) {
54 const t = u - Math.PI/2;
55 const a = k*u/2;
56 const r = R + v*Math.sin(a);
57
58 return [
59 r*Math.cos(t),
60 r*Math.sin(t),
61 v*Math.cos(a)
62 ];
63 }
64
65 function project(k, p) {
66 const [x, y, z] = p;
67
68 return [
69 (k ? 3 : -3) + x*cy + y*sy,
70 -x*sy*se + y*cy*se + z*ce,
71 x*sy*ce - y*cy*ce + z*se
72 ];
73 }
74
75 const at = (k, u, v) => project(k, point(k, u, v));
76
77 const path = ps => 'M' + ps.map(p =>
78 ((p[0]+5.9)*100).toFixed(2) + ',' +
79 ((2.1-p[1])*100).toFixed(2)
80 ).join('L');
81
82 // Surfaces vectorielles fixes et contours noirs.
83 function surface(k) {
84 const pieces = [];
85 const du = TAU/180;
86
87 for (let i = 0; i < 180; i++) {
88 const u = i*du, m = u+du/2, e = 0.0001;
89 const n = unit(cross(
90 sub(point(k,m+e,0), point(k,m-e,0)),
91 sub(point(k,m,e), point(k,m,-e))
92 ));
93
94 const shade = 0.15 + 0.08 * (
95 1 - Math.abs(n.reduce((s,x,j) => s+x*light[j], 0))
96 );
97 const c = Math.round(255*(1-shade) + 32*shade);
98
99 pieces.push({
100 ps: [
101 at(k,u,-W), at(k,u+du,-W),
102 at(k,u+du,W), at(k,u,W)
103 ],
104 color: `rgb(${c},${c},${c})`
105 });
106
107 for (const v of [-W,W]) {
108 pieces.push({
109 ps: [at(k,u,v), at(k,u+du,v)],
110 edge: true
111 });
112 }
113 }
114
115 pieces.forEach(p => {
116 p.depth = p.ps.reduce((s,q) => s+q[2], 0) /
117 p.ps.length + (p.edge ? 0.012 : 0);
118 });
119 pieces.sort((a,b) => a.depth-b.depth);
120
121 const faces = pieces.filter(p => !p.edge);
122
123 const filter = `
124 <filter id="rim${k}"
125 x="-5%" y="-5%" width="110%" height="110%"
126 color-interpolation-filters="sRGB">
127 <feComponentTransfer in="SourceAlpha" result="solid">
128 <feFuncA type="linear" slope="2"/>
129 </feComponentTransfer>
130 <feMorphology in="solid" operator="dilate"
131 radius="0.95" result="outer"/>
132 <feMorphology in="solid" operator="erode"
133 radius="0.75" result="inner"/>
134 <feComposite in="outer" in2="inner"
135 operator="out" result="outline"/>
136 <feFlood flood-color="${BLACK}" result="ink"/>
137 <feComposite in="ink" in2="outline" operator="in"/>
138 </filter>`;
139
140 const mesh = pieces.map(p => p.edge
141 ? `<path d="${path(p.ps)}" fill="none"
142 stroke="${BLACK}" stroke-width="1.7"
143 stroke-linecap="round"/>`
144 : `<path d="${path(p.ps)}Z" fill="${p.color}"
145 stroke="${p.color}" stroke-width="1"/>`
146 ).join('');
147
148 const outline = faces.map(p =>
149 `<path d="${path(p.ps)}Z" fill="black"
150 stroke="black" stroke-width="1"/>`
151 ).join('');
152
153 const route = Array.from(
154 {length:161}, (_,i) => at(k,i*TAU/160,0)
155 );
156
157 return `
158 <defs>${filter}</defs>
159 ${mesh}
160 <g filter="url(#rim${k})">${outline}</g>
161 <path d="${path(route)}" fill="none"
162 stroke="${GRAY}" stroke-width="1"
163 stroke-dasharray="3 6" opacity="0.38"/>`;
164 }
165
166 board.suspendUpdate();
167
168 const svg = `
169 <svg xmlns="http://www.w3.org/2000/svg"
170 width="1180" height="420" viewBox="0 0 1180 420">
171 ${surface(0)}
172 ${surface(1)}
173 </svg>`;
174
175 board.create('image', [
176 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg),
177 [-5.9,-2.1],
178 [11.8,4.2]
179 ], {
180 fixed: true,
181 highlight: false,
182 layer: 0
183 });
184
185 const slider = board.create('slider', [
186 [-5.35,2.35], [5.35,2.35], [0,0,1]
187 ], {
188 name: '',
189 withLabel: false,
190 withTicks: false,
191 snapWidth: 0.005,
192 size: 5,
193 strokeColor: BLACK,
194 fillColor: BLACK,
195 highlight: false,
196 baseline: {
197 strokeColor: '#d2d5da',
198 strokeWidth: 3,
199 highlight: false,
200 fixed: true
201 },
202 highline: {
203 strokeColor: BLACK,
204 strokeWidth: 3,
205 highlight: false
206 },
207 point1: { visible:false, fixed:true },
208 point2: { visible:false, fixed:true }
209 });
210
211 // Transport continu du cercle dans les coordonnées du ruban.
212 function arrow(k, u, reference) {
213 const ps = Array.from({length:81}, (_,i) => {
214 const a = 0.42 + (TAU-0.84)*i/80;
215 return at(k, u+A*Math.cos(a)/R, A*Math.sin(a));
216 });
217
218 // Pointe calculée en pixels pour rester bien visible.
219 const screen = ps.map(p => [
220 p[0]*board.unitX,
221 p[1]*board.unitY
222 ]);
223
224 const tip = screen[80], prev = screen[79];
225 const dx = tip[0]-prev[0], dy = tip[1]-prev[1];
226 const d = Math.hypot(dx,dy) || 1;
227 const tx = dx/d, ty = dy/d;
228
229 const len = clamp(
230 board.unitX*0.17,
231 reference ? 5 : 6,
232 reference ? 9 : 11
233 );
234
235 const head = [
236 tip,
237 [
238 tip[0]-len*tx-len*0.58*ty,
239 tip[1]-len*ty+len*0.58*tx
240 ],
241 [
242 tip[0]-len*tx+len*0.58*ty,
243 tip[1]-len*ty-len*0.58*tx
244 ],
245 tip
246 ];
247
248 const body = screen.slice();
249 let remaining = len*0.72;
250
251 while (body.length > 2) {
252 const b = body[body.length-1];
253 const a = body[body.length-2];
254 const distance = Math.hypot(b[0]-a[0], b[1]-a[1]);
255
256 if (distance > remaining) {
257 body[body.length-1] = b.map((x,j) =>
258 x + (a[j]-x)*remaining/distance
259 );
260 break;
261 }
262
263 remaining -= distance;
264 body.pop();
265 }
266
267 const coords = points => points.map(p => [
268 p[0]/board.unitX,
269 p[1]/board.unitY
270 ]);
271
272 return { body:coords(body), head:coords(head) };
273 }
274
275 function marker(k, reference) {
276 const color = reference ? GRAY : BLUE;
277
278 for (const part of ['body','head']) {
279 const curve = board.create('curve', [[0],[0]], {
280 fixed: true,
281 highlight: false,
282 withLabel: false,
283 layer: reference ? 5 : 6,
284 visible: () => !reference || slider.Value() >= 0.015,
285 strokeColor: color,
286 fillColor: color,
287 strokeWidth: part === 'head'
288 ? 0 : (reference ? 1.5 : 2.4),
289 fillOpacity: part === 'head'
290 ? (reference ? 0.75 : 1) : 0,
291 strokeOpacity: reference ? 0.65 : 1,
292 dash: reference && part === 'body' ? 2 : 0
293 });
294
295 curve.updateDataArray = function () {
296 const points = arrow(
297 k,
298 reference ? 0 : slider.Value()*TAU,
299 reference
300 )[part];
301
302 this.dataX = points.map(p => p[0]);
303 this.dataY = points.map(p => p[1]);
304 };
305 }
306 }
307
308 for (const k of [0,1]) {
309 marker(k,true);
310 marker(k,false);
311 }
312
313 board.unsuspendUpdate();
314})();
315</script>
316}
317```
318*Un [[cylindre|cylindre]] est une [[surface|surface]] orientable. Mais un [[ruban de Möbius|ruban de Möbius]], lorsqu'on effectue un tour complet en transportant cette orientation locale, on revient au point de départ avec le sens opposé. Le ruban de Möbius n'est donc pas orientable.*
318*Tirer le curseur pour visualiser : un [[cylindre|cylindre]] est une [[surface|surface]] orientable. Mais un [[ruban de Möbius|ruban de Möbius]], lorsqu'on effectue un tour complet en transportant cette orientation locale, on revient au point de départ avec le sens opposé. Le ruban de Möbius n'est donc pas orientable.*
319
320##### Définition formelle
321
322Soit $M$ une [[variete_differentiable|variété différentiable]] de dimension $n$.
323
324On dit que $M$ est orientable s'il existe un atlas orienté $(U_i,\varphi_i)_{i\in I}$, c'est-à-dire que, pour tous $i,j\in I$, les [[Fonction de transition|fonctions de transition]] sont de déterminant strictement positif :
325$$
326\det D(\varphi_j\circ\varphi_i^{-1})>0
327$$
328en tout point de $\varphi_i(U_i\cap U_j)$.

Revision 7816

10/26/2026, 11:48:08 AM · Ancient Tree

Updated text

Compare with revision 7815318 changed lines
1##### Intuition
2
3Une [[variete|variété]] est orientable si l'on peut choisir un « sens positif » de manière cohérente sur toute la variété, sans que ce sens s'inverse lorsqu'on se déplace.
4Sur une surface, on peut imaginer choisir un sens de rotation : celui des aiguilles d'une montre ou son inverse.
5![non orientable](https://s3.pub2.infomaniak.cloud/object/v1/AUTH_7cc517879b0040959f7d12abb1f0e72d/mathwoods-images/uploads/2026/10/user-1/1793013090182-f4210aa95c55b2b2-non-orientable.png)
5```jsxgraph
6*Sur un [[ruban de Möbius|ruban de Möbius]], lorsqu'on effectue un tour complet en transportant cette orientation locale, on revient au point de départ avec le sens opposé. Le ruban de Möbius n'est donc pas orientable.*
6{
7<script src="https://cdn.jsdelivr.net/npm/jsxgraph@1.10.1/distrib/jsxgraphcore.js"></script>
7
9<div id="jxgbox" style="
10 width:100%;
11 aspect-ratio:1180 / 480;
12 border:0;
13 background:transparent;
14 overflow:hidden;
15 touch-action:none;
16"></div>
17
18<script>
19(function () {
20 const board = JXG.JSXGraph.initBoard('jxgbox', {
21 boundingbox: [-5.9, 2.7, 5.9, -2.1],
22 keepaspectratio: true,
23 axis: false,
24 grid: false,
25 showNavigation: false,
26 showCopyright: false,
27 pan: { enabled: false },
28 zoom: { enabled: false, wheel: false },
29 keyboard: { enabled: false },
30 renderer: 'svg'
31 });
32
33 const TAU = 2 * Math.PI;
34 const R = 2, W = 0.62, A = 0.34;
35 const BLACK = '#202124';
36 const BLUE = '#2f6fd6';
37 const GRAY = '#8b9097';
38
39 const sy = Math.sin(0.13), cy = Math.cos(0.13);
40 const se = Math.sin(0.64), ce = Math.cos(0.64);
41
42 const clamp = (x, a, b) => Math.max(a, Math.min(b, x));
43 const sub = (a, b) => a.map((x, i) => x - b[i]);
44 const cross = (a, b) => [
45 a[1]*b[2] - a[2]*b[1],
46 a[2]*b[0] - a[0]*b[2],
47 a[0]*b[1] - a[1]*b[0]
48 ];
49 const unit = a => a.map(x => x / Math.hypot(...a));
50 const light = unit([-0.4, -0.5, 1]);
51
52 // k = 0 : cylindre ; k = 1 : Möbius.
53 function point(k, u, v) {
54 const t = u - Math.PI/2;
55 const a = k*u/2;
56 const r = R + v*Math.sin(a);
57
58 return [
59 r*Math.cos(t),
60 r*Math.sin(t),
61 v*Math.cos(a)
62 ];
63 }
64
65 function project(k, p) {
66 const [x, y, z] = p;
67
68 return [
69 (k ? 3 : -3) + x*cy + y*sy,
70 -x*sy*se + y*cy*se + z*ce,
71 x*sy*ce - y*cy*ce + z*se
72 ];
73 }
74
75 const at = (k, u, v) => project(k, point(k, u, v));
76
77 const path = ps => 'M' + ps.map(p =>
78 ((p[0]+5.9)*100).toFixed(2) + ',' +
79 ((2.1-p[1])*100).toFixed(2)
80 ).join('L');
81
82 // Surfaces vectorielles fixes et contours noirs.
83 function surface(k) {
84 const pieces = [];
85 const du = TAU/180;
86
87 for (let i = 0; i < 180; i++) {
88 const u = i*du, m = u+du/2, e = 0.0001;
89 const n = unit(cross(
90 sub(point(k,m+e,0), point(k,m-e,0)),
91 sub(point(k,m,e), point(k,m,-e))
92 ));
93
94 const shade = 0.15 + 0.08 * (
95 1 - Math.abs(n.reduce((s,x,j) => s+x*light[j], 0))
96 );
97 const c = Math.round(255*(1-shade) + 32*shade);
98
99 pieces.push({
100 ps: [
101 at(k,u,-W), at(k,u+du,-W),
102 at(k,u+du,W), at(k,u,W)
103 ],
104 color: `rgb(${c},${c},${c})`
105 });
106
107 for (const v of [-W,W]) {
108 pieces.push({
109 ps: [at(k,u,v), at(k,u+du,v)],
110 edge: true
111 });
112 }
113 }
114
115 pieces.forEach(p => {
116 p.depth = p.ps.reduce((s,q) => s+q[2], 0) /
117 p.ps.length + (p.edge ? 0.012 : 0);
118 });
119 pieces.sort((a,b) => a.depth-b.depth);
120
121 const faces = pieces.filter(p => !p.edge);
122
123 const filter = `
124 <filter id="rim${k}"
125 x="-5%" y="-5%" width="110%" height="110%"
126 color-interpolation-filters="sRGB">
127 <feComponentTransfer in="SourceAlpha" result="solid">
128 <feFuncA type="linear" slope="2"/>
129 </feComponentTransfer>
130 <feMorphology in="solid" operator="dilate"
131 radius="0.95" result="outer"/>
132 <feMorphology in="solid" operator="erode"
133 radius="0.75" result="inner"/>
134 <feComposite in="outer" in2="inner"
135 operator="out" result="outline"/>
136 <feFlood flood-color="${BLACK}" result="ink"/>
137 <feComposite in="ink" in2="outline" operator="in"/>
138 </filter>`;
139
140 const mesh = pieces.map(p => p.edge
141 ? `<path d="${path(p.ps)}" fill="none"
142 stroke="${BLACK}" stroke-width="1.7"
143 stroke-linecap="round"/>`
144 : `<path d="${path(p.ps)}Z" fill="${p.color}"
145 stroke="${p.color}" stroke-width="1"/>`
146 ).join('');
147
148 const outline = faces.map(p =>
149 `<path d="${path(p.ps)}Z" fill="black"
150 stroke="black" stroke-width="1"/>`
151 ).join('');
152
153 const route = Array.from(
154 {length:161}, (_,i) => at(k,i*TAU/160,0)
155 );
156
157 return `
158 <defs>${filter}</defs>
159 ${mesh}
160 <g filter="url(#rim${k})">${outline}</g>
161 <path d="${path(route)}" fill="none"
162 stroke="${GRAY}" stroke-width="1"
163 stroke-dasharray="3 6" opacity="0.38"/>`;
164 }
165
166 board.suspendUpdate();
167
168 const svg = `
169 <svg xmlns="http://www.w3.org/2000/svg"
170 width="1180" height="420" viewBox="0 0 1180 420">
171 ${surface(0)}
172 ${surface(1)}
173 </svg>`;
174
175 board.create('image', [
176 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg),
177 [-5.9,-2.1],
178 [11.8,4.2]
179 ], {
180 fixed: true,
181 highlight: false,
182 layer: 0
183 });
184
185 const slider = board.create('slider', [
186 [-5.35,2.35], [5.35,2.35], [0,0,1]
187 ], {
188 name: '',
189 withLabel: false,
190 withTicks: false,
191 snapWidth: 0.005,
192 size: 5,
193 strokeColor: BLACK,
194 fillColor: BLACK,
195 highlight: false,
196 baseline: {
197 strokeColor: '#d2d5da',
198 strokeWidth: 3,
199 highlight: false,
200 fixed: true
201 },
202 highline: {
203 strokeColor: BLACK,
204 strokeWidth: 3,
205 highlight: false
206 },
207 point1: { visible:false, fixed:true },
208 point2: { visible:false, fixed:true }
209 });
210
211 // Transport continu du cercle dans les coordonnées du ruban.
212 function arrow(k, u, reference) {
213 const ps = Array.from({length:81}, (_,i) => {
214 const a = 0.42 + (TAU-0.84)*i/80;
215 return at(k, u+A*Math.cos(a)/R, A*Math.sin(a));
216 });
217
218 // Pointe calculée en pixels pour rester bien visible.
219 const screen = ps.map(p => [
220 p[0]*board.unitX,
221 p[1]*board.unitY
222 ]);
223
224 const tip = screen[80], prev = screen[79];
225 const dx = tip[0]-prev[0], dy = tip[1]-prev[1];
226 const d = Math.hypot(dx,dy) || 1;
227 const tx = dx/d, ty = dy/d;
228
229 const len = clamp(
230 board.unitX*0.17,
231 reference ? 5 : 6,
232 reference ? 9 : 11
233 );
234
235 const head = [
236 tip,
237 [
238 tip[0]-len*tx-len*0.58*ty,
239 tip[1]-len*ty+len*0.58*tx
240 ],
241 [
242 tip[0]-len*tx+len*0.58*ty,
243 tip[1]-len*ty-len*0.58*tx
244 ],
245 tip
246 ];
247
248 const body = screen.slice();
249 let remaining = len*0.72;
250
251 while (body.length > 2) {
252 const b = body[body.length-1];
253 const a = body[body.length-2];
254 const distance = Math.hypot(b[0]-a[0], b[1]-a[1]);
255
256 if (distance > remaining) {
257 body[body.length-1] = b.map((x,j) =>
258 x + (a[j]-x)*remaining/distance
259 );
260 break;
261 }
262
263 remaining -= distance;
264 body.pop();
265 }
266
267 const coords = points => points.map(p => [
268 p[0]/board.unitX,
269 p[1]/board.unitY
270 ]);
271
272 return { body:coords(body), head:coords(head) };
273 }
274
275 function marker(k, reference) {
276 const color = reference ? GRAY : BLUE;
277
278 for (const part of ['body','head']) {
279 const curve = board.create('curve', [[0],[0]], {
280 fixed: true,
281 highlight: false,
282 withLabel: false,
283 layer: reference ? 5 : 6,
284 visible: () => !reference || slider.Value() >= 0.015,
285 strokeColor: color,
286 fillColor: color,
287 strokeWidth: part === 'head'
288 ? 0 : (reference ? 1.5 : 2.4),
289 fillOpacity: part === 'head'
290 ? (reference ? 0.75 : 1) : 0,
291 strokeOpacity: reference ? 0.65 : 1,
292 dash: reference && part === 'body' ? 2 : 0
293 });
294
295 curve.updateDataArray = function () {
296 const points = arrow(
297 k,
298 reference ? 0 : slider.Value()*TAU,
299 reference
300 )[part];
301
302 this.dataX = points.map(p => p[0]);
303 this.dataY = points.map(p => p[1]);
304 };
305 }
306 }
307
308 for (const k of [0,1]) {
309 marker(k,true);
310 marker(k,false);
311 }
312
313 board.unsuspendUpdate();
314})();
315</script>
316}
317```
318*Un [[cylindre|cylindre]] est une [[surface|surface]] orientable. Mais un [[ruban de Möbius|ruban de Möbius]], lorsqu'on effectue un tour complet en transportant cette orientation locale, on revient au point de départ avec le sens opposé. Le ruban de Möbius n'est donc pas orientable.*
319
8##### Définition formelle
9
10Soit $M$ une [[variete_differentiable|variété différentiable]] de dimension $n$.
11
12On dit que $M$ est orientable s'il existe un atlas orienté $(U_i,\varphi_i)_{i\in I}$, c'est-à-dire que, pour tous $i,j\in I$, les [[carte-locale|changements de cartes]] vérifient :
324On dit que $M$ est orientable s'il existe un atlas orienté $(U_i,\varphi_i)_{i\in I}$, c'est-à-dire que, pour tous $i,j\in I$, les [[Fonction de transition|fonctions de transition]] sont de déterminant strictement positif :
13$$
14\det D(\varphi_j\circ\varphi_i^{-1})>0
15$$
16en tout point de $\varphi_i(U_i\cap U_j)$.

Revision 7815

10/26/2026, 11:15:27 AM · Ancient Tree

Concept created

##### Intuition

Une [[variete|variété]] est orientable si l'on peut choisir un « sens positif » de manière cohérente sur toute la variété, sans que ce sens s'inverse lorsqu'on se déplace.
Sur une surface, on peut imaginer choisir un sens de rotation : celui des aiguilles d'une montre ou son inverse.
![non orientable](https://s3.pub2.infomaniak.cloud/object/v1/AUTH_7cc517879b0040959f7d12abb1f0e72d/mathwoods-images/uploads/2026/10/user-1/1793013090182-f4210aa95c55b2b2-non-orientable.png)
*Sur un [[ruban de Möbius|ruban de Möbius]], lorsqu'on effectue un tour complet en transportant cette orientation locale, on revient au point de départ avec le sens opposé. Le ruban de Möbius n'est donc pas orientable.*

##### Définition formelle

Soit $M$ une [[variete_differentiable|variété différentiable]] de dimension $n$.

On dit que $M$ est orientable s'il existe un atlas orienté $(U_i,\varphi_i)_{i\in I}$, c'est-à-dire que, pour tous $i,j\in I$, les [[carte-locale|changements de cartes]] vérifient :
$$
\det D(\varphi_j\circ\varphi_i^{-1})>0
$$
en tout point de $\varphi_i(U_i\cap U_j)$.