Skip to content

Commit db3a955

Browse files
gHashTagclaude
andcommitted
feat: logo hover highlight — block under cursor lights up pink, solid #08FAB5 fill, larger scale
- Replace cursor push physics with point-in-polygon hit detection - Hovered block highlights #FF69B4 (pink), base color solid #08FAB5 - Black outline 5px for clear block separation - Logo scale increased to 0.35 - Remove transparency — fully opaque blocks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b959390 commit db3a955

1 file changed

Lines changed: 155 additions & 70 deletions

File tree

src/vsa/photon_trinity_canvas.zig

Lines changed: 155 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -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

118129
const 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

Comments
 (0)