Skip to content

Commit 05485ee

Browse files
committed
feat: homepage MorphingAscii animation and MDX per-page LLM route
- src/components/MorphingAscii.tsx: ASCII art animation that morphs between Cadence code snippets on the landing page hero - src/routes/llms[.]mdx.docs.$.ts: per-page MDX plain-text endpoint at /llms.mdx/<doc-slug> for LLM-friendly single-page context fetching
1 parent 0076872 commit 05485ee

4 files changed

Lines changed: 254 additions & 45 deletions

File tree

src/components/MorphingAscii.tsx

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import React, { useState, useEffect, useCallback, Suspense } from 'react';
2+
import { Canvas } from '@react-three/fiber';
3+
import { AsciiRenderer, Float, useTexture, OrbitControls, useGLTF } from '@react-three/drei';
4+
import * as THREE from 'three';
5+
import { useTheme } from 'next-themes';
6+
7+
// ----------------------------------------------------
8+
// SCENE COMPONENTS
9+
// ----------------------------------------------------
10+
11+
function FlowCoin() {
12+
const texture = useTexture('/assets/flow-logo.svg');
13+
14+
return (
15+
<group scale={0.85}>
16+
{/* Front Face */}
17+
<mesh position={[0, 0, 0.11]}>
18+
<circleGeometry args={[2, 64]} />
19+
<meshStandardMaterial map={texture} roughness={0.3} metalness={0.8} />
20+
</mesh>
21+
{/* Back Face */}
22+
<mesh position={[0, 0, -0.11]} rotation={[0, Math.PI, 0]}>
23+
<circleGeometry args={[2, 64]} />
24+
<meshStandardMaterial map={texture} roughness={0.3} metalness={0.8} />
25+
</mesh>
26+
{/* Edge */}
27+
<mesh rotation={[Math.PI / 2, 0, 0]}>
28+
<cylinderGeometry args={[2, 2, 0.22, 64, 1, true]} />
29+
<meshStandardMaterial color="#00FF94" roughness={0.3} metalness={0.8} />
30+
</mesh>
31+
</group>
32+
);
33+
}
34+
35+
function CadenceLogo3D() {
36+
const { scene } = useGLTF('/assets/logo.glb');
37+
38+
useEffect(() => {
39+
scene.traverse((child) => {
40+
if (child instanceof THREE.Mesh) {
41+
child.material.roughness = 0.4;
42+
child.material.metalness = 0.6;
43+
}
44+
});
45+
}, [scene]);
46+
47+
return (
48+
<group position={[0, 0, 0]}>
49+
<primitive object={scene} scale={1.2} />
50+
</group>
51+
);
52+
}
53+
54+
function CryptoKitty3D() {
55+
const { scene } = useGLTF('/assets/cryptokitty.glb');
56+
57+
useEffect(() => {
58+
scene.traverse((child) => {
59+
if (child instanceof THREE.Mesh) {
60+
child.material.roughness = 0.5;
61+
child.material.metalness = 0.2;
62+
}
63+
});
64+
}, [scene]);
65+
66+
return (
67+
<group position={[0, -0.4, 0]}>
68+
<primitive object={scene} scale={2.2} />
69+
</group>
70+
);
71+
}
72+
73+
useGLTF.preload('/assets/logo.glb');
74+
useGLTF.preload('/assets/cryptokitty.glb');
75+
76+
function AsciiScene({ activeCycleIdx, fgColor }: { activeCycleIdx: number; fgColor: string }) {
77+
return (
78+
<>
79+
<ambientLight intensity={1.5} />
80+
<directionalLight position={[10, 10, 10]} intensity={3} />
81+
<pointLight position={[-10, -10, -10]} intensity={1} />
82+
83+
<Suspense fallback={null}>
84+
<Float speed={2.5} rotationIntensity={0.2} floatIntensity={0.5}>
85+
<group visible={activeCycleIdx === 0}>
86+
<CadenceLogo3D />
87+
</group>
88+
<group visible={activeCycleIdx === 1}>
89+
<FlowCoin />
90+
</group>
91+
<group visible={activeCycleIdx === 2}>
92+
<CryptoKitty3D />
93+
</group>
94+
</Float>
95+
</Suspense>
96+
97+
{/* Ascii shader layer */}
98+
<AsciiRenderer
99+
fgColor={fgColor}
100+
bgColor="transparent"
101+
characters=" .:'-+*=%@#"
102+
resolution={0.25}
103+
color={false}
104+
invert={false}
105+
/>
106+
</>
107+
);
108+
}
109+
110+
// ----------------------------------------------------
111+
// MAIN COMPONENT
112+
// ----------------------------------------------------
113+
114+
export function MorphingAscii() {
115+
const [activeCycleIdx, setActiveCycleIdx] = useState(0);
116+
const [mounted, setMounted] = useState(false);
117+
const [ready, setReady] = useState(false);
118+
const [isMobile, setIsMobile] = useState(false);
119+
const { resolvedTheme } = useTheme();
120+
121+
const onCreated = useCallback(() => {
122+
// Wait two frames so AsciiRenderer has fully taken over
123+
requestAnimationFrame(() => {
124+
requestAnimationFrame(() => {
125+
setReady(true);
126+
});
127+
});
128+
}, []);
129+
130+
useEffect(() => {
131+
setMounted(true);
132+
const mq = window.matchMedia('(max-width: 1023px)');
133+
setIsMobile(mq.matches);
134+
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
135+
mq.addEventListener('change', handler);
136+
return () => mq.removeEventListener('change', handler);
137+
}, []);
138+
139+
useEffect(() => {
140+
const interval = setInterval(() => {
141+
setActiveCycleIdx(prev => (prev + 1) % 3);
142+
}, 6000);
143+
return () => clearInterval(interval);
144+
}, []);
145+
146+
if (!mounted) {
147+
return <div className="w-full h-[280px] lg:h-[700px] lg:w-[700px]" />;
148+
}
149+
150+
return (
151+
<div
152+
className={[
153+
'relative flex flex-col items-center justify-center p-0 m-0 group',
154+
'w-full h-[280px]',
155+
'lg:h-[700px] lg:w-[700px] lg:-right-5',
156+
'overflow-hidden lg:overflow-visible',
157+
isMobile ? '' : 'cursor-grab active:cursor-grabbing',
158+
].join(' ')}
159+
>
160+
{/* 3D ASCII Canvas */}
161+
<div
162+
className="absolute inset-0 overflow-hidden lg:overflow-visible ascii-wrapper"
163+
style={{
164+
opacity: ready ? 1 : 0,
165+
transition: 'opacity 0.3s ease-in',
166+
}}
167+
>
168+
{resolvedTheme === 'light' && (
169+
<style>{`
170+
.ascii-wrapper > div {
171+
font-weight: 900 !important;
172+
}
173+
`}</style>
174+
)}
175+
<Canvas
176+
camera={{ position: [0, 0, 6.5], fov: 50 }}
177+
style={{ touchAction: isMobile ? 'pan-y' : 'none' }}
178+
onCreated={onCreated}
179+
>
180+
<color attach="background" args={['transparent']} />
181+
<AsciiScene
182+
activeCycleIdx={activeCycleIdx}
183+
fgColor={resolvedTheme === 'light' ? '#000000' : '#00FF94'}
184+
/>
185+
<OrbitControls
186+
autoRotate
187+
autoRotateSpeed={6}
188+
enableZoom={false}
189+
enablePan={false}
190+
enableRotate={!isMobile}
191+
/>
192+
</Canvas>
193+
</div>
194+
195+
{/* Mobile: transparent overlay above ascii-wrapper to intercept touches
196+
and pass vertical scroll to the browser natively via pan-y */}
197+
{isMobile && (
198+
<div
199+
style={{
200+
position: 'absolute',
201+
inset: 0,
202+
zIndex: 10,
203+
touchAction: 'pan-y',
204+
}}
205+
/>
206+
)}
207+
</div>
208+
);
209+
}

