1size 960 600
2background #070912
3timeline 30 300
4
5// Deux doubles pendules lâchés presque au même endroit (un millième de radian d'écart) : la physique est
6// intégrée à chaque pas (`every frame`, DT = 1/60 s, quatre sous-pas), et la traînée de chaque bout est
7// un `polyline` dont le point le plus récent est en tête des tableaux. Touche l'écran : ils repartent de là.
8def N = 220
9def ox = 480
10def oy = 300
11def L = 128
12def g = 9.81
13
14var t1a = 2.4
15var t2a = 2.9
16var w1a = 0
17var w2a = 0
18var t1b = 2.401
19var t2b = 2.9
20var w1b = 0
21var w2b = 0
22var axs = fill($(N), $(ox))
23var ays = fill($(N), $(oy))
24var bxs = fill($(N), $(ox))
25var bys = fill($(N), $(oy))
26
27fn alpha1(t1, t2, w1, w2) = (-$(g) * 3 * sin(t1) - $(g) * sin(t1 - 2 * t2) - 2 * sin(t1 - t2) * (w2 * w2 + w1 * w1 * cos(t1 - t2))) / (3 - cos(2 * t1 - 2 * t2))
28fn alpha2(t1, t2, w1, w2) = (2 * sin(t1 - t2) * (w1 * w1 * 2 + $(g) * 2 * cos(t1) + w2 * w2 * cos(t1 - t2))) / (3 - cos(2 * t1 - 2 * t2))
29
30scene {
31 layer "Fond" {
32 rect 0 0 960 600 fill radial(0.5, 0.38, 0.75, 0:#141a33, 1:#05060c)
33 repeat i from 0 to 40 { circle $(mod(i * 263, 960)) $(mod(i * 151, 600)) $(0.6 + mod(i, 3) * 0.5) fill #ffffff opacity $(0.08 + (i % 2) * 0.07) }
34 }
35 layer "Traines" {
36 polyline axs ays nofill stroke #ff8a3d22 3 cap round join round nohit
37 polyline axs ays count "70" nofill stroke #ffb070 3 cap round join round nohit
38 polyline bxs bys nofill stroke #3dd6ff22 3 cap round join round nohit
39 polyline bxs bys count "70" nofill stroke #8ae6ff 3 cap round join round nohit
40 }
41 layer "Pendules" {
42 group "BrasA1" at $(ox),$(oy) pivot 0,0 nohit { layer "c" { rect -2.5 0 5 $(L) 2.5 fill #ff8a3dcc } }
43 group "BrasA2" at $(ox),$(oy) pivot 0,0 nohit { layer "c" { rect -2.5 0 5 $(L) 2.5 fill #ff8a3dcc } }
44 group "BrasB1" at $(ox),$(oy) pivot 0,0 nohit { layer "c" { rect -2.5 0 5 $(L) 2.5 fill #3dd6ffcc } }
45 group "BrasB2" at $(ox),$(oy) pivot 0,0 nohit { layer "c" { rect -2.5 0 5 $(L) 2.5 fill #3dd6ffcc } }
46 group "MasseA1" at $(ox),$(oy) nohit { layer "c" { circle 0 0 11 fill radial(0.4, 0.35, 0.7, 0:#ffe2c8, 1:#ff7a2a) } }
47 group "MasseA2" at $(ox),$(oy) nohit { layer "c" { circle 0 0 26 fill radial(0.5, 0.5, 0.5, 0:#ff8a3d66, 1:#ff8a3d00) circle 0 0 13 fill radial(0.4, 0.35, 0.7, 0:#fff1e2, 1:#ff7a2a) } }
48 group "MasseB1" at $(ox),$(oy) nohit { layer "c" { circle 0 0 11 fill radial(0.4, 0.35, 0.7, 0:#d8f6ff, 1:#22b8f0) } }
49 group "MasseB2" at $(ox),$(oy) nohit { layer "c" { circle 0 0 26 fill radial(0.5, 0.5, 0.5, 0:#3dd6ff66, 1:#3dd6ff00) circle 0 0 13 fill radial(0.4, 0.35, 0.7, 0:#effbff, 1:#22b8f0) } }
50 circle $(ox) $(oy) 8 fill #e8ecff
51 circle $(ox) $(oy) 3 fill #070912
52 }
53 layer "Textes" {
54 text "LE DOUBLE PENDULE" at 32,28 font "Geist, system-ui, sans-serif" size 26 bold align left line 1.2 color #e8ecff box 400 34
55 text "deux départs à un millième de radian près" at 32,62 font "Geist, system-ui, sans-serif" size 15 align left line 1.2 color #7c86b8 box 420 22
56 text "écart : {}°" as "ecart" at 708,28 font "Geist Mono, ui-monospace, monospace" size 18 align right line 1.2 color #c9cff0 box 220 26 bind "deg(abs(mod(t2a - t2b + PI, TAU) - PI))" decimals 1
57 text "touche l'écran pour les relâcher depuis là" at 0,556 font "Geist, system-ui, sans-serif" size 15 italic align center line 1.2 color #5a6390 box 960 22
58 }
59 layer "Toucher" {
60 group "Toile" at 480,300 hitbox 960 600 { layer "c" { } }
61 }
62}
63
64object "BrasA1" { rotation = 0 - t1a }
65object "MasseA1" {
66 x = $(ox) + $(L) * sin(t1a)
67 y = $(oy) + $(L) * cos(t1a)
68}
69object "BrasA2" {
70 x = $(ox) + $(L) * sin(t1a)
71 y = $(oy) + $(L) * cos(t1a)
72 rotation = 0 - t2a
73}
74object "MasseA2" {
75 x = axs[0]
76 y = ays[0]
77}
78object "BrasB1" { rotation = 0 - t1b }
79object "MasseB1" {
80 x = $(ox) + $(L) * sin(t1b)
81 y = $(oy) + $(L) * cos(t1b)
82}
83object "BrasB2" {
84 x = $(ox) + $(L) * sin(t1b)
85 y = $(oy) + $(L) * cos(t1b)
86 rotation = 0 - t2b
87}
88object "MasseB2" {
89 x = bxs[0]
90 y = bys[0]
91}
92
93// Les traînées partent du bout des pendules ; trois secondes de mouvement sont jouées dès le chargement,
94// si bien que la scène (et sa vignette) s'ouvre sur un pendule déjà lancé, encore indiscernable de son jumeau.
95when loaded {
96 axs = fill($(N), $(ox) + $(L) * (sin(t1a) + sin(t2a)))
97 ays = fill($(N), $(oy) + $(L) * (cos(t1a) + cos(t2a)))
98 bxs = fill($(N), $(ox) + $(L) * (sin(t1b) + sin(t2b)))
99 bys = fill($(N), $(oy) + $(L) * (cos(t1b) + cos(t2b)))
100 repeat 150 times {
101 pas()
102 }
103}
104
105// Toucher : les deux pendules repartent, bras alignés vers le doigt, l'un décalé d'un millième de radian.
106object "Toile" {
107 when pressed {
108 t1a = atan2(mouse.x - $(ox), mouse.y - $(oy))
109 t2a = t1a + 0.5
110 w1a = 0
111 w2a = 0
112 t1b = t1a + 0.001
113 t2b = t2a
114 w1b = 0
115 w2b = 0
116 axs = fill($(N), $(ox) + $(L) * (sin(t1a) + sin(t2a)))
117 ays = fill($(N), $(oy) + $(L) * (cos(t1a) + cos(t2a)))
118 bxs = fill($(N), $(ox) + $(L) * (sin(t1b) + sin(t2b)))
119 bys = fill($(N), $(oy) + $(L) * (cos(t1b) + cos(t2b)))
120 }
121}
122
123fn pas() {
124 repeat 4 times {
125 w1a = w1a + alpha1(t1a, t2a, w1a, w2a) * DT / 4
126 w2a = w2a + alpha2(t1a, t2a, w1a, w2a) * DT / 4
127 t1a = t1a + w1a * DT / 4
128 t2a = t2a + w2a * DT / 4
129 w1b = w1b + alpha1(t1b, t2b, w1b, w2b) * DT / 4
130 w2b = w2b + alpha2(t1b, t2b, w1b, w2b) * DT / 4
131 t1b = t1b + w1b * DT / 4
132 t2b = t2b + w2b * DT / 4
133 }
134 repeat i from 0 to $(N - 2) {
135 axs[$(N - 1) - i] = axs[$(N - 2) - i]
136 ays[$(N - 1) - i] = ays[$(N - 2) - i]
137 bxs[$(N - 1) - i] = bxs[$(N - 2) - i]
138 bys[$(N - 1) - i] = bys[$(N - 2) - i]
139 }
140 axs[0] = $(ox) + $(L) * (sin(t1a) + sin(t2a))
141 ays[0] = $(oy) + $(L) * (cos(t1a) + cos(t2a))
142 bxs[0] = $(ox) + $(L) * (sin(t1b) + sin(t2b))
143 bys[0] = $(oy) + $(L) * (cos(t1b) + cos(t2b))
144}
145
146every frame {
147 pas()
148}