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:
2026-05-03 03:17:38 +08:00
parent dbe965d0e9
commit b510b33628
13 changed files with 818 additions and 32 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
uploads/
*.zip
/.build/

731
app.js Normal file
View 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
View File

@@ -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" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

27
deploy/build.sh Executable file
View 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)"

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;