@@ -113,6 +113,17 @@ const LogoBlock = struct {
113113 scale : f32 , // Current scale
114114 delay : f32 , // Animation start delay
115115 center : rl.Vector2 , // Center of the block for positioning
116+ // Assembly animation velocity (spring physics)
117+ anim_vx : f32 ,
118+ anim_vy : f32 ,
119+ anim_vr : f32 ,
120+ // Cursor physics
121+ push_x : f32 , // Current push displacement from cursor
122+ push_y : f32 ,
123+ push_rot : f32 , // Rotation from cursor push
124+ vel_x : f32 , // Velocity for spring-back
125+ vel_y : f32 ,
126+ vel_rot : f32 ,
116127};
117128
118129const LogoAnimation = struct {
@@ -122,6 +133,7 @@ const LogoAnimation = struct {
122133 is_complete : bool ,
123134 logo_scale : f32 , // Scale the logo to fit screen
124135 logo_offset : rl.Vector2 , // Center the logo on screen
136+ hovered_block : i32 , // Index of block under cursor (-1 = none)
125137
126138 // SVG viewBox: 596 x 526, center at ~298, 263
127139 const SVG_WIDTH : f32 = 596.0 ;
@@ -133,10 +145,11 @@ const LogoAnimation = struct {
133145 var self = LogoAnimation {
134146 .blocks = undefined ,
135147 .time = 0 ,
136- .duration = 2 .0 ,
148+ .duration = 5 .0 , // Luxury slow animation (Apple-style)
137149 .is_complete = false ,
138- .logo_scale = @min (screen_w / SVG_WIDTH , screen_h / SVG_HEIGHT ) * 0.6 ,
150+ .logo_scale = @min (screen_w / SVG_WIDTH , screen_h / SVG_HEIGHT ) * 0.35 ,
139151 .logo_offset = .{ .x = screen_w / 2 , .y = screen_h / 2 },
152+ .hovered_block = -1 ,
140153 };
141154
142155 // 27 blocks parsed from assets/999.svg
@@ -198,10 +211,6 @@ const LogoAnimation = struct {
198211 };
199212 const counts = [27 ]u8 { 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 };
200213
201- // Initialize blocks with random start positions
202- var prng = std .Random .DefaultPrng .init (12345 );
203- const random = prng .random ();
204-
205214 for (0.. 27) | i | {
206215 var center_x : f32 = 0 ;
207216 var center_y : f32 = 0 ;
@@ -220,16 +229,28 @@ const LogoAnimation = struct {
220229 self .blocks [i ].count = cnt ;
221230 self .blocks [i ].center = .{ .x = center_x , .y = center_y };
222231
223- // Random start offset (fly from outside screen)
224- const angle = random .float (f32 ) * TAU ;
225- const distance = 800.0 + random .float (f32 ) * 400.0 ;
232+ // Each block flies straight from its own direction — no chaos
233+ // Direction = from center through block's position, extended far out
234+ const dir_len = @sqrt (center_x * center_x + center_y * center_y );
235+ const norm_x = if (dir_len > 0.1 ) center_x / dir_len else @cos (@as (f32 , @floatFromInt (i )) * TAU / 27.0 );
236+ const norm_y = if (dir_len > 0.1 ) center_y / dir_len else @sin (@as (f32 , @floatFromInt (i )) * TAU / 27.0 );
237+ const distance : f32 = 1200.0 ; // Same distance for all — clean formation
226238 self .blocks [i ].offset = .{
227- .x = @cos ( angle ) * distance ,
228- .y = @sin ( angle ) * distance ,
239+ .x = norm_x * distance ,
240+ .y = norm_y * distance ,
229241 };
230- self .blocks [i ].rotation = (random .float (f32 ) - 0.5 ) * 6.0 ; // -3 to +3 radians
231- self .blocks [i ].scale = 0.3 + random .float (f32 ) * 0.3 ;
232- self .blocks [i ].delay = @as (f32 , @floatFromInt (i )) * 0.02 ; // Staggered
242+ self .blocks [i ].rotation = 0 ; // No rotation — flat, clean
243+ self .blocks [i ].scale = 1.0 ; // Full size from start
244+ self .blocks [i ].delay = 0 ; // All start simultaneously
245+ self .blocks [i ].anim_vx = 0 ;
246+ self .blocks [i ].anim_vy = 0 ;
247+ self .blocks [i ].anim_vr = 0 ;
248+ self .blocks [i ].push_x = 0 ;
249+ self .blocks [i ].push_y = 0 ;
250+ self .blocks [i ].push_rot = 0 ;
251+ self .blocks [i ].vel_x = 0 ;
252+ self .blocks [i ].vel_y = 0 ;
253+ self .blocks [i ].vel_rot = 0 ;
233254 }
234255
235256 return self ;
@@ -245,84 +266,156 @@ const LogoAnimation = struct {
245266 const t = @max (0 , self .time - block .delay );
246267 const progress = @min (1.0 , t / self .duration );
247268
248- // Phi-based easing (ease out)
249- const eased = 1.0 - (1.0 - progress ) * (1.0 - progress ) * PHI_INV ;
250-
251- // Animate offset towards zero
252- block .offset .x = block .offset .x * (1.0 - eased * 0.1 );
253- block .offset .y = block .offset .y * (1.0 - eased * 0.1 );
254-
255- // Animate rotation towards zero
256- block .rotation = block .rotation * (1.0 - eased * 0.05 );
257-
258- // Animate scale towards 1.0
259- block .scale = block .scale + (1.0 - block .scale ) * eased * 0.05 ;
269+ // Two phases:
270+ // Phase 1 (0–0.7): straight linear flight toward center
271+ // Phase 2 (0.7–1.0): spring compression + bounce
272+ const arrival = 0.7 ; // when blocks "arrive" and spring kicks in
273+
274+ if (progress < arrival ) {
275+ // Straight linear flight — each block slides to its place
276+ const speed = 2.0 * dt ;
277+ block .offset .x -= block .offset .x * speed ;
278+ block .offset .y -= block .offset .y * speed ;
279+
280+ // Carry momentum into spring phase
281+ block .anim_vx = - block .offset .x * 0.3 ;
282+ block .anim_vy = - block .offset .y * 0.3 ;
283+ block .anim_vr = 0 ;
284+ } else {
285+ // Spring phase — elastic bounce at destination
286+ const spring_k : f32 = 18.0 ;
287+ const damp : f32 = 0.90 ;
288+
289+ // Spring force pulls offset to zero
290+ block .anim_vx += (- block .offset .x * spring_k ) * dt ;
291+ block .anim_vy += (- block .offset .y * spring_k ) * dt ;
292+ block .anim_vx *= damp ;
293+ block .anim_vy *= damp ;
294+ block .offset .x += block .anim_vx * dt * 60.0 ;
295+ block .offset .y += block .anim_vy * dt * 60.0 ;
296+
297+ // Spring on rotation
298+ block .anim_vr += (- block .rotation * spring_k ) * dt ;
299+ block .anim_vr *= damp ;
300+ block .rotation += block .anim_vr * dt * 60.0 ;
301+
302+ // Scale settles to 1.0
303+ block .scale += (1.0 - block .scale ) * 0.1 ;
304+ }
260305
261- // Check if block is "home"
306+ // Check if settled
262307 const dist = @sqrt (block .offset .x * block .offset .x + block .offset .y * block .offset .y );
263- if (dist > 2.0 or @abs (block .rotation ) > 0.02 or @abs (block .scale - 1.0 ) > 0.02 ) {
308+ const vel = @sqrt (block .anim_vx * block .anim_vx + block .anim_vy * block .anim_vy );
309+ if (dist > 0.3 or vel > 0.3 or @abs (block .rotation ) > 0.003 ) {
264310 all_done = false ;
265311 }
266312 }
267313
268- if (all_done and self .time > self .duration + 0.5 ) {
314+ // Linger for 1.5s after assembly (Apple-style pause before transition)
315+ if (all_done and self .time > self .duration + 1.5 ) {
269316 self .is_complete = true ;
270317 }
271318 }
272319
320+ /// Point-in-polygon test (ray casting)
321+ fn pointInPoly (verts : [5 ]rl.Vector2 , cnt : u8 , px : f32 , py : f32 ) bool {
322+ var inside = false ;
323+ var j : usize = cnt - 1 ;
324+ var i : usize = 0 ;
325+ while (i < cnt ) : (i += 1 ) {
326+ const yi = verts [i ].y ;
327+ const yj = verts [j ].y ;
328+ const xi = verts [i ].x ;
329+ const xj = verts [j ].x ;
330+ if (((yi > py ) != (yj > py )) and
331+ (px < (xj - xi ) * (py - yi ) / (yj - yi ) + xi ))
332+ {
333+ inside = ! inside ;
334+ }
335+ j = i ;
336+ }
337+ return inside ;
338+ }
339+
340+ /// Highlight block under cursor (no physics — just detect hover)
341+ pub fn applyMouse (self : * LogoAnimation , mouse_x : f32 , mouse_y : f32 , _ : f32 ) void {
342+ const scale = self .logo_scale ;
343+ const ox = self .logo_offset .x ;
344+ const oy = self .logo_offset .y ;
345+
346+ self .hovered_block = -1 ;
347+
348+ for (self .blocks , 0.. ) | block , i | {
349+ var verts : [5 ]rl.Vector2 = undefined ;
350+ const cnt = block .count ;
351+
352+ for (0.. cnt ) | j | {
353+ const bx = block .v [j ].x * block .scale + block .offset .x ;
354+ const by = block .v [j ].y * block .scale + block .offset .y ;
355+ verts [j ] = .{
356+ .x = ox + bx * scale ,
357+ .y = oy + by * scale ,
358+ };
359+ }
360+
361+ if (pointInPoly (verts , cnt , mouse_x , mouse_y )) {
362+ self .hovered_block = @intCast (i );
363+ }
364+ }
365+ }
366+
273367 pub fn draw (self : * const LogoAnimation ) void {
274368 const scale = self .logo_scale ;
275369 const ox = self .logo_offset .x ;
276370 const oy = self .logo_offset .y ;
277371
278- for (self .blocks ) | block | {
279- // Transform vertices
372+ // Base color #08FAB5, highlight color #08FAE6
373+ const base_color = rl.Color { .r = 0x08 , .g = 0xFA , .b = 0xB5 , .a = 255 };
374+ const highlight_color = rl.Color { .r = 0xFF , .g = 0x69 , .b = 0xB4 , .a = 255 };
375+
376+ // Black outline — clear separation between parts
377+ const outline_color = rl.Color { .r = 0 , .g = 0 , .b = 0 , .a = 255 };
378+
379+ for (self .blocks , 0.. ) | block , idx | {
380+ const fill_color = if (self .hovered_block >= 0 and idx == @as (usize , @intCast (self .hovered_block ))) highlight_color else base_color ;
280381 var verts : [5 ]rl.Vector2 = undefined ;
281382 const cnt = block .count ;
282383
283384 const cos_r = @cos (block .rotation );
284385 const sin_r = @sin (block .rotation );
285386
286387 for (0.. cnt ) | j | {
287- // Apply block animation: offset + rotation + scale
288- var x = block .v [j ].x * block .scale ;
289- var y = block .v [j ].y * block .scale ;
388+ var bx = block .v [j ].x * block .scale ;
389+ var by = block .v [j ].y * block .scale ;
290390
291- // Rotate around block center
292- const dx = x - block .center .x * block .scale ;
293- const dy = y - block .center .y * block .scale ;
294- x = block .center .x * block .scale + dx * cos_r - dy * sin_r ;
295- y = block .center .y * block .scale + dx * sin_r + dy * cos_r ;
391+ const ddx = bx - block .center .x * block .scale ;
392+ const ddy = by - block .center .y * block .scale ;
393+ bx = block .center .x * block .scale + ddx * cos_r - ddy * sin_r ;
394+ by = block .center .y * block .scale + ddx * sin_r + ddy * cos_r ;
296395
297- // Apply offset
298- x += block .offset .x ;
299- y += block .offset .y ;
396+ bx += block .offset .x ;
397+ by += block .offset .y ;
300398
301- // Scale and translate to screen
302399 verts [j ] = .{
303- .x = ox + x * scale ,
304- .y = oy + y * scale ,
400+ .x = ox + bx * scale ,
401+ .y = oy + by * scale ,
305402 };
306403 }
307404
308- // Draw filled polygon (triangle fan from first vertex)
405+ // Fill
309406 if (cnt >= 3 ) {
310407 var k : usize = 1 ;
311408 while (k < cnt - 1 ) : (k += 1 ) {
312- rl .DrawTriangle (
313- verts [0 ],
314- verts [k ],
315- verts [k + 1 ],
316- LOGO_GREEN ,
317- );
409+ rl .DrawTriangle (verts [0 ], verts [k ], verts [k + 1 ], fill_color );
410+ rl .DrawTriangle (verts [0 ], verts [k + 1 ], verts [k ], fill_color );
318411 }
319412 }
320413
321- // Draw outline
414+ // Transparent outline
322415 var m : usize = 0 ;
323416 while (m < cnt ) : (m += 1 ) {
324417 const next = (m + 1 ) % cnt ;
325- rl .DrawLineEx (verts [m ], verts [next ], 1.5 , withAlpha ( BG_BLACK , 180 ) );
418+ rl .DrawLineEx (verts [m ], verts [next ], 5.0 , outline_color );
326419 }
327420 }
328421 }
@@ -2958,32 +3051,21 @@ pub fn main() !void {
29583051 defer rl .EndDrawing ();
29593052
29603053 // Pure black background - landing page style (alpha = 180 for transparency)
2961- rl .ClearBackground (rl.Color { .r = 0x00 , .g = 0x00 , .b = 0x00 , .a = 180 });
3054+ rl .ClearBackground (rl.Color { .r = 0x00 , .g = 0x00 , .b = 0x00 , .a = 0xF5 });
29623055
2963- // === LOGO LOADING ANIMATION ===
3056+ // === LOGO LOADING ANIMATION (Apple-style luxury welcome) ===
29643057 if (! loading_complete ) {
29653058 // Update logo animation
2966- logo_anim .logo_scale = @min (@as (f32 , @floatFromInt (g_width )) / LogoAnimation .SVG_WIDTH , @as (f32 , @floatFromInt (g_height )) / LogoAnimation .SVG_HEIGHT ) * 0.6 ;
3059+ logo_anim .logo_scale = @min (@as (f32 , @floatFromInt (g_width )) / LogoAnimation .SVG_WIDTH , @as (f32 , @floatFromInt (g_height )) / LogoAnimation .SVG_HEIGHT ) * 0.35 ;
29673060 logo_anim .logo_offset = .{ .x = @as (f32 , @floatFromInt (g_width )) / 2 , .y = @as (f32 , @floatFromInt (g_height )) / 2 };
29683061 logo_anim .update (dt );
29693062
29703063 // Draw logo animation
29713064 logo_anim .draw ();
29723065
2973- // Draw loading text
2974- const loading_text = "TRINITY" ;
2975- const text_size : f32 = 24 ;
2976- const text_width = rl .MeasureTextEx (font , loading_text , text_size , 1 ).x ;
2977- rl .DrawTextEx (font , loading_text , .{
2978- .x = (@as (f32 , @floatFromInt (g_width )) - text_width ) / 2 ,
2979- .y = @as (f32 , @floatFromInt (g_height )) * 0.8 ,
2980- }, text_size , 1 , LOGO_GREEN );
2981-
29823066 // Check if animation complete
29833067 if (logo_anim .is_complete ) {
29843068 loading_complete = true ;
2985- // Spawn welcome hint after loading
2986- clusters .spawn (640.0 , 400.0 , "SHIFT+1 = CHAT | SHIFT+2 = CODE" , false );
29873069 }
29883070
29893071 continue ; // Skip main canvas rendering during loading
@@ -2999,8 +3081,11 @@ pub fn main() !void {
29993081 effects .draw ();
30003082 goal .draw (time );
30013083
3002- // Cursor
3003- drawPhotonCursor (mx , my , cursor_hue , time );
3084+ // Static logo in center (small, glassmorphism green, stays after loading)
3085+ logo_anim .logo_scale = @min (@as (f32 , @floatFromInt (g_width )) / LogoAnimation .SVG_WIDTH , @as (f32 , @floatFromInt (g_height )) / LogoAnimation .SVG_HEIGHT ) * 0.35 ;
3086+ logo_anim .logo_offset = .{ .x = @as (f32 , @floatFromInt (g_width )) / 2 , .y = @as (f32 , @floatFromInt (g_height )) / 2 };
3087+ logo_anim .applyMouse (mx , my , dt );
3088+ logo_anim .draw ();
30043089
30053090 // Glass panels (on top of everything except UI)
30063091 panels .draw (time , font );
0 commit comments