update code and styles
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -154,7 +154,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
<div className="dm-st-card">
|
||||
{mountScene && (
|
||||
<div className="dm-st-canvas">
|
||||
<StrategyCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive} />
|
||||
<StrategyCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive} stage={active} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -171,127 +171,61 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
<span className="dm-st-arrow">↓</span>
|
||||
</motion.div>
|
||||
|
||||
{/* STAGE 01 — INPUT (green): orders + riders enter the system */}
|
||||
{/* Side cards are now light text anchors — the 3D world carries the
|
||||
detail. Each: short title + one lead line + a couple of key chips. */}
|
||||
|
||||
{/* STAGE 01 — INPUT (green) */}
|
||||
<StageCard i={0} scroll={scroll} side="left">
|
||||
<h3 className="dm-st-pillar__title">Orders & riders enter the system</h3>
|
||||
<div className="dm-st-grid2">
|
||||
<div className="dm-st-tile">
|
||||
<span className="dm-st-tile__ico">📄</span>
|
||||
<span className="dm-st-tile__num">59</span>
|
||||
<span className="dm-st-tile__lbl">Orders</span>
|
||||
<span className="dm-st-tile__sub">Order intake</span>
|
||||
</div>
|
||||
<div className="dm-st-tile">
|
||||
<span className="dm-st-tile__ico">🧑✈️</span>
|
||||
<span className="dm-st-tile__num">4</span>
|
||||
<span className="dm-st-tile__lbl">Total Riders</span>
|
||||
<span className="dm-st-tile__sub">Available fleet</span>
|
||||
</div>
|
||||
<p className="dm-st-anchor__lead">Orders are uploaded and matched against the available fleet, ready for assignment.</p>
|
||||
<div className="dm-st-anchor__chips">
|
||||
<span className="dm-st-anchor__chip">59 Orders</span>
|
||||
<span className="dm-st-anchor__chip">4 Riders</span>
|
||||
<span className="dm-st-anchor__chip">Fleet ready</span>
|
||||
</div>
|
||||
<div className="dm-st-file">
|
||||
<span className="dm-st-file__ico">⬆️</span>
|
||||
<span className="dm-st-file__name">orders.csv</span>
|
||||
<span className="dm-st-file__meta">59 rows · uploaded</span>
|
||||
<span className="dm-st-file__ok">✓</span>
|
||||
</div>
|
||||
<ul className="dm-st-riders">
|
||||
{[
|
||||
{ a: "A", name: "Rider A", v: "EV Bike", cap: "12 orders" },
|
||||
{ a: "B", name: "Rider B", v: "Auto", cap: "18 orders" },
|
||||
{ a: "C", name: "Rider C", v: "Cargo Truck", cap: "20 orders" },
|
||||
{ a: "D", name: "Rider D", v: "EV Van", cap: "9 orders" },
|
||||
].map((r) => (
|
||||
<li key={r.a} className="dm-st-rider">
|
||||
<span className="dm-st-rider__av">{r.a}<i /></span>
|
||||
<span className="dm-st-rider__name">{r.name}</span>
|
||||
<span className="dm-st-rider__veh">{r.v}</span>
|
||||
<span className="dm-st-rider__cap">{r.cap}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</StageCard>
|
||||
|
||||
{/* STAGE 02 — PARALLEL EXECUTION (purple): 6 strategies at once */}
|
||||
{/* STAGE 02 — PARALLEL EXECUTION (purple) */}
|
||||
<StageCard i={1} scroll={scroll} side="right">
|
||||
<h3 className="dm-st-pillar__title">Six strategies, evaluated in parallel</h3>
|
||||
<div className="dm-st-engines">
|
||||
<div className="dm-st-engine">
|
||||
<div className="dm-st-engine__head"><span className="dm-st-engine__name">Legacy Engine</span><span className="dm-st-engine__tag">Baseline</span></div>
|
||||
<div className="dm-st-pills">
|
||||
{["Proximity", "Balanced", "Fuel Saver"].map((p) => <span key={p} className="dm-st-pill">{p}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="dm-st-engine is-unified">
|
||||
<div className="dm-st-engine__head"><span className="dm-st-engine__name">Unified Engine</span><span className="dm-st-engine__tag dm-st-engine__tag--u">MileTruth</span></div>
|
||||
<div className="dm-st-pills">
|
||||
{["EV Aware", "Multi Trip", "Time Aware"].map((p) => <span key={p} className="dm-st-pill dm-st-pill--u">{p}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="dm-st-anchor__lead">The AI runs every routing strategy at the same time — legacy baselines and MileTruth's unified engine.</p>
|
||||
<div className="dm-st-anchor__chips">
|
||||
<span className="dm-st-anchor__chip">EV Aware</span>
|
||||
<span className="dm-st-anchor__chip">Multi Trip</span>
|
||||
<span className="dm-st-anchor__chip">+4 more</span>
|
||||
</div>
|
||||
<p className="dm-st-foot"><span className="dm-st-livedot" /> All 6 strategies run at the same time</p>
|
||||
</StageCard>
|
||||
|
||||
{/* STAGE 03 — SMART OPTIMIZATION (blue): validation pipeline */}
|
||||
{/* STAGE 03 — SMART OPTIMIZATION (blue) */}
|
||||
<StageCard i={2} scroll={scroll} side="left">
|
||||
<h3 className="dm-st-pillar__title">Routes validated & optimized</h3>
|
||||
<div className="dm-st-pipe">
|
||||
{[
|
||||
{ ico: "⚙️", name: "VRP Optimizer", desc: "Google OR-Tools solver" },
|
||||
{ ico: "🔋", name: "Battery Simulation", desc: "EV range & charging feasibility" },
|
||||
{ ico: "⏱️", name: "SLA Validator", desc: "ETA vs promised window" },
|
||||
].map((m, j) => (
|
||||
<React.Fragment key={m.name}>
|
||||
{j > 0 && <span className="dm-st-pipe__arrow"><i /></span>}
|
||||
<div className="dm-st-pipe__node">
|
||||
<span className="dm-st-pipe__ico">{m.ico}</span>
|
||||
<span className="dm-st-pipe__name">{m.name}</span>
|
||||
<span className="dm-st-pipe__desc">{m.desc}</span>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<h3 className="dm-st-pillar__title">Routes optimized & validated</h3>
|
||||
<p className="dm-st-anchor__lead">Every route is solved for distance, then checked against battery range and delivery SLAs.</p>
|
||||
<div className="dm-st-anchor__chips">
|
||||
<span className="dm-st-anchor__chip">Optimize</span>
|
||||
<span className="dm-st-anchor__chip">Battery</span>
|
||||
<span className="dm-st-anchor__chip">SLA</span>
|
||||
</div>
|
||||
</StageCard>
|
||||
|
||||
{/* STAGE 04 — PERFORMANCE GRADING (orange): every strategy scored */}
|
||||
{/* STAGE 04 — PERFORMANCE GRADING (orange) */}
|
||||
<StageCard i={3} scroll={scroll} side="right">
|
||||
<h3 className="dm-st-pillar__title">Every strategy is scored</h3>
|
||||
<div className="dm-st-stars"><span className="dm-st-stars__on">★★★★</span><span className="dm-st-stars__off">★</span><span className="dm-st-stars__txt">4.5 / 5 grade</span></div>
|
||||
<ul className="dm-st-metrics">
|
||||
{[
|
||||
{ name: "Fulfillment Rate", score: "88%", w: 88, status: "Good", desc: "Orders delivered vs total" },
|
||||
{ name: "SLA Compliance", score: "95%", w: 95, status: "Pass", desc: "On-time within window" },
|
||||
{ name: "Efficiency Score", score: "92", w: 92, status: "Strong", desc: "Distance & fleet usage" },
|
||||
{ name: "Battery & Route", score: "OK", w: 100, status: "Feasible", desc: "EV range respected" },
|
||||
].map((m) => (
|
||||
<li key={m.name} className="dm-st-metric">
|
||||
<span className="dm-st-metric__name">{m.name}</span>
|
||||
<span className="dm-st-metric__bar"><i style={{ width: `${m.w}%` }} /></span>
|
||||
<span className="dm-st-metric__score">{m.score}</span>
|
||||
<span className="dm-st-metric__status">✓ {m.status}</span>
|
||||
<span className="dm-st-metric__desc">{m.desc}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="dm-st-anchor__lead">Each strategy is graded live on fulfillment, SLA compliance, efficiency and battery feasibility.</p>
|
||||
<div className="dm-st-anchor__chips">
|
||||
<span className="dm-st-anchor__chip">Grade A</span>
|
||||
<span className="dm-st-anchor__chip">88% Fulfillment</span>
|
||||
<span className="dm-st-anchor__chip">95% SLA</span>
|
||||
</div>
|
||||
</StageCard>
|
||||
|
||||
{/* STAGE 05 — STRATEGY COMPARISON (red, hero): the winner */}
|
||||
{/* STAGE 05 — STRATEGY COMPARISON (red, hero) */}
|
||||
<StageCard i={4} scroll={scroll} side="right">
|
||||
<div className="dm-st-winner">
|
||||
<span className="dm-st-winner__eyebrow">Best strategy recommendation</span>
|
||||
<div className="dm-st-winner__name"><span className="dm-st-trophy">🏆</span> EV Aware</div>
|
||||
<span className="dm-st-winner__grade">High Performance Grade</span>
|
||||
</div>
|
||||
<div className="dm-st-hero">
|
||||
<div className="dm-st-hero__ring">
|
||||
<span className="dm-st-hero__pct">88%</span>
|
||||
<span className="dm-st-hero__sub">Score</span>
|
||||
</div>
|
||||
<ul className="dm-st-wins">
|
||||
<li><strong>52/59</strong> Orders Fulfilled</li>
|
||||
<li><strong>88%</strong> Performance Score</li>
|
||||
<li><strong>3</strong> SLA Violations</li>
|
||||
<li><strong>A</strong> Performance Grade</li>
|
||||
</ul>
|
||||
<h3 className="dm-st-pillar__title">Happier riders. Higher fulfillment.</h3>
|
||||
<p className="dm-st-anchor__lead">EV Aware wins — the best fulfillment with feasible, battery-safe routes for every rider.</p>
|
||||
<div className="dm-st-anchor__chips">
|
||||
<span className="dm-st-anchor__chip dm-st-anchor__chip--win">🏆 EV Aware</span>
|
||||
<span className="dm-st-anchor__chip">88% Score</span>
|
||||
<span className="dm-st-anchor__chip">52/59 Fulfilled</span>
|
||||
</div>
|
||||
</StageCard>
|
||||
</div>
|
||||
@@ -303,13 +237,13 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
}
|
||||
|
||||
const styles = `
|
||||
.dm-st { position: relative; height: 620vh; background: transparent; }
|
||||
.dm-st { position: relative; height: 720vh; background: transparent; }
|
||||
.dm-st-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden; }
|
||||
.dm-st.is-pinned .dm-st-sticky { position: fixed; top: 0; left: 0; }
|
||||
.dm-st.is-after .dm-st-sticky { position: absolute; top: auto; bottom: 0; }
|
||||
|
||||
.dm-st-card {
|
||||
position: absolute !important; inset: 16px !important;
|
||||
position: absolute !important; inset: 20px !important;
|
||||
border-radius: 28px !important; overflow: hidden !important;
|
||||
background: radial-gradient(120% 100% at 50% 0%, #ffffff 0%, #eef1f6 60%, #e6eaf2 100%) !important;
|
||||
border: 1px solid rgba(15,23,42,0.08) !important;
|
||||
@@ -321,7 +255,7 @@ const styles = `
|
||||
/* Connected mode (inside Workflow 3): flatten the card's bottom so the Strategy
|
||||
content card below butts directly against it — same seam as Workflow 1 & 2. */
|
||||
.dm-st.is-connected .dm-st-card {
|
||||
top: 16px !important; left: 16px !important; right: 16px !important; bottom: 0 !important;
|
||||
top: 20px !important; left: 20px !important; right: 20px !important; bottom: 0 !important;
|
||||
border-radius: 28px 28px 0 0 !important; border-bottom: none !important;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
@@ -414,90 +348,48 @@ const styles = `
|
||||
.dm-st3d-chip__txt b { font-size: 12.5px; font-weight: 800; color: #0f172a; }
|
||||
.dm-st3d-chip__txt { font-size: 10.5px; color: #475569; }
|
||||
|
||||
/* STAGE 01 — Input */
|
||||
.dm-st-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
|
||||
.dm-st-tile { position: relative; background: color-mix(in srgb, var(--c) 7%, white); border: 1px solid color-mix(in srgb, var(--c) 22%, white);
|
||||
border-radius: 14px; padding: 12px 14px; display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 10px; align-items: center; }
|
||||
.dm-st-tile__ico { grid-row: 1 / 3; font-size: 24px; }
|
||||
.dm-st-tile__num { font-size: 26px; font-weight: 800; color: #0f172a; line-height: 1; }
|
||||
.dm-st-tile__lbl { font-size: 12px; font-weight: 700; color: #334155; }
|
||||
.dm-st-tile__sub { grid-column: 1 / 3; margin-top: 4px; font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; color: #64748b; }
|
||||
.dm-st-file { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; padding: 9px 12px; border-radius: 12px;
|
||||
background: rgba(15,23,42,0.04); border: 1px dashed rgba(15,23,42,0.18); }
|
||||
.dm-st-file__ico { font-size: 15px; }
|
||||
.dm-st-file__name { font-size: 13px; font-weight: 700; color: #0f172a; }
|
||||
.dm-st-file__meta { font-size: 11.5px; color: #64748b; }
|
||||
.dm-st-file__ok { margin-left: auto; font-size: 12px; font-weight: 800; color: #22C55E; }
|
||||
.dm-st-riders { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
|
||||
.dm-st-rider { display: grid; grid-template-columns: 26px 1fr auto auto; align-items: center; gap: 9px;
|
||||
padding: 6px 8px; border-radius: 10px; background: rgba(15,23,42,0.03); }
|
||||
.dm-st-rider__av { position: relative; width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 11px; font-weight: 800; color: #fff; background: linear-gradient(135deg, var(--c), color-mix(in srgb, var(--c) 60%, #1e293b)); }
|
||||
.dm-st-rider__av i { position: absolute; right: -1px; bottom: -1px; width: 8px; height: 8px; border-radius: 50%; background: #22C55E; border: 2px solid #fff; }
|
||||
.dm-st-rider__name { font-size: 12.5px; font-weight: 700; color: #0f172a; }
|
||||
.dm-st-rider__veh { font-size: 11px; font-weight: 600; color: #475569; padding: 2px 8px; border-radius: 999px; background: rgba(15,23,42,0.06); }
|
||||
.dm-st-rider__cap { font-size: 11px; color: #64748b; white-space: nowrap; }
|
||||
/* Generic themed 3D chips (stages 02–05) — colour comes from --tc per element */
|
||||
.dm-st3d-tag, .dm-st3d-score {
|
||||
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
|
||||
pointer-events: none; user-select: none; white-space: nowrap; transition: opacity 0.2s linear; }
|
||||
.dm-st3d-tag { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 700; color: #0f172a;
|
||||
background: rgba(255,255,255,0.9); border: 1px solid color-mix(in srgb, var(--tc, #8B5CF6) 55%, white);
|
||||
border-radius: 999px; padding: 5px 11px; box-shadow: 0 8px 20px -12px var(--tc, #8B5CF6); backdrop-filter: blur(8px); }
|
||||
.dm-st3d-tag b { font-weight: 800; color: var(--tc, #0f172a); }
|
||||
.dm-st3d-tag.is-u { background: color-mix(in srgb, var(--tc) 14%, white); border-color: var(--tc); }
|
||||
.dm-st3d-tag.is-muted { opacity: 0.82; border-style: dashed; }
|
||||
.dm-st3d-tag.is-win { border-color: var(--tc); box-shadow: 0 10px 26px -10px var(--tc); }
|
||||
.dm-st3d-score { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700; color: #0f172a;
|
||||
background: rgba(255,255,255,0.92); border: 1px solid color-mix(in srgb, var(--tc, #0f172a) 45%, white);
|
||||
border-radius: 12px; padding: 6px 13px; box-shadow: 0 10px 26px -12px var(--tc, #0f172a); backdrop-filter: blur(8px); }
|
||||
.dm-st3d-score b { font-size: 16px; font-weight: 800; color: var(--tc, #0f172a); }
|
||||
.dm-st3d-score.is-win { border-color: var(--tc); }
|
||||
|
||||
/* STAGE 02 — Parallel execution */
|
||||
.dm-st-engines { display: grid; gap: 10px; }
|
||||
.dm-st-engine { padding: 11px 12px; border-radius: 14px; background: rgba(15,23,42,0.03); border: 1px solid rgba(15,23,42,0.07); }
|
||||
.dm-st-engine.is-unified { background: color-mix(in srgb, var(--c) 8%, white); border-color: color-mix(in srgb, var(--c) 28%, white); }
|
||||
.dm-st-engine__head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.dm-st-engine__name { font-size: 13px; font-weight: 800; color: #0f172a; }
|
||||
.dm-st-engine__tag { font-size: 9.5px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: #64748b;
|
||||
background: rgba(15,23,42,0.07); padding: 2px 7px; border-radius: 6px; }
|
||||
.dm-st-engine__tag--u { color: #fff; background: var(--c); }
|
||||
.dm-st-pills { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
.dm-st-pill { font-size: 12px; font-weight: 700; color: #334155; padding: 6px 12px; border-radius: 999px;
|
||||
background: rgba(255,255,255,0.9); border: 1px solid rgba(15,23,42,0.12); }
|
||||
.dm-st-pill--u { color: #0f172a; background: color-mix(in srgb, var(--c) 14%, white); border-color: color-mix(in srgb, var(--c) 40%, white); }
|
||||
/* Light side-card anchor — the 3D world now carries the detail */
|
||||
.dm-st-anchor__lead { margin: 0 0 14px; font-size: clamp(13px, 1.2vw, 15px); line-height: 1.55; color: #475569; }
|
||||
.dm-st-anchor__chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.dm-st-anchor__chip { font-size: 12px; font-weight: 700; color: #334155; padding: 6px 12px; border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--c) 9%, white); border: 1px solid color-mix(in srgb, var(--c) 30%, white); }
|
||||
.dm-st-anchor__chip--win { color: #fff; background: linear-gradient(90deg, #C01227, #E2354A); border-color: transparent; }
|
||||
|
||||
/* STAGE 03 — Optimization pipeline */
|
||||
.dm-st-pipe { display: grid; gap: 0; }
|
||||
.dm-st-pipe__node { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 11px; align-items: center;
|
||||
padding: 11px 14px; border-radius: 14px; background: rgba(255,255,255,0.92);
|
||||
border: 1px solid color-mix(in srgb, var(--c) 30%, white); box-shadow: 0 8px 22px -14px var(--c); }
|
||||
.dm-st-pipe__ico { grid-row: 1 / 3; font-size: 22px; }
|
||||
.dm-st-pipe__name { font-size: 13.5px; font-weight: 800; color: #0f172a; }
|
||||
.dm-st-pipe__desc { font-size: 11.5px; color: #64748b; }
|
||||
.dm-st-pipe__arrow { display: flex; align-items: center; justify-content: center; height: 22px; }
|
||||
.dm-st-pipe__arrow i { position: relative; width: 2px; height: 22px; background: linear-gradient(180deg, color-mix(in srgb, var(--c) 50%, transparent), var(--c)); overflow: visible; }
|
||||
.dm-st-pipe__arrow i::after { content: ""; position: absolute; left: 50%; top: -4px; width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--c); box-shadow: 0 0 8px var(--c); transform: translateX(-50%); animation: dmStFlow 1.4s linear infinite; }
|
||||
@keyframes dmStFlow { 0% { top: -4px; opacity: 0; } 20% { opacity: 1; } 100% { top: 22px; opacity: 0; } }
|
||||
|
||||
/* STAGE 04 — Performance grading */
|
||||
.dm-st-stars { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; }
|
||||
.dm-st-stars__on { color: var(--c); letter-spacing: 2px; font-size: 16px; }
|
||||
.dm-st-stars__off { color: rgba(15,23,42,0.18); font-size: 16px; }
|
||||
.dm-st-stars__txt { font-size: 12px; font-weight: 700; color: #475569; margin-left: 4px; }
|
||||
.dm-st-metrics { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
|
||||
.dm-st-metric { display: grid; grid-template-columns: 1fr 70px auto; grid-template-rows: auto auto; column-gap: 10px; row-gap: 3px; align-items: center; }
|
||||
.dm-st-metric__name { font-size: 12.5px; font-weight: 700; color: #0f172a; }
|
||||
.dm-st-metric__bar { grid-column: 1 / 2; grid-row: 2; height: 6px; border-radius: 999px; background: rgba(15,23,42,0.08); overflow: hidden; }
|
||||
.dm-st-metric__bar i { display: block; height: 100%; border-radius: 999px; background: var(--c); }
|
||||
.dm-st-metric__score { grid-column: 2; grid-row: 1 / 3; font-size: 17px; font-weight: 800; color: #0f172a; text-align: right; }
|
||||
.dm-st-metric__status { grid-column: 3; grid-row: 1 / 3; font-size: 11px; font-weight: 800; color: #16a34a;
|
||||
background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.3); padding: 3px 8px; border-radius: 999px; white-space: nowrap; }
|
||||
.dm-st-metric__desc { grid-column: 1 / 2; grid-row: 1; font-size: 10.5px; color: #64748b; align-self: end; display: none; }
|
||||
|
||||
/* STAGE 05 — Strategy comparison (hero) */
|
||||
.dm-st-winner { margin-bottom: 14px; }
|
||||
.dm-st-winner__eyebrow { display: block; font-size: 10.5px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; color: #64748b; }
|
||||
.dm-st-winner__name { display: flex; align-items: center; gap: 10px; margin: 4px 0; font-size: clamp(24px, 2.8vw, 34px); font-weight: 800; color: #0f172a; letter-spacing: -0.02em; }
|
||||
.dm-st-winner__grade { display: inline-block; font-size: 11px; font-weight: 800; letter-spacing: 0.04em; color: #fff;
|
||||
background: linear-gradient(90deg, #C01227, #E2354A); padding: 4px 11px; border-radius: 999px; }
|
||||
.dm-st-trophy { font-size: 32px; filter: drop-shadow(0 8px 18px rgba(192,18,39,0.4)); animation: dmStFloat 3s ease-in-out infinite; }
|
||||
@keyframes dmStFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
|
||||
.dm-st-hero { display: flex; align-items: center; gap: 20px; }
|
||||
.dm-st-hero__ring { position: relative; flex-shrink: 0; width: 96px; height: 96px; border-radius: 50%; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; background: conic-gradient(#C01227 88%, rgba(15,23,42,0.1) 0); }
|
||||
.dm-st-hero__ring::after { content: ""; position: absolute; inset: 9px; border-radius: 50%; background: #fff; }
|
||||
.dm-st-hero__pct { position: relative; z-index: 1; font-size: 24px; font-weight: 800; color: #C01227; }
|
||||
.dm-st-hero__sub { position: relative; z-index: 1; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: #64748b; }
|
||||
.dm-st-wins { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
||||
.dm-st-wins li { font-size: 13px; color: #334155; display: flex; align-items: baseline; gap: 8px; }
|
||||
.dm-st-wins strong { color: #C01227; font-weight: 800; min-width: 48px; }
|
||||
/* In-world Command Center KPI card + Winner card (drei <Html>, faded by proximity) */
|
||||
.dm-st3d-kpi, .dm-st3d-winner3d {
|
||||
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
|
||||
pointer-events: none; user-select: none; transition: opacity 0.2s linear; }
|
||||
.dm-st3d-kpi { display: flex; flex-direction: column; gap: 5px; width: 132px; padding: 9px 12px; border-radius: 12px;
|
||||
background: rgba(255,255,255,0.95); border: 1px solid color-mix(in srgb, var(--tc, #F59E0B) 40%, white);
|
||||
box-shadow: 0 10px 26px -14px var(--tc, #F59E0B); backdrop-filter: blur(8px); }
|
||||
.dm-st3d-kpi__n { font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: #64748b; }
|
||||
.dm-st3d-kpi__v { font-size: 24px; font-weight: 800; color: #0f172a; line-height: 1; }
|
||||
.dm-st3d-kpi__v i { font-size: 14px; font-weight: 700; color: var(--tc, #F59E0B); font-style: normal; margin-left: 1px; }
|
||||
.dm-st3d-kpi__bar { height: 6px; border-radius: 999px; background: rgba(15,23,42,0.08); overflow: hidden; }
|
||||
.dm-st3d-kpi__bar i { display: block; height: 100%; border-radius: 999px; background: var(--tc, #F59E0B); }
|
||||
.dm-st3d-winner3d { display: flex; flex-direction: column; gap: 3px; width: 184px; padding: 13px 15px; border-radius: 14px;
|
||||
background: rgba(255,255,255,0.96); border: 1px solid rgba(192,18,39,0.4); box-shadow: 0 16px 40px -16px rgba(192,18,39,0.6); backdrop-filter: blur(8px); }
|
||||
.dm-st3d-winner3d__top { font-size: 10.5px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: #C01227; }
|
||||
.dm-st3d-winner3d__name { font-size: 26px; font-weight: 800; color: #0f172a; letter-spacing: -0.02em; line-height: 1.05; margin-bottom: 4px; }
|
||||
.dm-st3d-winner3d__row { font-size: 12px; color: #475569; }
|
||||
.dm-st3d-winner3d__row b { color: #C01227; font-weight: 800; margin-right: 4px; }
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.dm-st-rail__title { display: none; }
|
||||
@@ -505,12 +397,11 @@ const styles = `
|
||||
.dm-st-rail__line { width: 9px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.dm-st { height: 560vh; }
|
||||
.dm-st { height: 640vh; }
|
||||
.dm-st-card-story { left: 0 !important; right: 0 !important; margin: 0 auto; width: calc(100% - 28px);
|
||||
bottom: clamp(18px, 4vh, 40px); padding: 15px 16px; }
|
||||
.dm-st-rider__cap { display: none; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dm-st-arrow, .dm-st-trophy { animation: none !important; }
|
||||
.dm-st-arrow { animation: none !important; }
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { STAGES, N, Z_SPACING, X_OFFSET, stagePosition, samplePath } from "./theme";
|
||||
import { STAGES, N, D_SPACING, X_SWAY, districtPosition, samplePath, cameraFor } from "./theme";
|
||||
|
||||
describe("strategy/theme — stage data", () => {
|
||||
it("exposes five stages and a matching N", () => {
|
||||
@@ -17,45 +17,51 @@ describe("strategy/theme — stage data", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("strategy/theme — stagePosition()", () => {
|
||||
it("alternates x sign by parity and steps z by -Z_SPACING", () => {
|
||||
expect(stagePosition(0)).toEqual([-X_OFFSET, -0.45, -0]);
|
||||
expect(stagePosition(1)).toEqual([X_OFFSET, 0.55, -Z_SPACING]);
|
||||
expect(stagePosition(2)).toEqual([-X_OFFSET, -0.45, -2 * Z_SPACING]);
|
||||
expect(stagePosition(3)).toEqual([X_OFFSET, 0.55, -3 * Z_SPACING]);
|
||||
describe("strategy/theme — districtPosition()", () => {
|
||||
it("sits on the floor (y=0), sways x by parity, steps z by -D_SPACING", () => {
|
||||
expect(districtPosition(0)).toEqual([-X_SWAY, 0, -0]);
|
||||
expect(districtPosition(1)).toEqual([X_SWAY, 0, -D_SPACING]);
|
||||
expect(districtPosition(2)).toEqual([-X_SWAY, 0, -2 * D_SPACING]);
|
||||
expect(districtPosition(3)).toEqual([X_SWAY, 0, -3 * D_SPACING]);
|
||||
});
|
||||
|
||||
it("places even stages left and odd stages right", () => {
|
||||
it("places even districts left and odd districts right", () => {
|
||||
for (let i = 0; i < N; i++) {
|
||||
const [x] = stagePosition(i);
|
||||
const [x] = districtPosition(i);
|
||||
if (i % 2 === 0) expect(x).toBeLessThan(0);
|
||||
else expect(x).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("marches monotonically into -Z", () => {
|
||||
for (let i = 1; i < N; i++) {
|
||||
expect(districtPosition(i)[2]).toBeLessThan(districtPosition(i - 1)[2]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("strategy/theme — samplePath()", () => {
|
||||
it("equals stagePosition() at integer indices", () => {
|
||||
it("equals districtPosition() at integer indices", () => {
|
||||
for (let i = 0; i < N; i++) {
|
||||
const sampled = samplePath(i);
|
||||
const exact = stagePosition(i);
|
||||
const exact = districtPosition(i);
|
||||
sampled.forEach((v, k) => expect(v).toBeCloseTo(exact[k], 10));
|
||||
}
|
||||
});
|
||||
|
||||
it("linearly interpolates between adjacent stages", () => {
|
||||
it("linearly interpolates between adjacent districts", () => {
|
||||
const [x, y, z] = samplePath(0.5);
|
||||
expect(x).toBeCloseTo(0, 10); // midpoint of -2.5 and +2.5
|
||||
expect(y).toBeCloseTo(0.05, 10); // midpoint of -0.45 and 0.55
|
||||
expect(z).toBeCloseTo(-Z_SPACING / 2, 10);
|
||||
expect(x).toBeCloseTo(0, 10); // midpoint of -X_SWAY and +X_SWAY
|
||||
expect(y).toBeCloseTo(0, 10);
|
||||
expect(z).toBeCloseTo(-D_SPACING / 2, 10);
|
||||
});
|
||||
|
||||
it("clamps indices below 0 to the first stage", () => {
|
||||
expect(samplePath(-3)).toEqual(stagePosition(0));
|
||||
it("clamps indices below 0 to the first district", () => {
|
||||
expect(samplePath(-3)).toEqual(districtPosition(0));
|
||||
});
|
||||
|
||||
it("clamps indices above N-1 to the last stage", () => {
|
||||
expect(samplePath(99)).toEqual(stagePosition(N - 1));
|
||||
it("clamps indices above N-1 to the last district", () => {
|
||||
expect(samplePath(99)).toEqual(districtPosition(N - 1));
|
||||
});
|
||||
|
||||
it("never produces NaN across a fine sweep", () => {
|
||||
@@ -65,3 +71,22 @@ describe("strategy/theme — samplePath()", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("strategy/theme — cameraFor()", () => {
|
||||
it("keeps the camera behind (+Z) and above (+Y) the active district", () => {
|
||||
for (let i = 0; i < N; i++) {
|
||||
const { pos, look } = cameraFor(i);
|
||||
const [, , dz] = districtPosition(i);
|
||||
expect(pos[2]).toBeGreaterThan(dz); // camera is back from the district
|
||||
expect(pos[1]).toBeGreaterThan(look[1]); // camera looks down
|
||||
expect(look[2]).toBeLessThan(pos[2]); // look target leads forward
|
||||
}
|
||||
});
|
||||
|
||||
it("never produces NaN across a fine sweep", () => {
|
||||
for (let p = 0; p <= 1.0001; p += 0.05) {
|
||||
const { pos, look } = cameraFor(p * (N - 1));
|
||||
for (const v of [...pos, ...look]) expect(Number.isNaN(v)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Shared data + geometry for the "Strategy" 3D scroll-storytelling section.
|
||||
// Five themed stages laid out along a zig-zag path in 3D. The camera travels
|
||||
// stage-to-stage as a normalized scroll progress (0→1) advances.
|
||||
// The five stages are five DISTRICTS of one continuous logistics world laid out
|
||||
// along the camera's forward (−Z) axis. As normalized scroll progress (0→1)
|
||||
// advances, the camera dollies forward through the world: Intake dock → Strategy
|
||||
// network → City route map → Command center → Winner podium.
|
||||
|
||||
export type StageTheme = {
|
||||
n: string; // "01"
|
||||
@@ -19,25 +21,43 @@ export const STAGES: StageTheme[] = [
|
||||
|
||||
export const N = STAGES.length;
|
||||
|
||||
// Zig-zag layout constants (world units).
|
||||
export const Z_SPACING = 6.4;
|
||||
export const X_OFFSET = 2.5;
|
||||
// Forward-journey layout (world units). Each district sits on the floor (y≈0) at
|
||||
// an increasing depth; a slight alternating x sway gives the dolly some rhythm
|
||||
// without ever turning the world into a zig-zag of separate panels.
|
||||
export const D_SPACING = 13; // depth between districts
|
||||
export const X_SWAY = 1.4; // gentle left/right sway between districts
|
||||
|
||||
/** Resting world position of stage `i` along the zig-zag path. */
|
||||
export function stagePosition(i: number): [number, number, number] {
|
||||
const x = (i % 2 === 0 ? -1 : 1) * X_OFFSET;
|
||||
const y = i % 2 === 0 ? -0.45 : 0.55;
|
||||
const z = -i * Z_SPACING;
|
||||
return [x, y, z];
|
||||
/** Resting world position (floor anchor) of district `i` along the journey. */
|
||||
export function districtPosition(i: number): [number, number, number] {
|
||||
const x = (i % 2 === 0 ? -1 : 1) * X_SWAY;
|
||||
const z = -i * D_SPACING;
|
||||
return [x, 0, z];
|
||||
}
|
||||
|
||||
/** Continuous position sampled at a fractional stage index (lerp between stages). */
|
||||
/** Continuous floor position sampled at a fractional district index. */
|
||||
export function samplePath(idx: number): [number, number, number] {
|
||||
const clamped = Math.max(0, Math.min(N - 1, idx));
|
||||
const f = Math.floor(clamped);
|
||||
const c = Math.min(N - 1, f + 1);
|
||||
const t = clamped - f;
|
||||
const a = stagePosition(f);
|
||||
const b = stagePosition(c);
|
||||
const a = districtPosition(f);
|
||||
const b = districtPosition(c);
|
||||
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
||||
}
|
||||
|
||||
/**
|
||||
* Camera dolly + look target for a fractional district index. The camera rides
|
||||
* behind and above the active district and looks slightly forward+down, so the
|
||||
* district fills the frame while the next one eases into view ahead.
|
||||
*/
|
||||
export function cameraFor(idx: number): {
|
||||
pos: [number, number, number];
|
||||
look: [number, number, number];
|
||||
} {
|
||||
const [tx, ty, tz] = samplePath(idx);
|
||||
// Look target leads a little forward (deeper into −Z) and sits at eye height.
|
||||
const look: [number, number, number] = [tx * 0.55, ty + 1.05, tz - 1.6];
|
||||
// Camera sits back (+Z) and up (+Y) from the active district.
|
||||
const pos: [number, number, number] = [tx * 0.7, ty + 4.0, tz + 8.2];
|
||||
return { pos, look };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user