1414 display : flex; flex-direction : column; align-items : center; justify-content : center;
1515 padding : 1rem ; gap : 1rem ;
1616 }
17- h1 {
18- font-size : clamp (1rem , 2.6vw , 1.4rem ); letter-spacing : .1em ; margin : 0 ; text-align : center; color : # ddd ;
19- }
17+ h1 { font-size : clamp (1rem , 2.6vw , 1.4rem ); letter-spacing : .1em ; margin : 0 ; text-align : center; color : # ddd }
2018 a { color : # ddd ; text-decoration : none } a : hover { color : var (--ink ); text-decoration : underline }
2119 canvas { width : min (84vmin , 980px ); height : auto; aspect-ratio : 4 / 1.6 ; display : block }
2220 footer { color : # 777 ; font-size : clamp (.7rem , 1.5vw , .85rem ) }
2927 < a href ="https://ukb-pyro.github.io/inf/ " target ="_blank " rel ="noopener "> Sonata</ a > →
3028 </ h1 >
3129
32- < canvas id ="stage " aria-label ="Adagio morph : line → circle (closing ends) → ∞ → broken loop (two ends) → line "> </ canvas >
30+ < canvas id ="stage " aria-label ="Adagio sostenuto : line → circle → ∞ → broken ∞ → line "> </ canvas >
3331
3432 < footer > © 2025 Ukubona LLC</ footer >
3533
3836 const canvas = document . getElementById ( 'stage' ) ;
3937 const ctx = canvas . getContext ( '2d' ) ;
4038
41- // --- sizing (crisp on HiDPI) ---
39+ // HiDPI sizing
4240 function fit ( ) {
4341 const cssW = canvas . getBoundingClientRect ( ) . width ;
4442 const cssH = cssW / ( 4 / 1.6 ) ;
@@ -48,149 +46,139 @@ <h1>
4846 ctx . setTransform ( dpr , 0 , 0 , dpr , 0 , 0 ) ;
4947 }
5048
51- // --- parameters ---
52- const N = 1200 ; // dense sampling for imperceptible motion
53- const LEG_MS = 16000 ; // adagio: 16s per leg
49+ // --- tempo & resolution ---
50+ const N = 1400 ; // dense sampling for imperceptible morphs
51+ const LEG_MS = 45000 ; // adagio sostenuto: 45s per leg
5452 const TWO_PI = Math . PI * 2 ;
5553
56- // very gentle cosine ease
54+ // ultra- gentle easing (cosine-in-out)
5755 const ease = t => 0.5 - 0.5 * Math . cos ( Math . PI * t ) ;
5856
59- // sample helpers
57+ // sample helper
6058 function sample ( fn ) {
6159 const pts = new Array ( N ) ;
62- for ( let i = 0 ; i < N ; i ++ ) {
63- pts [ i ] = fn ( i / N ) ;
64- }
60+ for ( let i = 0 ; i < N ; i ++ ) pts [ i ] = fn ( i / N ) ;
6561 return pts ;
6662 }
6763
68- // canonical shapes, centered at origin
69- const line = sample ( t => [ ( t * 2 - 1 ) , 0 ] ) ;
70- const circle = sample ( t => [ Math . cos ( TWO_PI * t ) , Math . sin ( TWO_PI * t ) ] ) ;
71- // Lemniscate of Gerono (∞), scaled vertically for pleasing proportion
72- const lemni = sample ( t => {
73- const th = TWO_PI * t ;
74- const x = Math . cos ( th ) ;
75- const y = 0.60 * Math . sin ( th ) * Math . cos ( th ) ; // 0.5*sin(2θ) scaled
76- return [ x , y ] ;
64+ // canonical shapes (centered at origin, radius≈1)
65+ const line = sample ( u => [ ( u * 2 - 1 ) , 0 ] ) ;
66+ const circle = sample ( u => [ Math . cos ( TWO_PI * u ) , Math . sin ( TWO_PI * u ) ] ) ;
67+ const lemni = sample ( u => { // lemniscate of Gerono, softened vertically
68+ const th = TWO_PI * u ;
69+ return [ Math . cos ( th ) , 0.60 * Math . sin ( th ) * Math . cos ( th ) ] ;
7770 } ) ;
7871
79- // index of the lemniscate's crossing (nearest to 0,0), used as "break" point
72+ // index near lemniscate crossing (for the "break")
8073 const lemniCrossIdx = ( ( ) => {
81- let best = 0 , bestR = 1e9 ;
74+ let k = 0 , best = 1e9 ;
8275 for ( let i = 0 ; i < N ; i ++ ) {
8376 const [ x , y ] = lemni [ i ] ;
84- const r = x * x + y * y ;
85- if ( r < bestR ) { bestR = r ; best = i ; }
77+ const r2 = x * x + y * y ;
78+ if ( r2 < best ) { best = r2 ; k = i ; }
8679 }
87- return best ;
80+ return k ;
8881 } ) ( ) ;
8982
90- // blend two shapes pointwise
91- function blend ( A , B , m ) {
83+ // pointwise blend
84+ function blend ( A , B , m ) {
9285 const out = new Array ( N ) ;
9386 for ( let i = 0 ; i < N ; i ++ ) {
94- const ax = A [ i ] , bx = B [ i ] ;
95- out [ i ] = [ ax [ 0 ] + ( bx [ 0 ] - ax [ 0 ] ) * m , ax [ 1 ] + ( bx [ 1 ] - ax [ 1 ] ) * m ] ;
87+ const a = A [ i ] , b = B [ i ] ;
88+ out [ i ] = [ a [ 0 ] + ( b [ 0 ] - a [ 0 ] ) * m , a [ 1 ] + ( b [ 1 ] - a [ 1 ] ) * m ] ;
9689 }
9790 return out ;
9891 }
9992
100- // subtle breathing to avoid mechanical feel
93+ // subtle breathing on global radius so it never feels mechanical
10194 function radius ( now , base ) {
102- return base * ( 1 + 0.02 * Math . sin ( now / 4200 ) + 0.01 * Math . cos ( now / 7600 ) ) ;
95+ return base * ( 1 + 0.02 * Math . sin ( now / 5200 ) + 0.01 * Math . cos ( now / 8800 ) ) ;
10396 }
10497
105- // draw with optional "gap" on closed curves; gap starts at given index
106- function drawPolyline ( pts , opts ) {
107- const { close= false , gapFrac= 0 , gapIndex= 0 , w, h} = opts ;
98+ // draw a (possibly open) curve; when gapFrac>0 omit an arc (two ends)
99+ function strokeCurve ( pts , { closed= false , gapFrac= 0 , gapIndex= 0 , w} ) {
108100 ctx . beginPath ( ) ;
109101
110102 if ( gapFrac > 0 ) {
111- const n = pts . length ;
112- const gapN = Math . max ( 2 , Math . floor ( n * gapFrac ) ) ;
113- const start = ( ( gapIndex % n ) + n ) % n ;
114- const count = n - gapN ;
115- for ( let k = 0 ; k < count ; k ++ ) {
116- const i = ( start + k ) % n ;
103+ const n = pts . length ;
104+ const gapN = Math . max ( 2 , Math . floor ( n * gapFrac ) ) ;
105+ const s = ( ( gapIndex % n ) + n ) % n ;
106+ const cnt = n - gapN ;
107+
108+ // draw cnt points as a single open strand
109+ for ( let k = 0 ; k < cnt ; k ++ ) {
110+ const i = ( s + k ) % n ;
117111 const p = pts [ i ] ;
118112 if ( k === 0 ) ctx . moveTo ( p [ 0 ] , p [ 1 ] ) ; else ctx . lineTo ( p [ 0 ] , p [ 1 ] ) ;
119113 }
120- // open ends (no closePath)
121- } else {
122- // full stroke
114+ } else {
115+ // full curve
123116 ctx . moveTo ( pts [ 0 ] [ 0 ] , pts [ 0 ] [ 1 ] ) ;
124117 for ( let i = 1 ; i < pts . length ; i ++ ) ctx . lineTo ( pts [ i ] [ 0 ] , pts [ i ] [ 1 ] ) ;
125- if ( close ) ctx . closePath ( ) ;
118+ if ( closed ) ctx . closePath ( ) ;
126119 }
127120
128- ctx . lineWidth = Math . max ( 2 , Math . min ( 6 , w * 0.006 ) ) ;
129- ctx . lineJoin = "round" ;
130- ctx . lineCap = "round" ;
121+ ctx . lineWidth = Math . max ( 2 , Math . min ( 6 , w * 0.006 ) ) ;
122+ ctx . lineJoin = "round" ;
123+ ctx . lineCap = "round" ;
131124 ctx . strokeStyle = "#9ad1ff" ;
132- ctx . shadowColor = "rgba(154,209,255,.22 )" ;
133- ctx . shadowBlur = 8 ;
125+ ctx . shadowColor = "rgba(154,209,255,.18 )" ;
126+ ctx . shadowBlur = 6 ;
134127 ctx . stroke ( ) ;
135128 }
136129
137130 function frame ( now ) {
138131 fit ( ) ;
139- const w = canvas . width / ( window . devicePixelRatio || 1 ) ;
140- const h = canvas . height / ( window . devicePixelRatio || 1 ) ;
132+
133+ const w = canvas . width / ( window . devicePixelRatio || 1 ) ;
134+ const h = canvas . height / ( window . devicePixelRatio || 1 ) ;
141135 const cx = w / 2 , cy = h / 2 ;
142136 const base = Math . min ( w , h ) * 0.38 ;
143- const r = radius ( now , base ) ;
137+ const r = radius ( now , base ) ;
144138
145- // 4-leg cycle:
146- // 0) line -> circle (ends stay open; closes only at tail)
147- // 1) circle -> lemni (closed)
148- // 2) lemni -> circle (break open during this leg at the crossing)
149- // 3) circle (open) -> line (open)
150- const CYCLE = LEG_MS * 4 ;
151- const t = ( now % CYCLE ) ;
152- const leg = Math . floor ( t / LEG_MS ) ;
153- const local = ( t % LEG_MS ) / LEG_MS ;
154- const m = ease ( local ) ;
139+ // four-leg cycle
140+ const CYCLE = LEG_MS * 4 ;
141+ const t = ( now % CYCLE ) ;
142+ const leg = Math . floor ( t / LEG_MS ) ; // 0..3
143+ const local = ( t % LEG_MS ) / LEG_MS ; // 0..1
144+ const m = ease ( local ) ;
155145
156- let A , B , closeNow = false , gapFrac = 0 , gapIndex = 0 ;
146+ let A , B , closed = false , gapFrac = 0 , gapIndex = 0 ;
157147
158148 if ( leg === 0 ) {
159- // line -> circle, keep ends visible until the very end
149+ // line -> circle (two teloi remain visible; seal only at transition edge)
160150 A = line ; B = circle ;
161- closeNow = ( m > 0.995 ) ; // seal at the last breath
162- gapFrac = 0 ; // no artificial gap; openness comes from not closing
163- gapIndex = 0 ;
164- } else if ( leg === 1 ) {
165- // circle -> infinity, closed throughout
151+ closed = false ; // keep open; endpoints "kiss" at the very end
152+ gapFrac = 0 ; // no artificial gap
153+ } else if ( leg === 1 ) {
154+ // circle -> lemni (fully closed; absolutely no gap)
166155 A = circle ; B = lemni ;
167- closeNow = true ;
156+ closed = true ;
168157 gapFrac = 0 ;
169- gapIndex = 0 ;
170- } else if ( leg === 2 ) {
171- // infinity -> circle while BREAKING the loop (two ends) at the crossing
172- A = lemni ; B = circle ;
173- closeNow = false ;
174- // grow a visible break (up to ~22% of the loop)
175- gapFrac = 0.22 * Math . pow ( m , 0.85 ) ;
176- gapIndex = lemniCrossIdx ; // break begins at the ∞ crossing
177- } else {
178- // open circle -> line (stay open)
179- A = circle ; B = line ;
180- closeNow = false ;
181- gapFrac = 0.22 ; // keep two ends while it straightens
182- gapIndex = 0 ;
158+ } else if ( leg === 2 ) {
159+ // lemni -> broken lemni (grow a gap at the crossing)
160+ A = lemni ; B = lemni ; // shape fixed; only gap changes
161+ closed = false ;
162+ gapFrac = 0.22 * m ; // up to ~22% arc removed
163+ gapIndex = lemniCrossIdx ;
164+ } else {
165+ // broken lemni -> line (stay open; shrink gap smoothly while straightening)
166+ A = lemni ; B = line ;
167+ closed = false ;
168+ // carry the break at start, then fade it out as we approach the line
169+ gapFrac = 0.22 * ( 1 - m ) ;
170+ gapIndex = lemniCrossIdx ;
183171 }
184172
173+ // blend & place
185174 const pts = blend ( A , B , m ) ;
186-
187- // transform to canvas space
188- for ( let i = 0 ; i < pts . length ; i ++ ) {
189- pts [ i ] = [ cx + pts [ i ] [ 0 ] * r , cy + pts [ i ] [ 1 ] * r ] ;
175+ for ( let i = 0 ; i < N ; i ++ ) {
176+ pts [ i ] [ 0 ] = cx + pts [ i ] [ 0 ] * r ;
177+ pts [ i ] [ 1 ] = cy + pts [ i ] [ 1 ] * r ;
190178 }
191179
192180 ctx . clearRect ( 0 , 0 , w , h ) ;
193- drawPolyline ( pts , { close : closeNow , gapFrac, gapIndex, w, h } ) ;
181+ strokeCurve ( pts , { closed , gapFrac, gapIndex, w} ) ;
194182
195183 requestAnimationFrame ( frame ) ;
196184 }
0 commit comments