1size 960 600
2background #05080a
3timeline 30 300
4
5// Courbes de Lissajous : x = sin(a·t + φ), y = sin(b·t). La courbe est un `polyline` dont les points
6// sont deux tableaux recalculés à chaque pas ; deux curseurs règlent a et b, la phase φ tourne seule.
7def N = 400
8def cx = 330
9def cy = 300
10def R = 210
11def tx0 = 670
12def tw = 220
13
14var px = fill($(N), 0)
15var py = fill($(N), 0)
16var a = 3
17var b = 2
18var ka = $(tx0 + tw * 2 / 5)
19var kb = $(tx0 + tw * 1 / 5)
20var vit = 0.35
21var kv = $(tx0 + tw * 0.35)
22var phi = 0
23
24scene {
25 layer "Ecran" {
26 rect 60 40 540 520 28 fill linear(90, 0:#0b1a14, 1:#06100c) stroke #1d3a2c 2
27 repeat i from 1 to 9 { path "M$(60 + i * 54) 52 L$(60 + i * 54) 548" nofill stroke #12261c 1 }
28 repeat i from 1 to 9 { path "M72 $(40 + i * 52) L588 $(40 + i * 52)" nofill stroke #12261c 1 }
29 path "M$(cx) 52 L$(cx) 548" nofill stroke #1d3a2c 1.5
30 path "M72 $(cy) L588 $(cy)" nofill stroke #1d3a2c 1.5
31 }
32 layer "Trace" {
33 polyline px py closed nofill stroke #2bff8a30 14 cap round join round nohit
34 polyline px py closed nofill stroke #2bff8a70 6 cap round join round nohit
35 polyline px py closed nofill stroke #d9ffe9 2 cap round join round nohit
36 group "Spot" at $(cx),$(cy) nohit {
37 layer "c" { circle 0 0 26 fill radial(0.5, 0.5, 0.5, 0:#7dffb8aa, 1:#2bff8a00) circle 0 0 6 fill #ffffff }
38 }
39 }
40 layer "Vitre" {
41 rect 60 40 540 520 28 fill radial(0.5, 0.45, 0.75, 0:#ffffff00, 0.8:#ffffff00, 1:#00000066) nohit
42 path "M96 64 C220 50 440 50 564 66" nofill stroke #ffffff0a 6 cap round nohit
43 }
44 layer "Panneau" {
45 text "LISSAJOUS" at $(tx0),60 font "Geist, system-ui, sans-serif" size 34 bold align left line 1.2 color #d9ffe9 box 240 44
46 text "x = sin(a·t + φ) y = sin(b·t)" at $(tx0),104 font "Geist Mono, ui-monospace, monospace" size 13 align left line 1.2 color #5f8f76 box 270 20
47
48 text "fréquence a" at $(tx0),170 font "Geist, system-ui, sans-serif" size 16 align left line 1.2 color #8fb8a2 box 160 22
49 text "{}" as "valA" at $(tx0 + tw - 40),164 font "Geist Mono, ui-monospace, monospace" size 26 bold align right line 1.2 color #2bff8a box 40 30 bind "a"
50 group "PisteA" at $(tx0),215 { layer "c" { rect 0 -3 $(tw) 6 3 fill #183327 } }
51 repeat i from 0 to 5 { circle $(tx0 + tw * i / 5) 215 3 fill #2b5a43 }
52 group "CurseurA" at $(tx0),215 pivot 0,0 hitbox 44 44 {
53 layer "c" { circle 0 0 15 fill #0b1a14 stroke #2bff8a 3 circle 0 0 5 fill #2bff8a }
54 }
55
56 text "fréquence b" at $(tx0),270 font "Geist, system-ui, sans-serif" size 16 align left line 1.2 color #8fb8a2 box 160 22
57 text "{}" as "valB" at $(tx0 + tw - 40),264 font "Geist Mono, ui-monospace, monospace" size 26 bold align right line 1.2 color #2bff8a box 40 30 bind "b"
58 group "PisteB" at $(tx0),315 { layer "c" { rect 0 -3 $(tw) 6 3 fill #183327 } }
59 repeat i from 0 to 5 { circle $(tx0 + tw * i / 5) 315 3 fill #2b5a43 }
60 group "CurseurB" at $(tx0),315 pivot 0,0 hitbox 44 44 {
61 layer "c" { circle 0 0 15 fill #0b1a14 stroke #2bff8a 3 circle 0 0 5 fill #2bff8a }
62 }
63
64 text "rotation de φ" at $(tx0),370 font "Geist, system-ui, sans-serif" size 16 align left line 1.2 color #8fb8a2 box 160 22
65 group "PisteV" at $(tx0),415 { layer "c" { rect 0 -3 $(tw) 6 3 fill #183327 } }
66 group "CurseurV" at $(tx0),415 pivot 0,0 hitbox 44 44 {
67 layer "c" { circle 0 0 15 fill #0b1a14 stroke #9affd0 3 circle 0 0 5 fill #9affd0 }
68 }
69
70 text "rapport {}" as "rapport" at $(tx0),470 font "Geist Mono, ui-monospace, monospace" size 18 align left line 1.2 color #5f8f76 box 240 26 bind "a / b" decimals 3
71 text "glisse les curseurs" at $(tx0),520 font "Geist, system-ui, sans-serif" size 15 italic align left line 1.2 color #3d6652 box 240 22
72 }
73}
74
75// Chaque curseur écrit sa position ; la valeur entière en est déduite, et le bouton se cale dessus.
76object "CurseurA" {
77 dragX ka {
78 confine to PisteA
79 }
80 smooth x = $(tx0) + (a - 1) * $(tw / 5) { k 0.35 }
81 scaleX = self.grabbed ? 1.25 : (self.hovered ? 1.12 : 1)
82 scaleY = self.grabbed ? 1.25 : (self.hovered ? 1.12 : 1)
83}
84object "CurseurB" {
85 dragX kb {
86 confine to PisteB
87 }
88 smooth x = $(tx0) + (b - 1) * $(tw / 5) { k 0.35 }
89 scaleX = self.grabbed ? 1.25 : (self.hovered ? 1.12 : 1)
90 scaleY = self.grabbed ? 1.25 : (self.hovered ? 1.12 : 1)
91}
92object "CurseurV" {
93 dragX kv {
94 confine to PisteV
95 }
96 x = kv
97 scaleX = self.grabbed ? 1.25 : (self.hovered ? 1.12 : 1)
98 scaleY = self.grabbed ? 1.25 : (self.hovered ? 1.12 : 1)
99}
100
101// Le spot parcourt la courbe : un tour toutes les quatre secondes.
102object "Spot" {
103 x = px[floor(mod(clock * 100, $(N)))]
104 y = py[floor(mod(clock * 100, $(N)))]
105}
106
107fn calcule() {
108 repeat i from 0 to $(N - 1) {
109 px[i] = $(cx) + $(R) * sin(a * i * TAU / $(N) + phi)
110 py[i] = $(cy) + $(R) * sin(b * i * TAU / $(N))
111 }
112}
113
114// La courbe existe dès le chargement (vignette, premier affichage), puis suit les curseurs à chaque pas.
115when loaded {
116 phi = 0.6
117 calcule()
118}
119
120every frame {
121 a = 1 + round(clamp((ka - $(tx0)) / $(tw), 0, 1) * 5)
122 b = 1 + round(clamp((kb - $(tx0)) / $(tw), 0, 1) * 5)
123 vit = clamp((kv - $(tx0)) / $(tw), 0, 1)
124 phi = phi + vit * 2 * DT
125 calcule()
126}