Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
87ca723
feat(intro): cinematic 3D particle entry + zoom reveal (skeleton)
joyehuang Jun 22, 2026
a616704
feat(intro): rewrite with ASCII-particle avatar assemble
joyehuang Jun 22, 2026
7a60bba
fix(intro): replay button visibility + align avatar assemble to real …
joyehuang Jun 22, 2026
11dd961
feat(tour): JoJo mascot onboarding tour with spotlight
joyehuang Jun 22, 2026
1729f7f
fix(tour): scroll restoration, spotlight mis-position, bouncy easing
joyehuang Jun 22, 2026
4256d23
feat(intro): rewrite as 'Embedding Space Dive' — 3D Three.js journey
joyehuang Jun 22, 2026
b0fd264
fix(intro): avatar silhouette alignment + position calibration
joyehuang Jun 22, 2026
4826dc8
feat(intro): rewrite as 'joye.log' — typewriter + log cascade
joyehuang Jun 22, 2026
c18a2d9
feat(intro): atmosphere + per-char stagger + scan light + token glow
joyehuang Jun 22, 2026
3884373
feat(intro): grouped log + morph into landing sections
joyehuang Jun 22, 2026
a1f62e7
feat(intro): particle-dispersion morph — chars shatter + fly to section
joyehuang Jun 22, 2026
1d3cb84
feat(intro): Experience showcase — terminal 'company tour' + reorder
joyehuang Jun 22, 2026
f7553d6
fix(intro): softer particle morph — drift past + swift-out + 7-keyframe
joyehuang Jun 22, 2026
c07b0f7
feat(intro): interactive showcase — rail selector + hover info + clic…
joyehuang Jun 22, 2026
9a3902e
feat(intro): boot sequence + keyboard nav + glitch + char trails
joyehuang Jun 22, 2026
5520a95
fix(intro): kill jank + parallel reveal for soft morph-to-page handoff
joyehuang Jun 22, 2026
4f7a69a
fix(intro): guarantee Experience section can never get stuck hidden
joyehuang Jun 23, 2026
94828ef
feat(intro): redesign entry as a portfolio card-grid pop-in
joyehuang Jun 23, 2026
f4cd345
feat(intro): holographic 3D deck with GSAP timeline + particle shatter
joyehuang Jun 23, 2026
a097ec9
feat(intro): reconceive as 'The Generation' — LLM-streamed self-intro
joyehuang Jun 23, 2026
175f279
fix(intro): trim tour file eof whitespace
joyehuang Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
729 changes: 729 additions & 0 deletions .intro-archive/intro-3d-embedding.ts.bak

Large diffs are not rendered by default.

Binary file added public/intro/aixcut.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/intro/atypica.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/intro/faishion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/intro/playyy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
130 changes: 130 additions & 0 deletions src/components/intro/IntroOverlay.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
/**
* IntroOverlay — "The Generation"
*
* The site introduces its author the same way their products work: by
* generating. A prompt appears, then a short bio streams in token-by-token;
* the entities it names (products, companies, topics) light up as the
* model "predicts" them. When generation finishes, the text lifts away and
* the landing page rises into place beneath.
*
* SSR renders the full bio (with entity spans already in place); the client
* script walks the text, hides it char-by-char, then streams it back in.
*/
import { JOYE_LOG_GROUPS } from '@/data/joye-log'
import './intro.css'

const isDev = import.meta.env.DEV

// Counts are derived from the canonical inventory so the status line never
// drifts out of sync with the rest of the site.
const counts = (() => {
const by = (t: string) =>
JOYE_LOG_GROUPS.find((g) => g.type === t)?.entries.length ?? 0
return { roles: by('work'), repos: by('repo'), posts: by('blog') }
})()
---

<script is:inline>
;(function () {
try {
var doc = document.documentElement
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches
var played = sessionStorage.getItem('intro-played')
if (reduce || played) {
doc.classList.add('intro-skip')
} else {
doc.classList.add('intro-active')
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual'
}
window.scrollTo(0, 0)
try {
sessionStorage.setItem('intro-played', '1')
} catch (e) {}
}
} catch (e) {
document.documentElement.classList.add('intro-skip')
}
})()
</script>

<div id='intro-overlay' aria-hidden='true'>
<div class='intro-atmosphere' aria-hidden='true'></div>

<div id='intro-gen-wrap'>
<div id='intro-gen-prompt'>
<span class='prompt-arrow'>&gt;</span>
<span class='prompt-text'>who is joye?</span>
</div>

{/*
The generated bio. Entity spans are lit up by the client once their
last character streams in. Keep this copy tight and scannable — it's
the whole point of the intro.
*/}
<div id='intro-gen'>
<p class='gen-line'>
joye is an AI engineer based in Melbourne,
</p>
<p class='gen-line'>
shipping <span class='gen-entity' data-kind='work'>Playyy.ai</span>
at Adastra Labs,
</p>
<p class='gen-line'>
building agents at <span class='gen-entity' data-kind='work'>atypica</span>,
</p>
<p class='gen-line'>
and writing about <span class='gen-entity' data-kind='topic'>Transformers</span>,
<span class='gen-entity' data-kind='topic'>Attention</span>, and what it
takes to ship <span class='gen-entity' data-kind='topic'>LLM</span> products.
</p>
</div>

<div id='intro-gen-status'>
<span class='status-dot' aria-hidden='true'></span>
<span class='status-text'>generating</span>
</div>

<div id='intro-gen-foot'>
{`${counts.roles} roles · ${counts.repos} repos · ${counts.posts} posts`}
</div>
</div>
</div>

<script>
import '@/scripts/intro.ts'
</script>

{
isDev && (
<button id='intro-replay' type='button' aria-label='Replay intro' title='Replay intro'>
<svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>
<path d='M3 12a9 9 0 1 0 3-6.7' />
<path d='M3 4v5h5' />
</svg>
</button>
)
}

{
isDev && (
<script is:inline>
;(function () {
var btn = document.getElementById('intro-replay')
if (!btn) return
btn.addEventListener('click', function () {
try {
sessionStorage.removeItem('intro-played')
sessionStorage.removeItem('tour-played')
} catch (e) {}
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual'
}
window.scrollTo(0, 0)
location.reload()
})
})()
</script>
)
}
34 changes: 34 additions & 0 deletions src/components/intro/JoJoTour.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
/**
* JoJoTour — onboarding tour driven by the JoJo mascot.
*
* Mounts the React <JoJoTour> component via Astro island (client:idle).
* The component itself decides whether to play (based on prefers-reduced-motion
* + sessionStorage) and waits for the `intro:complete` event dispatched by
* src/scripts/intro.ts once the cinematic intro finishes.
*/
import './jojo-tour.css'
import JoJoTourRoot from './JoJoTour.tsx'

const isDev = import.meta.env.DEV
---

<JoJoTourRoot client:idle />

{
/* Expose a window hook for the dev Replay button to reset both
intro + tour so the whole experience can be re-watched in dev. */
isDev && (
<script is:inline>
;(function () {
window.__resetIntroAndTour = function () {
try {
sessionStorage.removeItem('intro-played')
sessionStorage.removeItem('tour-played')
} catch (e) {}
location.reload()
}
})()
</script>
)
}
Loading