src/routes/community.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ function Community() {
6161
linkText: 'Read FLIPs',
6262
},
6363
{
64-
title: 'Join Design Meetings',
65-
description: 'Join the core contributors and community to investigate, design, and decide on language features.',
64+
title: 'Join Dev Office Hours',
65+
description: 'Follow the Flow developers on X for announcements about upcoming office hours and community sessions.',
6666
icon: <Users className="w-5 h-5 text-blue-400" />,
67-
link: 'https://docs.google.com/document/d/1KMGdiZ7qX9aoyH2WEVGHjsvBTNPTN6my8LcNmSVivLQ/edit',
68-
linkText: 'Meeting Notes',
67+
link: 'https://calendar.google.com/calendar/u/0/r?cid=c_47978f5cd9da636cadc6b8473102b5092c1a865dd010558393ecb7f9fd0c9ad0@group.calendar.google.com',
68+
linkText: 'Add to Calendar',
6969
},
7070
];
7171

src/routes/index.tsx

Lines changed: 21 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ function Home() {
305305
const [mcpClient, setMcpClient] = useState<string>(mcpClients[0].value);
306306
const [mcpMode, setMcpMode] = useState<McpMode>("local");
307307
const [typingKey, setTypingKey] = useState(0);
308-
const [activeCodeTab, setActiveCodeTab] = useState<"nft" | "defi">("nft");
308+
const [activeCodeTab, setActiveCodeTab] = useState<"nft" | "defi">("defi");
309309
const codeContainerRef = useRef<HTMLDivElement>(null);
310310
const codeInnerRef = useRef<HTMLDivElement>(null);
311311
const [codeHeight, setCodeHeight] = useState<string>("auto");
@@ -501,18 +501,18 @@ function Home() {
501501
<div className="w-3 h-3 rounded-full bg-green-500/80" />
502502
</div>
503503
<div className="flex items-center gap-1 bg-black/5 dark:bg-white/5 p-1 rounded-lg self-start sm:self-auto">
504-
<button
505-
onClick={() => setActiveCodeTab("nft")}
506-
className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${activeCodeTab === "nft" ? "bg-white dark:bg-white/10 text-neutral-900 dark:text-white shadow-sm" : "text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"}`}
507-
>
508-
NFT
509-
</button>
510504
<button
511505
onClick={() => setActiveCodeTab("defi")}
512506
className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${activeCodeTab === "defi" ? "bg-white dark:bg-white/10 text-neutral-900 dark:text-white shadow-sm" : "text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"}`}
513507
>
514508
DeFi
515509
</button>
510+
<button
511+
onClick={() => setActiveCodeTab("nft")}
512+
className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${activeCodeTab === "nft" ? "bg-white dark:bg-white/10 text-neutral-900 dark:text-white shadow-sm" : "text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"}`}
513+
>
514+
NFT
515+
</button>
516516
</div>
517517
</div>
518518
<div className="overflow-hidden transition-[height] duration-300 ease-in-out" ref={codeContainerRef} style={{ height: codeHeight }}>
@@ -586,17 +586,24 @@ function Home() {
586586
Direct Ownership
587587
</h3>
588588
<p className="text-neutral-600 dark:text-[#888] leading-relaxed mb-8">
589-
Assets are objects stored directly in the user's account. To
590-
move value, you physically move the object. Impossible to
591-
duplicate.
589+
Tokens live in the user's account as vault resources. Withdrawing
590+
creates a new resource the compiler tracks: it must be deposited
591+
or explicitly destroyed, it cannot be silently lost or duplicated.
592592
</p>
593593
<div className="p-6 bg-[var(--accent)]/5 border border-[var(--accent)]/20 rounded-lg font-mono text-sm text-[var(--accent)]/80 leading-relaxed">
594-
let vault {"<-"} account.withdraw(amount: 10.0)
594+
let vault = signer.storage.borrow{"<"}
595+
<br />
596+
{" "}auth(FungibleToken.Withdraw) {"&"}FlowToken.Vault{">"}
597+
<br />
598+
{" "}(from: /storage/flowTokenVault)!
599+
<br />
600+
<br />
601+
let payment {"<-"} vault.withdraw(amount: 10.0)
595602
<br />
596-
receiver.deposit(vault: {"<-"} vault)
603+
receiver.deposit(from: {"<-"} payment)
597604
<br />
598-
<span className="text-white/40 mt-2 block">
599-
// vault is now empty, reentrancy impossible
605+
<span className="text-[var(--accent)]/40 mt-2 block">
606+
{"// payment: must be deposited, cannot be duplicated"}
600607
</span>
601608
</div>
602609
</div>
@@ -629,33 +636,6 @@ function Home() {
629636
</div>
630637
</section>
631638

632-
{/* ════════ SCALE & TRUST ════════ */}
633-
<section className="relative py-24 px-6 border-t border-black/5 dark:border-white/5">
634-
<div className="max-w-7xl mx-auto">
635-
<div className="flex items-center gap-6 mb-16">
636-
<div className="h-px flex-1 bg-gradient-to-r from-transparent to-black/10 dark:to-white/10" />
637-
<span className="font-mono text-xs text-neutral-500 dark:text-[#666] uppercase tracking-widest">
638-
Built For Scale
639-
</span>
640-
<div className="h-px flex-1 bg-gradient-to-l from-transparent to-black/10 dark:to-white/10" />
641-
</div>
642-
643-
<div className="flex flex-wrap justify-center gap-12 md:gap-24 opacity-30 grayscale hover:grayscale-0 transition-all duration-500">
644-
<span className="text-2xl font-black tracking-tighter">
645-
NBA TOP SHOT
646-
</span>
647-
<span className="text-2xl font-black tracking-tighter">
648-
NFL ALL DAY
649-
</span>
650-
<span className="text-2xl font-black tracking-tighter">
651-
TICKETMASTER
652-
</span>
653-
<span className="text-2xl font-black tracking-tighter">
654-
DISNEY
655-
</span>
656-
</div>
657-
</div>
658-
</section>
659639

660640
{/* ════════ FOOTER ════════ */}
661641
<footer className="relative py-16 px-6 border-t border-black/5 dark:border-white/5 bg-neutral-100 dark:bg-[#050505]">

src/routes/llms[.]mdx.docs.$.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createFileRoute, notFound } from '@tanstack/react-router';
2+
import { source } from '@/lib/source';
3+
4+
export const Route = createFileRoute('/llms.mdx/docs/$')({
5+
server: {
6+
handlers: {
7+
GET: async ({ params }) => {
8+
const slugs = params._splat?.split('/') ?? [];
9+
const page = source.getPage(slugs);
10+
if (!page) throw notFound();
11+
12+
return new Response(await page.data.getText('processed'), {
13+
headers: {
14+
'Content-Type': 'text/markdown',
15+
},
16+
});
17+
},
18+
},
19+
},
20+
});

0 commit comments

Comments
 (0)