Files
FacereWeb/app.js
Knowit ea85a01edf Install scene: fix cmd-bar overlap, swap install command
The "CLICK TO COPY" hint was absolute-positioned at top:10/right:14 of
the cmd-box, landing on top of "~/facere — bash" in the bar header.
Move the hint into the bar as a flex sibling so the title and hint
share the row cleanly without overlap.

Replace the placeholder install command with the real installer pulled
from raw.githubusercontent.com, with FACERE_GH_TOKEN as a
"contact us for demo" placeholder.

Also fix deploy/build.sh: `npm init -y` rejects the leading-dot dir
name `.build`, and the preset must be resolved by absolute path since
babel runs from the repo root.
2026-05-08 00:45:21 +08:00

732 lines
23 KiB
JavaScript
Raw Permalink 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;
// 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://raw.githubusercontent.com/ZhaoYiping789/facere-installer/main/bootstrap.sh | FACERE_GH_TOKEN="contact us for demo"`;
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("span", {
className: "cmd-bar-hint"
}, copied ? "✓ COPIED" : "CLICK TO COPY")), /*#__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("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));