Files
FacereWeb/app.js
Knowit b510b33628 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>
2026-05-03 03:17:38 +08:00

732 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Facere — cinematic scroll landing
Three sections, three production-art backgrounds, one continuous camera push. */
const {
useEffect,
useRef,
useState
} = React;
// 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));