Perf: pre-compile JSX, prod React, WebP assets, immutable cache
This is the big "make scrolling smooth" pass — the biggest wins live in cold-start, not in the scroll loop itself, but a fast cold start means GSAP starts in a non-thrashing state and stays there. - Drop @babel/standalone (~3MB) from the page entirely. app.jsx is pre-compiled to app.js via deploy/build.sh; the browser loads the compiled bundle directly. JSX-in-browser was running babel transform on every page load, which is brutal on phones. - Switch React UMD bundles from .development to .production.min: ~1.1MB → ~140KB, no dev-mode warnings/checks in the hot path. - Add `defer` + `<link rel=preload as=script>` for the React/GSAP CDN scripts so they fetch in parallel with HTML parse, execute in order after DOM is ready, and don't block first paint. - Re-encode the three 1.4–1.8MB PNG backgrounds as WebP at full size (~190KB total) plus a 900px-wide mobile variant (~52KB total). Mobile preload links use `media=` so phones never download the full-size variants. - Move bg-image URLs from inline JSX styles into styles.css so the mobile media query can swap them in cleanly. - nginx: long-cache versioned static assets (Cache-Control immutable, 1 year) since URLs already carry ?v=… cache busters; keep the HTML itself on must-revalidate so the version pointer can update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
uploads/
|
||||
*.zip
|
||||
/.build/
|
||||
|
||||
731
app.js
Normal file
731
app.js
Normal file
@@ -0,0 +1,731 @@
|
||||
/* Facere — cinematic scroll landing
|
||||
Three sections, three production-art backgrounds, one continuous camera push. */
|
||||
|
||||
const {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} = React;
|
||||
|
||||
// Mobile heuristic — captured once at boot. Used to skip expensive
|
||||
// effects (extra particles, heavy scrub smoothing) on phones.
|
||||
const IS_MOBILE = typeof window !== "undefined" && window.matchMedia("(max-width: 760px), (pointer: coarse) and (max-width: 900px)").matches;
|
||||
|
||||
/* ───────────────── 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 useResponsiveLogoScale(word = "FACERE") {
|
||||
const [scale, setScale] = useState(() => computeLogoScale(word));
|
||||
useEffect(() => {
|
||||
let raf = 0;
|
||||
const onResize = () => {
|
||||
cancelAnimationFrame(raf);
|
||||
raf = requestAnimationFrame(() => setScale(computeLogoScale(word)));
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
window.addEventListener("orientationchange", onResize);
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener("resize", onResize);
|
||||
window.removeEventListener("orientationchange", onResize);
|
||||
};
|
||||
}, [word]);
|
||||
return scale;
|
||||
}
|
||||
function computeLogoScale(word) {
|
||||
// Total logo width = letters * GLYPH_W * scale + (letters-1) * scale * 1.6
|
||||
// Solve for scale that fits the viewport with side margins.
|
||||
if (typeof window === "undefined") return 11;
|
||||
const vw = window.innerWidth;
|
||||
const letters = word.length;
|
||||
const sideMargin = vw < 480 ? 0.10 : vw < 760 ? 0.12 : 0.16; // each side
|
||||
const usable = vw * (1 - 2 * sideMargin);
|
||||
const denom = letters * GLYPH_W + (letters - 1) * 1.6;
|
||||
const fit = Math.floor(usable / denom);
|
||||
// Clamp so it doesn't get too small or absurdly large on ultrawide.
|
||||
return Math.max(4, Math.min(11, fit));
|
||||
}
|
||||
function PixelLogo({
|
||||
word = "FACERE",
|
||||
scale,
|
||||
gap = 0
|
||||
}) {
|
||||
const responsiveScale = useResponsiveLogoScale(word);
|
||||
const px = scale ?? responsiveScale;
|
||||
const letters = word.split("");
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: "pixel-logo",
|
||||
"data-text": word
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "pixel-logo-grid",
|
||||
style: {
|
||||
gap: `0 ${px * 1.6}px`
|
||||
}
|
||||
}, letters.map((ch, li) => {
|
||||
const g = GLYPHS[ch];
|
||||
if (!g) return null;
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
key: li,
|
||||
className: "pixel-letter",
|
||||
style: {
|
||||
gridTemplateColumns: `repeat(${GLYPH_W}, ${px}px)`,
|
||||
gridTemplateRows: `repeat(${GLYPH_H}, ${px}px)`,
|
||||
gap: `${gap}px`
|
||||
}
|
||||
}, g.flatMap((row, ri) => row.split("").map((c, ci) => /*#__PURE__*/React.createElement("span", {
|
||||
key: `${ri}-${ci}`,
|
||||
className: c === "#" ? "px on" : "px"
|
||||
}))));
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "pixel-logo-ghost ghost-r",
|
||||
"aria-hidden": "true"
|
||||
}, /*#__PURE__*/React.createElement(PixelLogoStatic, {
|
||||
word: word,
|
||||
scale: px,
|
||||
gap: gap,
|
||||
color: "rgba(255,80,120,0.45)"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "pixel-logo-ghost ghost-c",
|
||||
"aria-hidden": "true"
|
||||
}, /*#__PURE__*/React.createElement(PixelLogoStatic, {
|
||||
word: word,
|
||||
scale: px,
|
||||
gap: gap,
|
||||
color: "rgba(33,234,255,0.55)"
|
||||
})));
|
||||
}
|
||||
function PixelLogoStatic({
|
||||
word,
|
||||
scale,
|
||||
gap,
|
||||
color
|
||||
}) {
|
||||
const letters = word.split("");
|
||||
return /*#__PURE__*/React.createElement("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 /*#__PURE__*/React.createElement("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) => /*#__PURE__*/React.createElement("span", {
|
||||
key: `${ri}-${ci}`,
|
||||
className: "px",
|
||||
style: c === "#" ? {
|
||||
background: color,
|
||||
boxShadow: "none"
|
||||
} : {
|
||||
background: "transparent"
|
||||
}
|
||||
}))));
|
||||
}));
|
||||
}
|
||||
|
||||
/* ───────────────── 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 = IS_MOBILE ? 2 : 3;
|
||||
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 /*#__PURE__*/React.createElement("div", {
|
||||
className: "smoke-layer",
|
||||
style: {
|
||||
"--smoke-scale": scale,
|
||||
"--smoke-intensity": intensity
|
||||
}
|
||||
}, puffs.map(p => /*#__PURE__*/React.createElement("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`
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
/* ───────────────── 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 /*#__PURE__*/React.createElement("div", {
|
||||
className: "ember-layer"
|
||||
}, embers.current.map((e, i) => /*#__PURE__*/React.createElement("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`
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
/* ───────────────── SparkLayer ─────────────────
|
||||
For factory interior — small orange sparks bursting from the welding zone. */
|
||||
function SparkLayer() {
|
||||
const sparks = useRef(null);
|
||||
if (!sparks.current) {
|
||||
const arr = [];
|
||||
const count = IS_MOBILE ? 7 : 14;
|
||||
for (let i = 0; i < count; 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 /*#__PURE__*/React.createElement("div", {
|
||||
className: "spark-layer"
|
||||
}, /*#__PURE__*/React.createElement("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 /*#__PURE__*/React.createElement("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`
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/* ───────────────── ChipPulses ─────────────────
|
||||
Subtle pulsing orange dots over the PCB area. */
|
||||
function ChipPulses() {
|
||||
// hand-placed % over the chip image
|
||||
const allDots = [{
|
||||
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
|
||||
}];
|
||||
const dots = IS_MOBILE ? allDots.filter((_, i) => i % 2 === 0) : allDots;
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: "chip-pulses"
|
||||
}, dots.map((d, i) => /*#__PURE__*/React.createElement("span", {
|
||||
key: i,
|
||||
className: "chip-pulse",
|
||||
style: {
|
||||
left: `${d.x}%`,
|
||||
top: `${d.y}%`,
|
||||
animationDuration: `${d.dur}s`,
|
||||
animationDelay: `-${d.delay}s`
|
||||
}
|
||||
})), /*#__PURE__*/React.createElement("span", {
|
||||
className: "chip-glow"
|
||||
}));
|
||||
}
|
||||
|
||||
/* ───────────────── 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: 71.5,
|
||||
y: 51.0,
|
||||
size: 22,
|
||||
dur: 7.2,
|
||||
drift: -0.3
|
||||
}, {
|
||||
x: 74.5,
|
||||
y: 49.5,
|
||||
size: 32,
|
||||
dur: 8.0,
|
||||
drift: -0.4
|
||||
}, {
|
||||
x: 78.5,
|
||||
y: 50.4,
|
||||
size: 30,
|
||||
dur: 7.6,
|
||||
drift: 0.2
|
||||
}, {
|
||||
x: 80.5,
|
||||
y: 51.0,
|
||||
size: 22,
|
||||
dur: 7.0,
|
||||
drift: 0.3
|
||||
}, {
|
||||
x: 83.5,
|
||||
y: 49.8,
|
||||
size: 32,
|
||||
dur: 8.2,
|
||||
drift: -0.2
|
||||
}, {
|
||||
x: 86.0,
|
||||
y: 51.4,
|
||||
size: 22,
|
||||
dur: 7.4,
|
||||
drift: 0.4
|
||||
}];
|
||||
return /*#__PURE__*/React.createElement("section", {
|
||||
className: "scene scene-hero",
|
||||
"data-screen-label": "01 Hero"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "bg-wrap",
|
||||
ref: bgRef
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "image-frame"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "bg-image"
|
||||
}), /*#__PURE__*/React.createElement(SmokeLayer, {
|
||||
chimneys: IS_MOBILE ? chimneys.filter((_, i) => i % 2 === 0) : chimneys
|
||||
}), /*#__PURE__*/React.createElement(EmberLayer, {
|
||||
count: IS_MOBILE ? 10 : 22,
|
||||
area: {
|
||||
x: 32,
|
||||
y: 48,
|
||||
w: 65,
|
||||
h: 8
|
||||
}
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "vignette"
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "haze"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "content content-hero",
|
||||
ref: contentRef
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "logo-wrap"
|
||||
}, /*#__PURE__*/React.createElement(PixelLogo, {
|
||||
word: "FACERE"
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "logo-scanlines"
|
||||
}))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "scroll-cue",
|
||||
"aria-hidden": "true"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "scroll-cue-line"
|
||||
}), /*#__PURE__*/React.createElement("span", {
|
||||
className: "scroll-cue-label"
|
||||
}, "SCROLL")));
|
||||
}
|
||||
|
||||
/* ───────────────── ProductSection ───────────────── */
|
||||
function ProductSection({
|
||||
bgRef,
|
||||
contentRef
|
||||
}) {
|
||||
return /*#__PURE__*/React.createElement("section", {
|
||||
className: "scene scene-product",
|
||||
"data-screen-label": "02 Product"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "bg-wrap",
|
||||
ref: bgRef
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "image-frame"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "bg-image"
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "arm-pivot arm-left"
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "arm-pivot arm-right"
|
||||
}), /*#__PURE__*/React.createElement(SparkLayer, null), /*#__PURE__*/React.createElement("div", {
|
||||
className: "conveyor-pulse"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "vignette product-vignette"
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "haze"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "content content-product",
|
||||
ref: contentRef
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "tag"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "tag-dot"
|
||||
}), /*#__PURE__*/React.createElement("span", null, "SYSTEM ONLINE \u2014 NODE 02")), /*#__PURE__*/React.createElement("h1", {
|
||||
className: "headline"
|
||||
}, "Facere is a", /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("span", {
|
||||
className: "hl"
|
||||
}, "Hardware Design Agent")), /*#__PURE__*/React.createElement("p", {
|
||||
className: "body"
|
||||
}, "From natural-language requirements to engineering files, Facere executes an end-to-end agent workflow")));
|
||||
}
|
||||
|
||||
/* ───────────────── 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 /*#__PURE__*/React.createElement("section", {
|
||||
className: "scene scene-install",
|
||||
"data-screen-label": "03 Install"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "bg-wrap",
|
||||
ref: bgRef
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "image-frame"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "bg-image"
|
||||
}), /*#__PURE__*/React.createElement(ChipPulses, null)), /*#__PURE__*/React.createElement("div", {
|
||||
className: "vignette"
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "haze haze-deep"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "content content-install",
|
||||
ref: contentRef
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "tag"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "tag-dot"
|
||||
}), /*#__PURE__*/React.createElement("span", null, "NODE 03 \u2014 SUBSTRATE")), /*#__PURE__*/React.createElement("h1", {
|
||||
className: "headline"
|
||||
}, "Get started with Facere"), /*#__PURE__*/React.createElement("p", {
|
||||
className: "subheadline"
|
||||
}, "Install the CLI"), /*#__PURE__*/React.createElement("div", {
|
||||
className: `cmd-box ${copied ? "copied" : ""}`,
|
||||
onClick: copy
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "cmd-bar"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "cmd-led"
|
||||
}), /*#__PURE__*/React.createElement("span", {
|
||||
className: "cmd-led led-amber"
|
||||
}), /*#__PURE__*/React.createElement("span", {
|
||||
className: "cmd-led led-cyan"
|
||||
}), /*#__PURE__*/React.createElement("span", {
|
||||
className: "cmd-title"
|
||||
}, "~/facere \u2014 bash")), /*#__PURE__*/React.createElement("div", {
|
||||
className: "cmd-body"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "cmd-prompt"
|
||||
}, "$"), /*#__PURE__*/React.createElement("span", {
|
||||
className: "cmd-text"
|
||||
}, cmd), /*#__PURE__*/React.createElement("span", {
|
||||
className: "cmd-cursor"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "cmd-hint"
|
||||
}, copied ? "✓ COPIED" : "CLICK TO COPY")), /*#__PURE__*/React.createElement("a", {
|
||||
className: "docs-link",
|
||||
href: "#",
|
||||
onClick: e => e.preventDefault()
|
||||
}, "docs.facere.ai/cli ", /*#__PURE__*/React.createElement("span", {
|
||||
className: "arrow"
|
||||
}, "\u2192"))));
|
||||
}
|
||||
|
||||
/* ───────────────── 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);
|
||||
const lastDepth = useRef(-1);
|
||||
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",
|
||||
// On mobile, scrub:true (no smoothing lag) feels snappier and
|
||||
// skips an extra rAF interpolation per frame.
|
||||
scrub: IS_MOBILE ? true : 0.6,
|
||||
onUpdate: self => {
|
||||
// Re-render only when the displayed depth (0..100) changes —
|
||||
// otherwise we re-render the whole HUD on every scroll frame.
|
||||
const depth = Math.round(self.progress * 100);
|
||||
if (depth !== lastDepth.current) {
|
||||
lastDepth.current = depth;
|
||||
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 /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
||||
className: "hud"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "hud-brand"
|
||||
}, "FACERE"), /*#__PURE__*/React.createElement("div", {
|
||||
className: "hud-nav",
|
||||
"aria-hidden": "true"
|
||||
}, ["EXT", "INT", "SUB"].map((label, i) => /*#__PURE__*/React.createElement("div", {
|
||||
key: i,
|
||||
className: `hud-step ${section === i ? "is-active" : section > i ? "is-past" : ""}`
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "hud-step-dot"
|
||||
}), /*#__PURE__*/React.createElement("span", {
|
||||
className: "hud-step-label"
|
||||
}, label)))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "hud-meta"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "hud-meta-line"
|
||||
}), /*#__PURE__*/React.createElement("span", null, "DEPTH ", Math.round(progress * 100).toString().padStart(2, "0")))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "scroll-track"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "stage",
|
||||
ref: stageRef
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "scene-stack"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "scene-slot",
|
||||
ref: heroSceneRef
|
||||
}, /*#__PURE__*/React.createElement(HeroSection, {
|
||||
bgRef: heroBg,
|
||||
contentRef: heroContent
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "scene-slot",
|
||||
ref: prodSceneRef
|
||||
}, /*#__PURE__*/React.createElement(ProductSection, {
|
||||
bgRef: prodBg,
|
||||
contentRef: prodContent
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "scene-slot",
|
||||
ref: instSceneRef
|
||||
}, /*#__PURE__*/React.createElement(InstallSection, {
|
||||
bgRef: instBg,
|
||||
contentRef: instContent
|
||||
}))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "grain"
|
||||
}))));
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(/*#__PURE__*/React.createElement(App, null));
|
||||
17
app.jsx
17
app.jsx
@@ -367,11 +367,8 @@ function HeroSection({ bgRef, contentRef }) {
|
||||
<section className="scene scene-hero" data-screen-label="01 Hero">
|
||||
<div className="bg-wrap" ref={bgRef}>
|
||||
<div className="image-frame">
|
||||
{/* Image 1 — exterior factory */}
|
||||
<div
|
||||
className="bg-image"
|
||||
style={{ backgroundImage: "url(assets/exterior-factory.png)" }}
|
||||
/>
|
||||
{/* Image 1 — exterior factory (background-image set in styles.css) */}
|
||||
<div className="bg-image" />
|
||||
<SmokeLayer chimneys={IS_MOBILE ? chimneys.filter((_, i) => i % 2 === 0) : chimneys} />
|
||||
<EmberLayer
|
||||
count={IS_MOBILE ? 10 : 22}
|
||||
@@ -404,10 +401,7 @@ function ProductSection({ bgRef, contentRef }) {
|
||||
<div className="bg-wrap" ref={bgRef}>
|
||||
<div className="image-frame">
|
||||
{/* Image 2 — factory interior / assembly line */}
|
||||
<div
|
||||
className="bg-image"
|
||||
style={{ backgroundImage: "url(assets/factory-interior.png)" }}
|
||||
/>
|
||||
<div className="bg-image" />
|
||||
{/* 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" />
|
||||
@@ -458,10 +452,7 @@ function InstallSection({ bgRef, contentRef }) {
|
||||
<div className="bg-wrap" ref={bgRef}>
|
||||
<div className="image-frame">
|
||||
{/* Image 3 — PCB / chip */}
|
||||
<div
|
||||
className="bg-image"
|
||||
style={{ backgroundImage: "url(assets/pcb-chip.png)" }}
|
||||
/>
|
||||
<div className="bg-image" />
|
||||
<ChipPulses />
|
||||
</div>
|
||||
<div className="vignette" />
|
||||
|
||||
BIN
assets/exterior-factory-mobile.webp
Normal file
BIN
assets/exterior-factory-mobile.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
BIN
assets/exterior-factory.webp
Normal file
BIN
assets/exterior-factory.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/factory-interior-mobile.webp
Normal file
BIN
assets/factory-interior-mobile.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/factory-interior.webp
Normal file
BIN
assets/factory-interior.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
assets/pcb-chip-mobile.webp
Normal file
BIN
assets/pcb-chip-mobile.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/pcb-chip.webp
Normal file
BIN
assets/pcb-chip.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
27
deploy/build.sh
Executable file
27
deploy/build.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Re-compile app.jsx -> app.js so the browser can load it directly,
|
||||
# without shipping ~3MB of @babel/standalone to every visitor.
|
||||
#
|
||||
# Usage: deploy/build.sh
|
||||
# Run this before `git commit` whenever app.jsx changes.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
ROOT="$PWD"
|
||||
|
||||
# Use a dedicated build dir so we don't litter the repo root with
|
||||
# node_modules / package-lock.json.
|
||||
BUILD_DIR=".build"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
if [ ! -f "$BUILD_DIR/package.json" ]; then
|
||||
( cd "$BUILD_DIR" && npm init -y >/dev/null \
|
||||
&& npm install --silent --no-audit --no-fund \
|
||||
@babel/core@7 @babel/cli@7 @babel/preset-react@7 )
|
||||
fi
|
||||
|
||||
"$BUILD_DIR/node_modules/.bin/babel" app.jsx \
|
||||
--presets=@babel/preset-react \
|
||||
-o app.js
|
||||
|
||||
echo "Built app.js ($(wc -c < app.js) bytes)"
|
||||
@@ -17,28 +17,38 @@ server {
|
||||
try_files /facere.html =404;
|
||||
}
|
||||
|
||||
# The HTML entrypoint must NOT cache long — it carries the
|
||||
# ?v=… cache-busting tags that everything else relies on.
|
||||
location = /facere.html {
|
||||
add_header Cache-Control "public, max-age=0, must-revalidate";
|
||||
expires off;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /facere.html;
|
||||
}
|
||||
|
||||
# CSS/JS/JSX: short cache so future deploys are picked up promptly
|
||||
# CSS/JS: long-cache + immutable. URLs are versioned with ?v=… so
|
||||
# each deploy is a brand-new key; anything served here will never
|
||||
# need to be re-fetched in a session.
|
||||
location ~* \.(?:css|js|jsx)$ {
|
||||
expires 5m;
|
||||
add_header Cache-Control "public, max-age=300, must-revalidate";
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
types { text/css css; application/javascript js; application/javascript jsx; }
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Static media: long cache (filenames are stable / change when content changes)
|
||||
location ~* \.(?:png|jpe?g|gif|webp|svg|mp4|webm|woff2?|ttf|otf|ico)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
# Static media: long cache + immutable (filenames change when content does).
|
||||
location ~* \.(?:png|jpe?g|gif|webp|avif|svg|mp4|webm|woff2?|ttf|otf|ico)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
gzip_vary on;
|
||||
|
||||
access_log /var/log/nginx/facere.access.log;
|
||||
error_log /var/log/nginx/facere.error.log;
|
||||
|
||||
38
facere.html
38
facere.html
@@ -5,29 +5,43 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>FACERE — Hardware Design Agent</title>
|
||||
|
||||
<!-- Establish CDN connections early -->
|
||||
<link rel="preconnect" href="https://unpkg.com" crossorigin />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="dns-prefetch" href="https://unpkg.com" />
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=VT323&display=swap" rel="stylesheet" />
|
||||
|
||||
<link rel="stylesheet" href="styles.css?v=20260503-mobile2" />
|
||||
<link rel="stylesheet" href="styles.css?v=20260503-perf" />
|
||||
|
||||
<!-- Preload background images -->
|
||||
<link rel="preload" as="image" href="assets/exterior-factory.png" />
|
||||
<link rel="preload" as="image" href="assets/factory-interior.png" />
|
||||
<link rel="preload" as="image" href="assets/pcb-chip.png" />
|
||||
<!-- Preload critical scripts so they fetch in parallel with the CSS -->
|
||||
<link rel="preload" as="script" href="https://unpkg.com/react@18.3.1/umd/react.production.min.js" crossorigin />
|
||||
<link rel="preload" as="script" href="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js" crossorigin />
|
||||
<link rel="preload" as="script" href="https://unpkg.com/gsap@3.12.5/dist/gsap.min.js" crossorigin />
|
||||
<link rel="preload" as="script" href="https://unpkg.com/gsap@3.12.5/dist/ScrollTrigger.min.js" crossorigin />
|
||||
<link rel="preload" as="script" href="app.js?v=20260503-perf" />
|
||||
|
||||
<!-- Preload background images. Mobile gets the smaller variant via media= -->
|
||||
<link rel="preload" as="image" href="assets/exterior-factory.webp" media="(min-width: 761px)" fetchpriority="high" />
|
||||
<link rel="preload" as="image" href="assets/factory-interior.webp" media="(min-width: 761px)" />
|
||||
<link rel="preload" as="image" href="assets/pcb-chip.webp" media="(min-width: 761px)" />
|
||||
<link rel="preload" as="image" href="assets/exterior-factory-mobile.webp" media="(max-width: 760px)" fetchpriority="high" />
|
||||
<link rel="preload" as="image" href="assets/factory-interior-mobile.webp" media="(max-width: 760px)" />
|
||||
<link rel="preload" as="image" href="assets/pcb-chip-mobile.webp" media="(max-width: 760px)" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- React -->
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
<!-- Production React (no dev warnings, ~140KB total vs ~1.1MB for dev builds) -->
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js" integrity="sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z" crossorigin="anonymous" defer></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js" integrity="sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1" crossorigin="anonymous" defer></script>
|
||||
|
||||
<!-- GSAP + ScrollTrigger -->
|
||||
<script src="https://unpkg.com/gsap@3.12.5/dist/gsap.min.js"></script>
|
||||
<script src="https://unpkg.com/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
|
||||
<script src="https://unpkg.com/gsap@3.12.5/dist/gsap.min.js" defer></script>
|
||||
<script src="https://unpkg.com/gsap@3.12.5/dist/ScrollTrigger.min.js" defer></script>
|
||||
|
||||
<script type="text/babel" src="app.jsx?v=20260503-mobile2"></script>
|
||||
<!-- Pre-compiled from app.jsx (see deploy/build.sh). No babel-standalone in browser. -->
|
||||
<script src="app.js?v=20260503-perf" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
12
styles.css
12
styles.css
@@ -94,6 +94,13 @@ a { color: inherit; text-decoration: none; }
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
|
||||
/* WebP-encoded backgrounds for each scene. Default is the full-res
|
||||
variant; the mobile media query below swaps in a smaller one so
|
||||
phones decode ~50KB instead of ~190KB. */
|
||||
.scene-hero .bg-image { background-image: url("assets/exterior-factory.webp"); }
|
||||
.scene-product .bg-image { background-image: url("assets/factory-interior.webp"); }
|
||||
.scene-install .bg-image { background-image: url("assets/pcb-chip.webp"); }
|
||||
|
||||
.vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -931,6 +938,11 @@ a { color: inherit; text-decoration: none; }
|
||||
.scene-product .bg-wrap { transform-origin: 55% 70% !important; }
|
||||
.scene-install .bg-wrap { transform-origin: 70% 55% !important; }
|
||||
|
||||
/* Use the smaller WebP variants — ~52KB total instead of ~190KB. */
|
||||
.scene-hero .bg-image { background-image: url("assets/exterior-factory-mobile.webp"); }
|
||||
.scene-product .bg-image { background-image: url("assets/factory-interior-mobile.webp"); }
|
||||
.scene-install .bg-image { background-image: url("assets/pcb-chip-mobile.webp"); }
|
||||
|
||||
/* Hero: center the (now responsively sized) logo. */
|
||||
.content-hero {
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user