update miletruth page and remove unwanted files
This commit is contained in:
@@ -12,18 +12,7 @@ type Props = {
|
||||
const COUNT_START = 0.7;
|
||||
const COUNT_END = 0.97;
|
||||
|
||||
function Metric({ kpi, scroll, index }: { kpi: Kpi; scroll: MotionValue<number>; index: number }) {
|
||||
const [jitter, setJitter] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
// Tiny micro-fluctuation between -0.4% and +0.4%
|
||||
const val = (Math.random() - 0.5) * 0.008;
|
||||
setJitter(val);
|
||||
}, 800 + Math.random() * 600); // slightly staggered updates
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
function Metric({ kpi, scroll, index, jitter }: { kpi: Kpi; scroll: MotionValue<number>; index: number; jitter: number }) {
|
||||
// Single reactive string — updates without re-rendering React.
|
||||
const value = useTransform(scroll, (v) => {
|
||||
const t = Math.min(1, Math.max(0, (v - COUNT_START) / (COUNT_END - COUNT_START)));
|
||||
@@ -140,10 +129,24 @@ function Metric({ kpi, scroll, index }: { kpi: Kpi; scroll: MotionValue<number>;
|
||||
const MetricCard = React.memo(Metric);
|
||||
|
||||
function MetricsPanel({ scroll }: Props) {
|
||||
// One shared interval drives the live "operational" micro-fluctuation for all
|
||||
// cards (was 5 independent timers). Paused under reduced-motion and when the
|
||||
// tab is hidden.
|
||||
const [jitters, setJitters] = React.useState<number[]>(() => KPIS.map(() => 0));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
const id = setInterval(() => {
|
||||
if (document.hidden) return;
|
||||
setJitters(KPIS.map(() => (Math.random() - 0.5) * 0.008));
|
||||
}, 1100);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="dm-opt-metrics" role="group" aria-label="Optimization results">
|
||||
{KPIS.map((kpi, i) => (
|
||||
<MetricCard key={kpi.key} kpi={kpi} scroll={scroll} index={i} />
|
||||
<MetricCard key={kpi.key} kpi={kpi} scroll={scroll} index={i} jitter={jitters[i]} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import { Canvas, useFrame } from "@react-three/fiber";
|
||||
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
|
||||
import { EffectComposer, Bloom } from "@react-three/postprocessing";
|
||||
import { KernelSize } from "postprocessing";
|
||||
import { COLORS } from "./constants";
|
||||
import { damp, lerp } from "./math";
|
||||
@@ -47,7 +47,7 @@ function OptimizationCanvas({ progress, reduced = false, isMobile = false, activ
|
||||
return (
|
||||
<Canvas
|
||||
flat
|
||||
dpr={[1, isMobile || reduced ? 1.3 : 1.6]}
|
||||
dpr={[1, isMobile || reduced ? 1.25 : 1.5]}
|
||||
camera={{ position: [0, 9, 19], fov: 50, near: 0.1, far: 120 }}
|
||||
gl={{ antialias: !isMobile, powerPreference: "high-performance", alpha: false }}
|
||||
frameloop={active ? "always" : "never"}
|
||||
@@ -74,7 +74,6 @@ function OptimizationCanvas({ progress, reduced = false, isMobile = false, activ
|
||||
radius={isMobile ? 0.6 : 0.75}
|
||||
kernelSize={KernelSize.MEDIUM}
|
||||
/>
|
||||
<Vignette eskil={false} offset={0.25} darkness={0.5} />
|
||||
</EffectComposer>
|
||||
)}
|
||||
</Canvas>
|
||||
|
||||
@@ -5,7 +5,6 @@ import dynamic from "next/dynamic";
|
||||
import { motion, AnimatePresence, useMotionValue, useTransform } from "framer-motion";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import Lenis from "lenis";
|
||||
import {
|
||||
COLORS,
|
||||
PHASE_LABELS,
|
||||
@@ -130,7 +129,7 @@ export default function OptimizationSection() {
|
||||
mountIo.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "120% 0px" },
|
||||
{ rootMargin: "70% 0px" },
|
||||
);
|
||||
const activeIo = new IntersectionObserver(
|
||||
(entries) => setSceneActive(entries.some((e) => e.isIntersecting)),
|
||||
@@ -155,23 +154,11 @@ export default function OptimizationSection() {
|
||||
if (!el) return;
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Smooth scroll (Lenis), driven by a SINGLE rAF source — GSAP's ticker.
|
||||
// Previously lenis.raf() was called from both a manual requestAnimationFrame
|
||||
// loop AND gsap.ticker, double-stepping the integrator every frame, which is
|
||||
// what made scrolling stutter. One source keeps Lenis + ScrollTrigger locked.
|
||||
const lenis = new Lenis({
|
||||
duration: 1.05,
|
||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||
orientation: "vertical",
|
||||
gestureOrientation: "vertical",
|
||||
smoothWheel: true,
|
||||
});
|
||||
|
||||
lenis.on("scroll", ScrollTrigger.update);
|
||||
const tickerCb = (time: number) => lenis.raf(time * 1000); // ticker is in seconds, Lenis wants ms
|
||||
gsap.ticker.add(tickerCb);
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
|
||||
// NOTE: /miletruth runs on native scroll (no Lenis). Smooth-scrolling this
|
||||
// page fought the three stacked pinned WebGL sections and caused scroll lag;
|
||||
// global Lenis (src/animations/SmoothScroll.tsx) is intentionally disabled on
|
||||
// this route. ScrollTrigger's scrub + self-managed fixed pin work as-is on
|
||||
// native scroll.
|
||||
let lastPhase: PhaseKey = "chaos";
|
||||
let lastPin: "before" | "pinned" | "after" = "before";
|
||||
const st = ScrollTrigger.create({
|
||||
@@ -201,8 +188,6 @@ export default function OptimizationSection() {
|
||||
return () => {
|
||||
clearTimeout(refresh);
|
||||
st.kill();
|
||||
gsap.ticker.remove(tickerCb);
|
||||
lenis.destroy();
|
||||
};
|
||||
}, [scroll]);
|
||||
|
||||
@@ -211,7 +196,8 @@ export default function OptimizationSection() {
|
||||
const leftBlur = useTransform(scroll, [0.3, 0.55], [0, 3]);
|
||||
const leftFilter = useTransform(leftBlur, (b) => `blur(${b}px)`);
|
||||
const rightOpacity = useTransform(scroll, [0.42, 0.66], [0.36, 1]);
|
||||
const scanWidth = useTransform(scroll, [0, 1], ["0%", "100%"]);
|
||||
// GPU transform (scaleX) instead of animating width, which forces layout each frame.
|
||||
const scanScaleX = useTransform(scroll, [0, 1], [0, 1]);
|
||||
const scanLineY = useTransform(scroll, [0.2, 0.42], ["8%", "92%"]);
|
||||
const scanLineOpacity = useTransform(scroll, [0.18, 0.22, 0.42, 0.46], [0, 1, 1, 0]);
|
||||
const dividerOpacity = useTransform(scroll, [0.45, 0.6], [0.15, 0.75]);
|
||||
@@ -350,7 +336,7 @@ export default function OptimizationSection() {
|
||||
{/* Scan progress bar */}
|
||||
<div className="dm-opt-progress">
|
||||
<div className="dm-opt-progress__track">
|
||||
<motion.div className="dm-opt-progress__fill" style={{ width: scanWidth }} />
|
||||
<motion.div className="dm-opt-progress__fill" style={{ scaleX: scanScaleX }} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -470,17 +456,20 @@ const styles = `
|
||||
}
|
||||
/* Animated subtle grid pattern */
|
||||
.dm-opt-card::before {
|
||||
content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
|
||||
/* Expanded one tile beyond the (clipped) card so we can drift it via a GPU
|
||||
transform instead of animating background-position (which repaints each frame). */
|
||||
content: ""; position: absolute; inset: -60px; z-index: 0; pointer-events: none;
|
||||
opacity: 0.035;
|
||||
background-image:
|
||||
linear-gradient(${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px),
|
||||
linear-gradient(90deg, ${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
animation: dmOptGridDrift 25s linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
@keyframes dmOptGridDrift {
|
||||
0% { background-position: 0 0; }
|
||||
100% { background-position: 60px 60px; }
|
||||
0% { transform: translate3d(0, 0, 0); }
|
||||
100% { transform: translate3d(60px, 60px, 0); }
|
||||
}
|
||||
/* Radial center glow behind 3D scene */
|
||||
.dm-opt-card::after {
|
||||
@@ -669,7 +658,7 @@ const styles = `
|
||||
background: ${rgba(COLORS.cyan, 0.10)}; max-width: 420px; margin: 0 auto;
|
||||
}
|
||||
.dm-opt-progress__fill {
|
||||
height: 100%; border-radius: 999px;
|
||||
height: 100%; width: 100%; transform-origin: left center; border-radius: 999px;
|
||||
background: linear-gradient(90deg, ${COLORS.cyan}, ${COLORS.green});
|
||||
box-shadow: 0 0 12px ${rgba(COLORS.cyan, 0.6)};
|
||||
}
|
||||
@@ -697,9 +686,10 @@ const styles = `
|
||||
.dm-opt-panel {
|
||||
pointer-events: auto; width: clamp(230px, 26vw, 340px);
|
||||
padding: 18px 20px; border-radius: 20px;
|
||||
background: ${rgba(COLORS.ink, 0.72)};
|
||||
background: ${rgba(COLORS.ink, 0.92)};
|
||||
border: 1px solid ${rgba(COLORS.slate, 0.22)};
|
||||
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
||||
/* backdrop blur removed — panel opacity is scroll-driven, so the blur was
|
||||
recomputed every scroll frame; a near-opaque fill reads the same and is free. */
|
||||
}
|
||||
.dm-opt-panel--bad {
|
||||
border-color: ${rgba(COLORS.red, 0.45)};
|
||||
@@ -772,9 +762,9 @@ const styles = `
|
||||
}
|
||||
.dm-opt-metric {
|
||||
position: relative; padding: 14px 14px 12px; border-radius: 16px;
|
||||
background: ${rgba(COLORS.ink, 0.72)};
|
||||
background: ${rgba(COLORS.ink, 0.92)};
|
||||
border: 1px solid ${rgba(COLORS.slate, 0.25)};
|
||||
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
||||
/* backdrop blur removed — 5 metric cards animate on scroll; per-frame blur recompute was a hotspot. */
|
||||
overflow: hidden;
|
||||
opacity: 0; transform: translateY(22px);
|
||||
animation: dmOptCardIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
|
||||
58
src/components/optimization/constants.test.ts
Normal file
58
src/components/optimization/constants.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { rgba, phaseFromProgress, PHASES, PHASE_LABELS, type PhaseKey } from "./constants";
|
||||
|
||||
describe("optimization/constants — rgba()", () => {
|
||||
it("converts a hex string to an rgba() string", () => {
|
||||
expect(rgba("#22C55E", 0.5)).toBe("rgba(34, 197, 94, 0.5)");
|
||||
expect(rgba("#000000", 1)).toBe("rgba(0, 0, 0, 1)");
|
||||
expect(rgba("#FFFFFF", 0)).toBe("rgba(255, 255, 255, 0)");
|
||||
});
|
||||
|
||||
it("is case-insensitive for hex digits", () => {
|
||||
expect(rgba("#ffffff", 1)).toBe(rgba("#FFFFFF", 1));
|
||||
});
|
||||
});
|
||||
|
||||
describe("optimization/constants — phaseFromProgress()", () => {
|
||||
it("returns the right phase at each threshold", () => {
|
||||
expect(phaseFromProgress(PHASES.chaos)).toBe("chaos");
|
||||
expect(phaseFromProgress(PHASES.scan)).toBe("scan");
|
||||
expect(phaseFromProgress(PHASES.dissolve)).toBe("dissolve");
|
||||
expect(phaseFromProgress(PHASES.optimize)).toBe("optimize");
|
||||
expect(phaseFromProgress(PHASES.reorganize)).toBe("reorganize");
|
||||
expect(phaseFromProgress(PHASES.metrics)).toBe("metrics");
|
||||
});
|
||||
|
||||
it("returns the previous phase just below a threshold", () => {
|
||||
expect(phaseFromProgress(PHASES.scan - 0.001)).toBe("chaos");
|
||||
expect(phaseFromProgress(PHASES.dissolve - 0.001)).toBe("scan");
|
||||
expect(phaseFromProgress(PHASES.metrics - 0.001)).toBe("reorganize");
|
||||
});
|
||||
|
||||
it("handles the 0 and 1 extremes", () => {
|
||||
expect(phaseFromProgress(0)).toBe("chaos");
|
||||
expect(phaseFromProgress(1)).toBe("metrics");
|
||||
});
|
||||
|
||||
it("only ever returns a defined phase key across a sweep", () => {
|
||||
const keys = Object.keys(PHASES) as PhaseKey[];
|
||||
for (let p = 0; p <= 1.0001; p += 0.02) {
|
||||
expect(keys).toContain(phaseFromProgress(p));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("optimization/constants — phase tables", () => {
|
||||
it("has strictly increasing phase thresholds", () => {
|
||||
const vals = Object.values(PHASES);
|
||||
for (let i = 1; i < vals.length; i++) {
|
||||
expect(vals[i]).toBeGreaterThan(vals[i - 1]);
|
||||
}
|
||||
});
|
||||
|
||||
it("has a label for every phase key", () => {
|
||||
for (const key of Object.keys(PHASES) as PhaseKey[]) {
|
||||
expect(typeof PHASE_LABELS[key]).toBe("string");
|
||||
expect(PHASE_LABELS[key].length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
99
src/components/optimization/math.test.ts
Normal file
99
src/components/optimization/math.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { clamp01, lerp, remap, smoothstep, damp, seeded } from "./math";
|
||||
|
||||
describe("optimization/math — clamp01()", () => {
|
||||
it("clamps to [0,1]", () => {
|
||||
expect(clamp01(-2)).toBe(0);
|
||||
expect(clamp01(0)).toBe(0);
|
||||
expect(clamp01(0.37)).toBe(0.37);
|
||||
expect(clamp01(1)).toBe(1);
|
||||
expect(clamp01(5)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("optimization/math — lerp()", () => {
|
||||
it("interpolates linearly", () => {
|
||||
expect(lerp(0, 10, 0)).toBe(0);
|
||||
expect(lerp(0, 10, 1)).toBe(10);
|
||||
expect(lerp(0, 10, 0.5)).toBe(5);
|
||||
expect(lerp(2, 4, 0.25)).toBe(2.5);
|
||||
});
|
||||
|
||||
it("extrapolates past the endpoints", () => {
|
||||
expect(lerp(0, 10, 2)).toBe(20);
|
||||
expect(lerp(0, 10, -1)).toBe(-10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("optimization/math — remap()", () => {
|
||||
it("remaps and clamps to the output range", () => {
|
||||
expect(remap(5, 0, 10, 0, 100)).toBe(50);
|
||||
expect(remap(-5, 0, 10, 0, 100)).toBe(0);
|
||||
expect(remap(15, 0, 10, 0, 100)).toBe(100);
|
||||
});
|
||||
|
||||
it("defaults the output range to [0,1]", () => {
|
||||
expect(remap(2.5, 0, 10)).toBeCloseTo(0.25, 10);
|
||||
});
|
||||
|
||||
it("guards against a zero-width input range (no NaN)", () => {
|
||||
const v = remap(5, 2, 2, 0, 1);
|
||||
expect(Number.isNaN(v)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("optimization/math — smoothstep()", () => {
|
||||
it("hits 0 and 1 at the edges and 0.5 at the middle", () => {
|
||||
expect(smoothstep(0, 1, 0)).toBe(0);
|
||||
expect(smoothstep(0, 1, 1)).toBe(1);
|
||||
expect(smoothstep(0, 1, 0.5)).toBeCloseTo(0.5, 10);
|
||||
});
|
||||
|
||||
it("clamps outside the edges", () => {
|
||||
expect(smoothstep(0, 1, -1)).toBe(0);
|
||||
expect(smoothstep(0, 1, 2)).toBe(1);
|
||||
});
|
||||
|
||||
it("is monotonically non-decreasing", () => {
|
||||
let prev = -Infinity;
|
||||
for (let x = -0.2; x <= 1.2; x += 0.1) {
|
||||
const v = smoothstep(0, 1, x);
|
||||
expect(v).toBeGreaterThanOrEqual(prev);
|
||||
prev = v;
|
||||
}
|
||||
});
|
||||
|
||||
it("guards against equal edges (no NaN)", () => {
|
||||
expect(Number.isNaN(smoothstep(1, 1, 1))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("optimization/math — damp()", () => {
|
||||
it("does not move when dt is 0", () => {
|
||||
expect(damp(3, 10, 5, 0)).toBe(3);
|
||||
});
|
||||
|
||||
it("moves toward the target and converges with large lambda*dt", () => {
|
||||
const once = damp(0, 10, 1, 0.5);
|
||||
expect(once).toBeGreaterThan(0);
|
||||
expect(once).toBeLessThan(10);
|
||||
expect(damp(0, 10, 50, 1)).toBeCloseTo(10, 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("optimization/math — seeded()", () => {
|
||||
it("is deterministic for a given input", () => {
|
||||
expect(seeded(7)).toBe(seeded(7));
|
||||
});
|
||||
|
||||
it("returns values within [0,1)", () => {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const v = seeded(i);
|
||||
expect(v).toBeGreaterThanOrEqual(0);
|
||||
expect(v).toBeLessThan(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("produces different values for different seeds", () => {
|
||||
expect(seeded(1)).not.toBe(seeded(2));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user