Add Facere site source, styles, and assets
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
571
app.jsx
Normal file
571
app.jsx
Normal file
@@ -0,0 +1,571 @@
|
||||
/* 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 />);
|
||||
Reference in New Issue
Block a user