[codex] Add cinematic intro animation#49
Draft
joyehuang wants to merge 21 commits into
Draft
Conversation
Adds a Three.js-driven cinematic intro overlay on the landing page: - SSR renders a dark overlay shell + an inline <head> script that decides play/skip synchronously (prefers-reduced-motion or sessionStorage flag) - src/scripts/intro.ts lazy-imports three + gsap so they never enter the first-paint critical path - Phase 1 (0~0.9s): 5000-particle cloud fades in (2200 on mobile) - Phase 2 (0.9~1.4s): hold - Phase 3 (1.4~2.3s): cloud expands + dissolves, camera dollies in - Phase 4 (2.0~2.8s): overlay dissolves, hero zooms from scale(0.94)+blur(8px) to scale(1)+blur(0) via GSAP - Locks the page (overflow hidden) and suppresses the default .animate fade-in-up while active so GSAP can take over hero transitions - 4.5s hard timeout fallback in case WebGL boot fails - Adds @/scripts/* path alias Phase 2 (particles forming the avatar silhouette) and the shader-based noise dissolution are intentionally deferred until the base vibe feels right. New deps: three, @types/three
Replaces the previous Three.js particle cloud (which didn't fit the terminal/CLI/minimalist theme DNA) with a Canvas 2D ASCII-particle version that matches the blog's visual language: - Drops three.js entirely (-150KB), keeps only gsap (already a dep) - Particles are now monospace glyphs (█▓▒░*··>/<_0123…) rendered via Canvas 2D - Strictly monochrome — single primary blue, no rainbow palette - Phase 1 ENTER (0~1.5s): chars fly in from off-screen - Phase 2 CHAOS (1.5~3.5s): chars drift + flicker (data-stream feel) - Phase 3 ASSEMBLE (3.5~6s): chars lerp to avatar silhouette targets, shifting from decode glyphs to dense block glyphs as they approach - Phase 4 HOLD (6~7s): avatar complete, micro-flicker on outline - Phase 5 REVEAL (7~8.5s): chars burst outward, overlay dissolves, hero zooms from scale(.94)+blur(8px) - Avatar PNG URL is passed via data-attribute and sampled client-side (alpha channel → ~1200 target points on a 220x220 grid) - Subtle scanline pattern reinforces terminal DNA - New dev-only Replay button (top-right) clears sessionStorage + reloads - Dev-mode __intro.state hook for live tuning from the console
…hero img - Replay button: CSS was hiding it in both states (default display:none plus html.intro-active override). Fixed by making it display:flex by default and only hiding while intro-active plays. - Avatar assemble position: was hard-coded to viewport center, but the real hero <img> lives at the top of the page (cy ≈ 148). Now queries the actual #content-header img via getBoundingClientRect and assembles the silhouette to overlap it, so the char-avatar lands exactly on top of the real avatar when the overlay dissolves. - Silhouette size now scales with the real avatar's rendered size (~1.6x to extend slightly past the edges for a more dramatic reveal).
Adds a 10-step narrative tour that plays after the cinematic intro
finishes, walking new visitors through the landing page's core sections.
- New <JoJoTour> React component, mounted via client:idle island
- Component decides to play based on prefers-reduced-motion +
sessionStorage('tour-played') — one play per session
- Waits for window event 'intro:complete' (or window.__introDone flag)
from src/scripts/intro.ts so it launches exactly when the intro
overlay dissolves
- 10 steps: welcome → terminal → about → blog → notes → talks →
experience → open-source → skills → bye
- Strong spotlight via oversized box-shadow (rgba(0,0,0,0.68)) with a
rounded primary-tinted border around the active section
- JoJo mascot flies between sections (GSAP-like CSS transitions) with
a multi-line speech bubble (jojo-bubble now white-space: pre-line)
- Bottom control bar: Skip / N/10 progress / Next
- Auto-advances 2.4s per step (3.2s for welcome + bye); user can Next
to accelerate or Skip to bail
- Locks background scroll while active
- Recomputes spotlight + JoJo position on resize
- Replay button (top-right, dev only) now clears both intro-played
and tour-played so the full experience can be re-watched
Three small UX bugs that compounded into a janky first impression:
1. Replay from mid-page kept scroll position
- history.scrollRestoration = 'manual' + window.scrollTo(0,0) before
reload so the intro always starts from the top of the page.
2. First spotlight frame was misplaced (then 'caught up' late)
- scrollIntoView is async, so reading getBoundingClientRect right
after gave the pre-scroll position. Now we compute the target's
'future' rect — the viewport-centered position it will land in
once scroll completes — and set the spotlight to that immediately.
The smooth scroll and the spotlight's CSS transition now move in
sync, instead of spotlight lagging 500ms behind.
3. Bouncy / floaty easing made each step feel like it overshot
- Was cubic-bezier(0.65, 0, 0.35, 1) at 0.55–0.65s. Swapped to
Material standard cubic-bezier(0.4, 0, 0.2, 1) at 0.4s for both
spotlight + mascot transitions. Settles fast, no rubber-band.
Also cleaned up the resize handler to use the same 'future-centered'
rect math so it stays consistent with the per-step computation.
Replaces the Canvas 2D ASCII-particle intro with a 3D Three.js experience that ties directly to the author's AI / Agent / LLM / Code / RAG work. The concept: the visitor's viewpoint dives into a 3D concept-space populated by 4 clusters of glowing tokens (Agent / LLM / Code / RAG), connected by attention lines that pulse like signal flow. The tokens then converge to the global center and finally materialize into the author's avatar silhouette, which lines up with the real hero <img> for the reveal. Architecture: - Three.js Points + custom ShaderMaterial (reliable, vs InstancedMesh + InstancedBufferAttribute which wasn't picking up per-instance attrs) - Vertex shader mixes between 3 positional states per token: home (cluster) -> converge center -> avatar silhouette target - Fragment shader paints soft glowing dots (core + halo) with additive blending - Avatar targets sampled from the hero <img> alpha channel, mapped into world coords so the particle silhouette lands at the same screen position as the real avatar at reveal time - Phase 1 boot line rendered as a styled HTML overlay (terminal DNA) - Attention lines: intra-cluster (2 nearest neighbors) + cross-cluster, with flicker driven by sin(time) - GSAP timeline drives fadeIn / converge / materialize globals + camera position; render loop pushes those into shader attributes each frame - Mouse parallax (desktop only), reduced-motion fallback, 16s hard timeout Re-installs three + @types/three (~150KB gz). Tuning still needed: avatar silhouette vertical position, attention pulse direction, possibly per-token z jitter for depth on the silhouette.
Three compounding bugs made the assembled avatar land in the wrong
place across the screen, plus a flat (overstretched) aspect ratio.
1. Page wasn't scrolled to top before measuring the hero <img>
- The inline script ran scrollRestoration='manual' but didn't actually
scrollTo(0,0). When the visitor previously left the page mid-scroll
(e.g. after the JoJo tour), reload restored that scroll, putting the
hero <img> at rect.top = -1669 — every world-space computation below
was then garbage.
- Now both the inline script AND runIntro() itself call window.scrollTo
(0, 0) defensively, in case anything scrolls the page in between.
2. Camera lookAt was running on GSAP's onUpdate only
- GSAP's onUpdate fires on its own tick which can lag the render loop
by a frame, leaving the camera matrix out of sync with the position
GSAP just wrote. The silhouette then rendered off-center.
- Now call camera.lookAt(0, 0, 0) every frame inside the render tick.
3. Silhouette was stretched flat (5:1 wide)
- Used sampleMax (the larger of sampleW/sampleH) as the divisor, but
the avatar PNG is mostly empty vertical space so sampleH is much
smaller than sampleW. The rendered silhouette came out 522x147.
- Now scale uniformly by sampleH and let width follow naturally,
preserving the PNG's intrinsic head+shoulders aspect ratio.
- Y axis also flipped (PNG pixel Y is downward, world Y is upward);
previously the silhouette was implicitly upside down, which is why
earlier 'fix the Y position' attempts overshot.
4. Silhouette sized to ~27% of viewport height (revealHalfH * 0.55),
capped so the top can't spill past the screen edge given the hero
img sits at NDC y ≈ 0.69.
5. Overlay background softened to ~94% opacity (was fully opaque) so
the hero silhouette bleeds through subtly as tokens approach their
target positions — makes the handoff feel continuous.
Verified end-to-end: rendered center (754, 146) vs expected (756, 153)
= dx=-2px, dy=-7px. Avatar bbox 243x84 fully on-screen.
Replaces the abstract 3D 'Embedding Space Dive' with a content-driven entry animation that tells Joye's actual story instead of decorating with shapes. The new intro types '> cat joye.log' one character at a time, then cascades Joye's real shipped work — 12 blog posts (oldest → newest), 2 open-source repos, 4 internship roles — down the screen as a log. The whole log then coalesces into a soft glow and the overlay dissolves to reveal the landing page. Why this and not the previous versions: - Previous versions (Three.js point cloud, ASCII chars, embedding clusters) were all variations on 'abstract particles converge on a shape' — technically different, narratively identical, and not meaningful to a first-time visitor. - This version uses REAL content (blog titles, repo names, role lines) that the visitor can actually read and remember. The animation IS the introduction, not a curtain in front of it. Architecture: - Drops three.js entirely (-150KB gz). Intro is now pure DOM + CSS transitions — ~5KB of logic, 60fps on any device, no WebGL required. - New src/data/joye-log.ts holds the canonical list of work; the SSR template renders all lines into the DOM up front (visible to crawlers, screen-reader friendly), the client script just orchestrates reveal timing. - Phase 1 TYPEWRITER: chars are appended at 65ms ± 30ms jitter so it reads as typing, not metronome. - Phase 2 CASCADE: lines reveal top-to-bottom, 90ms stagger for the first 6 (early work, less weight) and 130ms for the rest. - Phase 3 FOOTER: '18 entries · 12 posts · 2 repos · 4 roles' summary fades in. - Phase 4 COALESCE: log scales to 0.32 + blur(18px) + lift 12vh over 1.4s — metaphor of the body of work compacting into the author. - Phase 5 REVEAL: overlay opacity → 0 in parallel with hero scale(.96)+blur(8px) → scale(1)+blur(0) over 1s. 600ms overlap so there's no hard cut. - Visual: monochrome cool palette only (blue-gray + soft white), strict terminal DNA (JetBrains Mono, scanline overlay, soft dark navy background instead of pure black), breathing room between lines. Disables <JoJoTour> on the landing page for now (per the user's 'don't touch the tour yet' note — focus is on getting the intro right first). The component still exists and will be re-enabled later. Old Three.js intro archived under .intro-archive/ for reference.
Major UI polish pass on joye.log — the content/narrative was right, but the surface was too flat. This adds the cinematic feel without changing the story. Atmosphere (covers the whole overlay, doesn't touch the content): - New .intro-atmosphere layer with two pseudo-elements - ::before is an inline SVG feTurbulence noise pattern (180x180 tile, fractalNoise baseFrequency 0.85, two octaves) animated by 4-step grain-shift at 0.6s — gives a real film-grain texture that's never static. screen-blended at 10% opacity so it's a texture, not a haze. - The layer itself paints a radial vignette (transparent center → 55% black at corners) so the eye is funneled into the log column. Per-char stagger (the most visually distinctive change): - SSR now wraps every title char and meta char in <span class='char' style='--i:N'>. Char index drives a calc()-based transition-delay. - When a line receives .intro-visible, the line container fades in normally but each character inside also slides up + un-blurs with a 14ms stagger starting at 80ms. Result: each title visibly types itself out, line after line. Scan light sweep: - Each .intro-log-line has a ::before that's an oversized horizontal gradient band (cool blue → bright white → cool blue). - On .intro-visible, the gradient animates background-position from 100% → -50% over 0.85s, so a vertical 'beam' sweeps left → right across the line as it appears. screen-blended so it adds light without flattening text. Keyword + number accent tokens: - renderTokenized() in IntroOverlay.astro wraps keywords (Agent / LLM / Transformer / RoPE / Attention / RMSNorm / FeedForward / OpenHarness / Interview / Mock / Playyy.ai / atypica.ai / AIXCut / fAIshion.ai) and numbers (\b\d[\d,]*(?:\.\d+)?(?:h\d+m)?\b) in token-kw / token-num spans, while still char-staggering inside. - .token-kw: brighter blue + 8px+18px text-shadow glow - .token-num: mint green, semibold, smaller glow - These pull the visitor's eye to the concepts that matter. Footer chars also stagger in (8ms each) when the footer becomes visible. Footer placeholder class 'intro-hidden-init' removed (it was a leftover that was overriding the visible state). Footer now toggles via the same .intro-visible pattern. Verified: bun run check passes, dev server runs the full 9s timeline cleanly, all 18 log lines render at SSR with per-char spans (visible to view-source / crawlers).
Major restructuring to make the intro and the landing page read as one continuous experience instead of 'cinematic, then a hard cut to the page'. Content now grouped, not flat: - src/data/joye-log.ts restructured into JOYE_LOG_GROUPS — Blog / Open Source / Experience — matching the same Section titles the landing page already renders. - Each entry has a weight (0/1/2) driving brightness + size: weight=2 (Agent era, recent) is brightest and largest; weight=0 (early work, history) is dim and small. The cascade now has clear visual hierarchy instead of reading as a flat list. - SSR renders a group header ([ Blog ] 12) above each cluster with a gradient text label + accent bracket; a thin vertical rule on the left edge of each group ties entries to their parent. Cascade respects grouping: - Groups reveal top → bottom (Blog → Open Source → Experience). - Inside each group, lines reveal in weight-descending order so the eye lands on the marquee work first, then the history. - Group header slides in slightly before its entries. Phase 4 MORPH — the headline change: - For each intro-log-group, findLandingSection() locates the corresponding <main> <section> by matching its <h2> text against the group's label (Blog / Open Source / Experience). - morphGroupIntoSection() does a FLIP-style transform: translates the group onto the section's bounding rect + scales it to the section's size, then fades the group out (with a 6px blur) while the section itself fades in 350ms into the morph — the eye reads it as a handoff, not two parallel motions. - Hero (#content-header, #content) reveals in parallel as the visual anchor. Phase 5 REVEAL remaining sections: - Sections that don't have a corresponding log group (About, Notes, Talks, Education, Skills, SiteStats) were pre-hidden at the start of Phase 4 so dropping the .intro-active lock doesn't flash them. - revealRemainingSections() now staggers them top → bottom at 90ms each, so the page feels like it's being assembled, not 'switched on'. Bug fix wrapped in: pre-hide all landing sections before removing .intro-active so #content becoming visible doesn't momentarily show every section at once, then morph/reveal choreography drives each one individually.
Replaces the flat 'translate + scale + fade' group morph with a
proper particle-dispersion sequence so the transition from log → landing
page reads as the page literally growing out of the intro, not as a
crossfade.
Per group, the new morph has three beats over ~1.9s:
A. JITTER (0 → 0.3s)
Every char in the group runs a 5-keyframe Web Animations sequence
with small random translate offsets (±3-4px). Visual: the line
destabilizes, like a signal about to break up.
B. SCATTER + FLY (0.3 → 1.5s)
Each char picks:
- a scatter direction biased 180° away from the section's center
(so chars initially explode outward, away from where they're
going), with ±0.4π random spread
- a scatter distance 40-120px
- a final target = a random point inside the section's bounding
rect (so the chars actually 'land on' the section, not in front
of it)
- a random rotation (-45°..+45°) for visual richness
Then a 4-keyframe animation runs:
0% rest
25% scattered position (outward explosion, blur 2px, scale 0.9)
60% mid-flight between scatter and target (blur 4px, scale 0.7)
100% on-section target (blur 10px, scale 0.3, opacity 0)
Duration 1.1-1.45s, delay 0-180ms, all randomized per-char so the
cloud feels organic, not synchronized. easing: cubic-bezier(0.45, 0,
0.55, 1) — symmetric ease-in-out so the scatter and the arrival both
read.
While chars fly, the group's non-char chrome (header, stamp, meta
etc.) fades from 1 → 0 over 1.3s starting at +200ms.
C. MATERIALIZE (0.7s into the sequence)
Section content (opacity 0 → 1, translateY 16px → 0) fades in while
chars are still mid-flight. The eye reads this as a handoff: chars
arriving on the section dissolve into glow as the section's real
content fades up through them.
Implementation notes:
- Uses the native Web Animations API (element.animate) rather than GSAP
or CSS — gives us per-element keyframes + per-element delay without
spawning thousands of style tags.
- All random values (scatter angle, distance, target offset, rotation,
duration, delay) are computed per-char up front, so the same char
always animates the same way on a given run.
- fill: 'forwards' on every animation so chars stay in their end state
until the group is removed by overlay teardown.
Pixel-count diagnostic during testing showed the expected pattern:
during scatter, bright-pixel count climbs gradually as chars spread;
at fly-mid it jumps 40x as chars overlay the section and the section
fades in together — exactly the 'growing into the page' beat we want.
Adds a full-viewport 'company tour' beat between the Experience cascade
and the rest of the intro, so the visitor sees Joye's actual workplaces
instead of just text labels for them.
Reorder: Experience group is now first (was last). The visitor meets
Joye through where he works right now, then sees the body of work that
got him there.
Experience showcase:
- New #intro-showcase layer in IntroOverlay.astro, sibling of #intro-log.
SSR-renders one .showcase-panel per Experience entry that has a
screenshot field.
- Each panel = a CRT terminal frame (traffic-light dots + titlebar
reading 'joye@experience — zsh' + '01/04' counter) wrapping a full
homepage screenshot of the company.
- Screenshots are terminal-ized via CSS filter chain: grayscale .55 +
brightness .78 + contrast 1.12 + sepia .18 + hue-rotate 170deg +
saturate .85. Result reads as 'this site rendered through a CRT'.
- A repeating-linear-gradient scan-line pattern + radial vignette
overlay sells the CRT effect, multiplied into the screenshot.
- Caption (bottom-left): '> Adastra Labs → Playyy.ai · description',
JetBrains Mono with primary-blue glow on the title.
- The active panel gets a slow dolly-in (scale 1.08 → 1.0 over 1.6s)
so each company reads like a camera move, not a slideshow.
Showcase flow in intro.ts:
- After the Experience log group cascades in, it slides up + blurs out
(opacity 0, translateY -20px, scale .94, blur 6px).
- #intro-showcase scales in from .92 → 1.
- Panels cycle: 1.4s per company, last one holds 1.9s (so the current
role lands harder). Counter ('01' .. '04') updates in the titlebar.
- After the last panel, showcase fades out and the Blog + Open-Source
groups cascade in normally.
Screenshots:
- public/intro/{playyy,atypica,aixcut,faishion}.png — captured the four
company homepages at 1512x982 via browser-use, stored as static assets.
Extended morph on user request:
- Jitter phase 0.30s → 0.45s
- Scatter+fly 1.10-1.45s → 1.60-2.05s, with delay spread 0-180ms → 0-220ms
- Group fade 1.30s → 1.70s
- Materialize trigger 700ms → 950ms
- Total morph 1.90s → 2.60s
The dispersion now has time to breathe — you can see chars explode,
curve, and arrive.
Experience section doesn't re-morph — it was just showcased, so a
second particle dispersion would be redundant. It fades in directly
alongside the morph of Blog + Open Source.
The morph was reading as 'stiff' because: - 4 keyframes gave the motion a mechanical, evenly-spaced feel - chars hit the section and hard-cut to opacity 0 (no drift) - easing was symmetric ease-in-out — chars moved at constant speed visually, no inertia This rewrites the per-char animation to feel like real particles: 7 keyframes per char instead of 4: 0% rest 14% starting to scatter (gentle lead-in) 28% scattered peak (max outward displacement) 52% mid-flight, curving back toward section 76% approaching section 88% arrived, still glowing faintly 95% drifting past the section, dissolving 100% gone Two important changes: 1. DRIFT — after a char arrives at the section (88%), it keeps moving past it (95%, 100%) while fading from 0.22 → 0.08 → 0 opacity and blur from 11px → 16px → 22px. Removes the hard cut at the end. 2. SWIFT-OUT EASING — cubic-bezier(0.16, 1, 0.3, 1) on every char. Chars now decelerate naturally, like they have inertia. Was using symmetric ease-in-out which made speed feel constant. Plus: - Duration 1.6-2.05s → 2.2-2.8s (more time to read the motion) - Delay spread 0-220ms → 0-320ms (more staggered arrivals) - Rotation ±100° → ±120° with progressive multiplier 0.18 / 0.38 / 0.65 / 0.88 / 1.0 / 1.15 / 1.3 across keyframes — chars spin as they fly, accelerating slightly into the drift - Scale curve 1 → 0.96 → 0.86 → 0.68 → 0.48 → 0.32 → 0.2 → 0.12 — smoother than the previous 1 → 0.9 → 0.7 → 0.3 stair steps - Group chrome fade revised from 3-point to 4-point (1 → 0.7 → 0.3 → 0) so it doesn't outpace the chars - Total morph 2.6s → 3.2s, materialize trigger 950ms → 1100ms so the section fade aligns with chars arriving, not before Jitter phase also softened: 'linear' → 'ease-in-out' so the destabilization feels more like a wiggle than a vibration.
…k-to-visit Showcase is no longer a passive auto-playing slideshow. The visitor can now drive it. Layout rewrite: - Left rail (230px wide, terminal-menu styled) lists the four companies with idx + name. The active item gets a mint-green cursor (▸), brighter text, a left accent border, and a horizontal gradient background — same visual language as a focused row in tmux / vim. - Main stage is unchanged (CRT-framed screenshot) but now sits next to the rail instead of full-width. Interaction model (the actual point of this change): - Hover a rail item → showcase pauses auto-advance and jumps to that company. Move the mouse away → auto-advance resumes from the new position after the normal 1.5s tick. - Hover the main stage → also pauses (so the info panel below can be read without the screenshot changing underneath). - Click any rail item, or click the main stage, → opens the company's product site in a new tab (data-url driven). The dedicated 'visit site ↗' button inside the info panel navigates normally and stops propagation so clicks on it don't double-trigger. - Auto-cycle still happens by default (1.5s per company, 2s on the last) so a hands-off visitor still gets the full tour in ~6.5s. Hover info panel: - Floats on the right side of the stage, 280px wide, dark glass (backdrop-filter blur) with a primary-tinted border + soft glow. - Shows: company (small uppercase) → product name (large, blue-glow) → meta rows [role, period, stack chips] → highlight bullets (mint ▸) → 'visit site ↗' button. - New per-entry fields in joye-log.ts drive it: role / period / stack / highlights / url. Each Experience entry is now a fuller picture of what Joye actually did there, not just a label. - Caption at the bottom fades to 0.5 opacity while info is hovered so the two don't compete for the eye. Stronger CRT / terminal feel throughout: - Heavier screenshot filter: grayscale 0.7 (was 0.55), brightness 0.7 (was 0.78), contrast 1.18 (was 1.12) — screenshot reads more as 'rendered through a CRT' than 'photo with a tint'. - New .showcase-scanlines layer with a slow vertical sweep animation (5s linear infinite, 100px) — simulates CRT refresh. - New .showcase-stage::after radial bloom — cool blue glow at the center, screen-blended, sells the 'light being emitted' feel. - Traffic-light dots now have a glow halo (box-shadow on each i). - Active counter glows mint, title border is brighter, rail active state has a left accent border in mint. Verified end-to-end: showcase activates, auto-advances through 4 panels, hovering a rail item pauses + switches, leaving resumes, click opens URL. bun run check passes.
Free-form polish pass — four detail upgrades that each make the intro
feel more like a real terminal session and less like a slideshow.
1. Multi-line boot sequence (typewriter)
The single '> cat joye.log' was thin. Boot now types three lines in
sequence:
> ssh joye@mind
↳ loading 18 entries... ok
> cat joye.log▌
Reads like sshing into a system. The output line is dimmer, indented,
prefixed with ↳, and types faster (32ms vs 60ms per char) so it
doesn't drag the boot. Cursor only blinks on the final command line.
Boot phase is ~3.5s now (was 1.5s) but it's earnings — the visitor
sees the system 'come online'.
2. Keyboard navigation in the showcase
Real terminals are driven by the keyboard. Showcase now responds to:
ArrowDown / ArrowRight → next company
ArrowUp / ArrowLeft → previous company
Enter / Space → open the current company's URL
All keypresses pause auto-advance, same as hover. The keydown handler
is scoped to window but bails early if showcase isn't active, and
cleans up on teardown.
3. Glitch transition between panels
When the visitor switches companies (keyboard or rail hover), the new
panel briefly RGB-shifts: screenshot hue rotates 140° → 200° → 180°
while translating ±3-4px, and a horizontal scan band sweeps top to
bottom in 350ms steps(2, end). Sells a CRT channel-flip. The glitch
class self-clears after 350ms so it can re-trigger next time.
4. Character trails during the particle morph
Each flying char now carries two drop-shadows (cool blue trailing
left, brighter blue-white leading right) that grow then fade across
the trajectory — peak at mid-flight (52% keyframe), gone by arrival.
Reads as a comet trail / firefly light streak instead of a blur blob.
Two-sided shadow gives a subtle chromatic-aberration flavor at the
tails.
Verified: bun run check passes, boot renders 3 lines, ArrowDown moves
counter 02 → 03, glitch class is added + removed on switch, char
shadows are present in morph keyframes.
Two distinct problems from the user feedback:
1. '破碎感就是卡卡的' (the shatter feels janky)
Cause: I had added two drop-shadow filters per char (cool blue trail
+ bright leading edge) across 8 keyframes. Several hundred chars ×
8 keyframes of drop-shadow recomputation = GPU couldn't keep up,
so the morph stuttered.
Fix: drop-shadow removed entirely. Chars now animate transform +
opacity + blur only, which are all GPU-composited. The comet-trail
aesthetic is sacrificed for fluidity — fluidity wins.
2. '碎掉以后进入到主页的过渡太生硬' (morph-to-page handoff is stiff)
Cause: Phase 5 was串行 — await morphs → await revealRemaining →
overlay.classList.add('intro-hidden') → await sleep(500). The page
appeared in discrete steps: 'morph, then snap-to-page'.
Fix: complete timeline rewrite. Morph, remaining-sections reveal,
and overlay dissolve now all run IN PARALLEL:
- t+0 morph kicks off (chars fly)
- t+350ms Experience section fades in (showcased, no morph)
- t+500ms remaining 5 sections (About/Notes/Talks/Ed/Skills)
stagger in at 60ms each
- t+1100ms morphed sections (Blog/Open Source) start fading in
- t+1400ms overlay starts dissolving (scale 1.06 + blur 14px +
opacity 0, 0.9s)
- t+3200ms morph resolves
- t+4100ms overlay fully dissolved, removed from DOM
The page now 'emerges through' the dissipating overlay instead of
the overlay snapping away.
Overlay dissolve upgraded from a flat opacity fade to a real
'dissolve': opacity + scale(1.06) + blur(14px) over 0.9s. Reads as
the overlay diffusing outward, not being switched off.
Remaining-sections stagger tightened from 90ms to 60ms so all five
land within the morph window instead of trailing it.
Experience was the only landing section whose visibility relied entirely
on findLandingSection('Experience') succeeding — it was excluded from
the remainingSections safety-net, so a null lookup would leave it at
opacity 0 forever while every other section restored. Now Experience
rides the same stagger as the other non-morphed sections, and
revealImmediately() also clears any stale section inline styles if the
intro aborts mid-morph.
Replaces the log-cascade + CRT terminal showcase with a single portfolio card grid that pops up one-by-one in a diagonal wave. Flow (~11s, down from ~14s): typewriter boot -> grid pop-in wave -> hold -> dissolve + page reveal - New PORTFOLIO_CARDS model in joye-log.ts flattens every work role, open-source repo, and marquee (weight >= 2) blog post into one story-ordered deck: work -> repos -> writing (11 cards). - Cards are kind-accented (work=blue / repo=green / blog=cyan) with a screenshot media area for experiences and a glyph + gradient for repos/posts, a per-card scan-light sheen, and weight-driven glow. - Pop-in uses an overshoot easing + 75ms diagonal stagger; dissolve lifts cards away in reverse while the overlay opens up and landing sections stagger in beneath it. - Work/repo cards stay click-through-able to their product/repo URL. - Drops the now-unused cascade/showcase/particle-morph machinery.
Rebuilds the entry as a cinematic, GSAP-orchestrated sequence:
CRT power-on -> typewriter -> 3D materialize from depth
-> hold (parallax + holographic flicker) -> particle shatter to page
- Cards fly in from deep Z (translateZ -780) with rotation + blur + bloom,
assembling into a tilted deck with real perspective; the deck tilts down
to a resting angle and tracks the cursor via 3D parallax during the hold.
- Holographic card look: per-kind accent color, HUD corner brackets, card
+ global CRT scanlines, chromatic-aberration titles, bloom on weight-2
cards, animated diagonal sheen, subtle scanline flicker once settled.
- On handoff each card bursts into 28 particles that streak outward then
curve toward its matching landing section (work->Experience, repo->Open
Source, blog->Blog) while the sections materialize beneath and the
overlay dissolves. ~308 particles total, cleaned up after.
- Timeline tightened to ~9s. revealImmediately() now also clears GSAP
inline props so a failed intro can never leave the page blank.
Drops the busy card-grid / holographic-deck directions entirely. The new
intro is one coherent idea executed with restraint: the site introduces
its author the way their products work — by generating.
Flow (~7s):
wrap fades in -> bio streams token-by-token (entities light up)
-> status: done · Xs -> text lifts away, page rises beneath
- Centered layout with real negative space (padding 6vh + 30px gap), large
calm typography (clamp 18-25px). Fixes the cramped / edge-to-edge feel
of the previous directions.
- The bio is SSR'd with entity spans already in place; the client splits
it into per-char spans and emits them in small 1-3 char 'token' batches
with punctuation-aware pauses + occasional 'thinking' hesitations, so it
reads like real LLM sampling. A streaming cursor follows the caret.
- Entities (Playyy.ai / atypica / Transformers / Attention / LLM) light up
the moment their final char is emitted: accent color + glow + an
underline that draws in.
- Status line pulses 'generating', then resolves to 'done · 5.3s' with the
real elapsed stream time.
- Handoff is a single clean motion: the text lifts + blurs away while the
landing sections stagger up beneath and the overlay dissolves. No
particle explosions.
- Reverts joye-log.ts to its original state (the dead portfolio-card model
is gone).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR records the experimental homepage entrance-animation branch. It adds a cinematic intro layer for the blog homepage, centered on an LLM-style self-introduction: the site asks
who is joye?, streams the answer token-by-token, highlights named entities, then hands off into the normal landing page.This is opened as a draft because the concept is strong, but the interaction, pacing, mobile behavior, and long-term maintenance shape still need review.
What changed
IntroOverlay.astroandsrc/scripts/intro.tsfor the main entrance sequence.intro.cssfor the cinematic overlay, streaming text, entity highlights, atmosphere, reduced-motion skip behavior, and dev replay button.src/pages/index.astrobefore the homepage content.src/data/joye-log.tsas structured inventory for work/projects/posts used by the intro status line and earlier animation explorations.JoJoTour.astroJoJoTour.tsxjojo-tour.csspublic/intro/..intro-archive/intro-3d-embedding.ts.bakfor reference.175f279 fix(intro): trim tour file eof whitespace.Behavior notes
sessionStorage.prefers-reduced-motion: reduceskips the intro.Validation
git diff --check main...origin/feat/intro-cinematicpasses after the whitespace cleanup.I did not rerun the full Astro build/check in this request; this PR is primarily opened as a draft record for the existing branch and should get a proper visual/browser pass before merging.
Follow-up notes
Things worth reviewing before this becomes merge-ready: