Files
FacereWeb/app.jsx
Knowit 75aad16a26 Add Facere site source, styles, and assets
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:33:50 +08:00

572 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Facere — cinematic scroll landing
Three sections, three production-art backgrounds, one continuous camera push. */
const { useEffect, useRef, useState } = React;
/* ───────────────── PixelLogo ─────────────────
Bitmap glyphs rendered as a CSS grid of <span>s, so we control individual
pixel colors / glow / animations. Original blocky design (no copyrighted mark). */
// 7 wide × 9 tall chunky glyphs, designed in the style of the reference:
// thick stems, rounded/beveled corners on C/E/R, no diagonals.
const GLYPH_W = 7;
const GLYPH_H = 9;
const GLYPHS = {
F: [
"#######",
"#######",
"##.....",
"##.....",
"######.",
"######.",
"##.....",
"##.....",
"##.....",
],
A: [
".#####.",
"#######",
"##...##",
"##...##",
"#######",
"#######",
"##...##",
"##...##",
"##...##",
],
C: [
".######",
"#######",
"##.....",
"##.....",
"##.....",
"##.....",
"##.....",
"#######",
".######",
],
E: [
"#######",
"#######",
"##.....",
"##.....",
"######.",
"######.",
"##.....",
"#######",
"#######",
],
R: [
"######.",
"#######",
"##...##",
"##...##",
"######.",
"#####..",
"##.##..",
"##..##.",
"##...##",
],
};
function PixelLogo({ word = "FACERE", scale = 11, gap = 0 }) {
const letters = word.split("");
return (
<div className="pixel-logo" data-text={word}>
<div className="pixel-logo-grid" style={{ gap: `0 ${scale * 1.6}px` }}>
{letters.map((ch, li) => {
const g = GLYPHS[ch];
if (!g) return null;
return (
<div
key={li}
className="pixel-letter"
style={{
gridTemplateColumns: `repeat(${GLYPH_W}, ${scale}px)`,
gridTemplateRows: `repeat(${GLYPH_H}, ${scale}px)`,
gap: `${gap}px`,
}}
>
{g.flatMap((row, ri) =>
row.split("").map((c, ci) => (
<span
key={`${ri}-${ci}`}
className={c === "#" ? "px on" : "px"}
/>
))
)}
</div>
);
})}
</div>
{/* Ghost layers for subtle electric jitter */}
<div className="pixel-logo-ghost ghost-r" aria-hidden="true">
<PixelLogoStatic word={word} scale={scale} gap={gap} color="rgba(255,80,120,0.45)" />
</div>
<div className="pixel-logo-ghost ghost-c" aria-hidden="true">
<PixelLogoStatic word={word} scale={scale} gap={gap} color="rgba(33,234,255,0.55)" />
</div>
</div>
);
}
function PixelLogoStatic({ word, scale, gap, color }) {
const letters = word.split("");
return (
<div className="pixel-logo-grid" style={{ gap: `0 ${scale * 1.6}px` }}>
{letters.map((ch, li) => {
const g = GLYPHS[ch];
if (!g) return null;
return (
<div
key={li}
className="pixel-letter"
style={{
gridTemplateColumns: `repeat(${GLYPH_W}, ${scale}px)`,
gridTemplateRows: `repeat(${GLYPH_H}, ${scale}px)`,
gap: `${gap}px`,
}}
>
{g.flatMap((row, ri) =>
row.split("").map((c, ci) => (
<span
key={`${ri}-${ci}`}
className="px"
style={
c === "#"
? { background: color, boxShadow: "none" }
: { background: "transparent" }
}
/>
))
)}
</div>
);
})}
</div>
);
}
/* ───────────────── SmokeLayer ─────────────────
Pixel-art smoke puffs above each chimney, positioned as % over the bg image.
Each puff is a chunky, low-res-feeling translucent disc that drifts up + fades. */
function SmokeLayer({ chimneys, scale = 1, intensity = 1 }) {
// Each chimney emits N puffs on staggered phases.
const PUFFS_PER = 5;
const puffs = [];
chimneys.forEach((ch, ci) => {
for (let p = 0; p < PUFFS_PER; p++) {
const delay = (p / PUFFS_PER) * ch.dur + (ci * 0.3);
puffs.push({
id: `${ci}-${p}`,
x: ch.x,
y: ch.y,
size: ch.size * (0.85 + (p % 2) * 0.25),
dur: ch.dur,
delay,
drift: ch.drift + (p % 3 - 1) * 0.4,
});
}
});
return (
<div className="smoke-layer" style={{ "--smoke-scale": scale, "--smoke-intensity": intensity }}>
{puffs.map((p) => (
<span
key={p.id}
className="smoke-puff"
style={{
left: `${p.x}%`,
top: `${p.y}%`,
width: `${p.size}px`,
height: `${p.size}px`,
animationDuration: `${p.dur}s`,
animationDelay: `-${p.delay}s`,
"--drift": `${p.drift}rem`,
}}
/>
))}
</div>
);
}
/* ───────────────── EmberLayer ─────────────────
Tiny pulsing orange points scattered across the horizon — gives the city
/factory a flicker without recreating any structure in CSS. */
function EmberLayer({ count = 18, area }) {
const embers = useRef(null);
if (!embers.current) {
const arr = [];
for (let i = 0; i < count; i++) {
arr.push({
x: area.x + Math.random() * area.w,
y: area.y + Math.random() * area.h,
size: 2 + Math.random() * 2,
dur: 1.6 + Math.random() * 3,
delay: Math.random() * 4,
hue: Math.random() > 0.7 ? "warm" : "soft",
});
}
embers.current = arr;
}
return (
<div className="ember-layer">
{embers.current.map((e, i) => (
<span
key={i}
className={`ember ember-${e.hue}`}
style={{
left: `${e.x}%`,
top: `${e.y}%`,
width: `${e.size}px`,
height: `${e.size}px`,
animationDuration: `${e.dur}s`,
animationDelay: `-${e.delay}s`,
}}
/>
))}
</div>
);
}
/* ───────────────── SparkLayer ─────────────────
For factory interior — small orange sparks bursting from the welding zone. */
function SparkLayer() {
const sparks = useRef(null);
if (!sparks.current) {
const arr = [];
for (let i = 0; i < 14; i++) {
arr.push({
a: -90 + (Math.random() - 0.5) * 160, // angle deg
d: 18 + Math.random() * 36, // distance
size: 2 + Math.random() * 2,
dur: 0.6 + Math.random() * 1.0,
delay: Math.random() * 1.6,
});
}
sparks.current = arr;
}
return (
<div className="spark-layer">
<span className="spark-core" />
{sparks.current.map((s, i) => {
const rad = (s.a * Math.PI) / 180;
const tx = Math.cos(rad) * s.d;
const ty = Math.sin(rad) * s.d;
return (
<span
key={i}
className="spark"
style={{
"--tx": `${tx}px`,
"--ty": `${ty}px`,
width: `${s.size}px`,
height: `${s.size}px`,
animationDuration: `${s.dur}s`,
animationDelay: `-${s.delay}s`,
}}
/>
);
})}
</div>
);
}
/* ───────────────── ChipPulses ─────────────────
Subtle pulsing orange dots over the PCB area. */
function ChipPulses() {
// hand-placed % over the chip image
const dots = [
{ x: 76, y: 38, dur: 2.4, delay: 0 },
{ x: 82, y: 28, dur: 2.0, delay: 0.7 },
{ x: 88, y: 33, dur: 2.8, delay: 1.2 },
{ x: 73, y: 60, dur: 2.2, delay: 0.4 },
{ x: 80, y: 75, dur: 2.6, delay: 1.0 },
{ x: 92, y: 55, dur: 2.0, delay: 1.6 },
{ x: 70, y: 48, dur: 3.0, delay: 0.2 },
{ x: 95, y: 70, dur: 2.4, delay: 0.9 },
];
return (
<div className="chip-pulses">
{dots.map((d, i) => (
<span
key={i}
className="chip-pulse"
style={{
left: `${d.x}%`,
top: `${d.y}%`,
animationDuration: `${d.dur}s`,
animationDelay: `-${d.delay}s`,
}}
/>
))}
{/* Soft chip breathing glow */}
<span className="chip-glow" />
</div>
);
}
/* ───────────────── HeroSection ───────────────── */
function HeroSection({ bgRef, contentRef }) {
// Chimney positions (% over the exterior factory image) — hand-tuned to artwork.
// The three tall chimneys cluster at ~74%, 78%, 83% with tops near y≈49.5%.
// Smaller stacks at 71%, 80%, 85.5%.
// Image 1: assets/exterior-factory.png
const chimneys = [
{ x: 77.5, y: 51.0, size: 22, dur: 7.2, drift: -0.3 },
{ x: 80.5, y: 49.5, size: 32, dur: 8.0, drift: -0.4 },
{ x: 84.5, y: 50.4, size: 30, dur: 7.6, drift: 0.2 },
{ x: 86.5, y: 51.0, size: 22, dur: 7.0, drift: 0.3 },
{ x: 89.5, y: 49.8, size: 32, dur: 8.2, drift: -0.2 },
{ x: 92.0, y: 51.4, size: 22, dur: 7.4, drift: 0.4 },
];
return (
<section className="scene scene-hero" data-screen-label="01 Hero">
<div className="bg-wrap" ref={bgRef}>
{/* Image 1 — exterior factory */}
<div
className="bg-image"
style={{ backgroundImage: "url(assets/exterior-factory.png)" }}
/>
<SmokeLayer chimneys={chimneys} />
<EmberLayer
count={22}
area={{ x: 32, y: 48, w: 65, h: 8 }}
/>
<div className="vignette" />
<div className="haze" />
</div>
<div className="content content-hero" ref={contentRef}>
<div className="logo-wrap">
<PixelLogo word="FACERE" />
<div className="logo-scanlines" />
</div>
</div>
<div className="scroll-cue" aria-hidden="true">
<span className="scroll-cue-line" />
<span className="scroll-cue-label">SCROLL</span>
</div>
</section>
);
}
/* ───────────────── ProductSection ───────────────── */
function ProductSection({ bgRef, contentRef }) {
return (
<section className="scene scene-product" data-screen-label="02 Product">
<div className="bg-wrap" ref={bgRef}>
{/* Image 2 — factory interior / assembly line */}
<div
className="bg-image"
style={{ backgroundImage: "url(assets/factory-interior.png)" }}
/>
{/* Robotic arm idle motion: anchored to where the arms sit in the image */}
<div className="arm-pivot arm-left" />
<div className="arm-pivot arm-right" />
<SparkLayer />
{/* Conveyor pulse strip */}
<div className="conveyor-pulse" />
<div className="vignette product-vignette" />
<div className="haze" />
</div>
<div className="content content-product" ref={contentRef}>
<div className="tag">
<span className="tag-dot" />
<span>SYSTEM ONLINE NODE 02</span>
</div>
<h1 className="headline">
Facere is a
<br />
<span className="hl">Hardware Design Agent</span>
</h1>
<p className="body">
From natural-language requirements to engineering files, Facere executes an end-to-end agent workflow
</p>
</div>
</section>
);
}
/* ───────────────── InstallSection ───────────────── */
function InstallSection({ bgRef, contentRef }) {
const [copied, setCopied] = useState(false);
const cmd = "curl -fsSL https://facere.ai/install.sh | sh";
const copy = async () => {
try {
await navigator.clipboard.writeText(cmd);
setCopied(true);
setTimeout(() => setCopied(false), 1600);
} catch (e) {
setCopied(true);
setTimeout(() => setCopied(false), 1600);
}
};
return (
<section className="scene scene-install" data-screen-label="03 Install">
<div className="bg-wrap" ref={bgRef}>
{/* Image 3 — PCB / chip */}
<div
className="bg-image"
style={{ backgroundImage: "url(assets/pcb-chip.png)" }}
/>
<ChipPulses />
<div className="vignette" />
<div className="haze haze-deep" />
</div>
<div className="content content-install" ref={contentRef}>
<div className="tag">
<span className="tag-dot" />
<span>NODE 03 SUBSTRATE</span>
</div>
<h1 className="headline">
Get started with Facere
</h1>
<p className="subheadline">Install the CLI</p>
<div className={`cmd-box ${copied ? "copied" : ""}`} onClick={copy}>
<div className="cmd-bar">
<span className="cmd-led" />
<span className="cmd-led led-amber" />
<span className="cmd-led led-cyan" />
<span className="cmd-title">~/facere bash</span>
</div>
<div className="cmd-body">
<span className="cmd-prompt">$</span>
<span className="cmd-text">{cmd}</span>
<span className="cmd-cursor" />
</div>
<div className="cmd-hint">
{copied ? "✓ COPIED" : "CLICK TO COPY"}
</div>
</div>
<a className="docs-link" href="#" onClick={(e) => e.preventDefault()}>
docs.facere.ai/cli <span className="arrow"></span>
</a>
</div>
</section>
);
}
/* ───────────────── App / ScrollScene ─────────────────
Pin the stage, push the camera deeper through each section.
Each section's bg scales up while the next fades in over it. */
function App() {
const stageRef = useRef(null);
const heroBg = useRef(null), heroContent = useRef(null);
const prodBg = useRef(null), prodContent = useRef(null);
const instBg = useRef(null), instContent = useRef(null);
const heroSceneRef = useRef(null);
const prodSceneRef = useRef(null);
const instSceneRef = useRef(null);
const [progress, setProgress] = useState(0);
useEffect(() => {
if (!window.gsap || !window.ScrollTrigger) return;
const { gsap, ScrollTrigger } = window;
gsap.registerPlugin(ScrollTrigger);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// Initial state: only hero visible
gsap.set(prodSceneRef.current, { opacity: 0 });
gsap.set(instSceneRef.current, { opacity: 0 });
const ctx = gsap.context(() => {
// Master timeline pinned to stage; scrub linked to scroll.
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".scroll-track",
start: "top top",
end: "bottom bottom",
scrub: 0.6,
onUpdate: (self) => setProgress(self.progress),
},
});
// Timing map (extra-long product dwell):
// 0.00 → 0.20 Hero camera push
// 0.20 → 0.25 Hero → Product cross-fade
// 0.25 → 0.82 Product (very long dwell)
// 0.82 → 0.86 Product → Install cross-fade
// 0.86 → 1.00 Install settle
// Phase 1: Hero camera push-in
tl.to(heroBg.current, { scale: reduceMotion ? 1.05 : 1.35, ease: "none" }, 0);
tl.to(heroContent.current, { scale: reduceMotion ? 1.02 : 1.18, opacity: 0, ease: "none" }, 0);
// Phase 2: cross-fade hero → product
tl.to(heroSceneRef.current, { opacity: 0, ease: "none" }, 0.20);
tl.fromTo(prodSceneRef.current, { opacity: 0 }, { opacity: 1, ease: "none" }, 0.20);
tl.fromTo(prodBg.current, { scale: reduceMotion ? 1.05 : 1.18 }, { scale: 1, ease: "none" }, 0.20);
tl.fromTo(prodContent.current, { y: 40, opacity: 0 }, { y: 0, opacity: 1, ease: "none" }, 0.23);
// Phase 3: Long product dwell with slow camera creep
tl.to(prodBg.current, { scale: reduceMotion ? 1.05 : 1.22, ease: "none" }, 0.27);
tl.to(prodContent.current, { scale: reduceMotion ? 1.02 : 1.08, opacity: 0, ease: "none", duration: 0.06 }, 0.78);
// Phase 4: cross-fade product → install
tl.to(prodSceneRef.current, { opacity: 0, ease: "none" }, 0.82);
tl.fromTo(instSceneRef.current, { opacity: 0 }, { opacity: 1, ease: "none" }, 0.82);
tl.fromTo(instBg.current, { scale: reduceMotion ? 1.05 : 1.18 }, { scale: 1, ease: "none" }, 0.82);
tl.fromTo(instContent.current, { y: 40, opacity: 0 }, { y: 0, opacity: 1, ease: "none" }, 0.86);
// Phase 5: Install gentle settle
tl.to(instBg.current, { scale: reduceMotion ? 1.0 : 1.05, ease: "none" }, 0.90);
}, stageRef);
return () => ctx.revert();
}, []);
// Section indicator
const section = progress < 0.22 ? 0 : progress < 0.84 ? 1 : 2;
return (
<>
{/* Persistent UI: section indicator + brand mark in corner */}
<div className="hud">
<div className="hud-brand">FACERE</div>
<div className="hud-nav" aria-hidden="true">
{["EXT", "INT", "SUB"].map((label, i) => (
<div key={i} className={`hud-step ${section === i ? "is-active" : section > i ? "is-past" : ""}`}>
<span className="hud-step-dot" />
<span className="hud-step-label">{label}</span>
</div>
))}
</div>
<div className="hud-meta">
<span className="hud-meta-line" />
<span>DEPTH {Math.round(progress * 100).toString().padStart(2, "0")}</span>
</div>
</div>
{/* Scroll track sets total scrollable height; stage is pinned & sticky */}
<div className="scroll-track">
<div className="stage" ref={stageRef}>
<div className="scene-stack">
<div className="scene-slot" ref={heroSceneRef}>
<HeroSection bgRef={heroBg} contentRef={heroContent} />
</div>
<div className="scene-slot" ref={prodSceneRef}>
<ProductSection bgRef={prodBg} contentRef={prodContent} />
</div>
<div className="scene-slot" ref={instSceneRef}>
<InstallSection bgRef={instBg} contentRef={instContent} />
</div>
</div>
<div className="grain" />
</div>
</div>
</>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);