fix mobile view issue
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||
import { RoundedBox, Line, Sparkles, Html, useGLTF, ContactShadows, Environment, Lightformer, Preload } from "@react-three/drei";
|
||||
import { RoundedBox, Line, Sparkles, Html, useGLTF, useProgress, ContactShadows, Environment, Lightformer, Preload } from "@react-three/drei";
|
||||
import { EffectComposer, Bloom } from "@react-three/postprocessing";
|
||||
import { KernelSize } from "postprocessing";
|
||||
import * as THREE from "three";
|
||||
@@ -14,6 +14,8 @@ type Props = {
|
||||
isMobile?: boolean;
|
||||
active?: boolean;
|
||||
stage?: number;
|
||||
ready?: boolean;
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
const BG = "#eef1f6";
|
||||
@@ -785,7 +787,50 @@ function Floor() {
|
||||
);
|
||||
}
|
||||
|
||||
function Scene({ progress, reduced, isMobile, stage, active, perf }: { progress: React.RefObject<number>; reduced: boolean; isMobile: boolean; stage: number; active: boolean; perf: boolean }) {
|
||||
function SceneReadySignal({ onReady }: { onReady?: () => void }) {
|
||||
const { active, progress } = useProgress();
|
||||
const gl = useThree((s) => s.gl);
|
||||
const scene = useThree((s) => s.scene);
|
||||
const camera = useThree((s) => s.camera);
|
||||
const fired = useRef(false);
|
||||
const compiling = useRef(false);
|
||||
const compiled = useRef(false);
|
||||
const meaningfulFrames = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (fired.current || compiling.current || compiled.current || active || progress < 100) return;
|
||||
|
||||
compiling.current = true;
|
||||
const renderer = gl as THREE.WebGLRenderer & {
|
||||
compileAsync?: (scene: THREE.Scene, camera: THREE.Camera) => Promise<void>;
|
||||
};
|
||||
const compile = renderer.compileAsync
|
||||
? renderer.compileAsync(scene, camera)
|
||||
: Promise.resolve(renderer.compile(scene, camera));
|
||||
|
||||
compile
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
compiled.current = true;
|
||||
});
|
||||
}, [active, camera, gl, progress, scene]);
|
||||
|
||||
useFrame(() => {
|
||||
if (fired.current || active || progress < 100 || !compiled.current) return;
|
||||
|
||||
const renderInfo = gl.info.render;
|
||||
const hasMeaningfulScene = renderInfo.calls >= 12 && renderInfo.triangles >= 500;
|
||||
meaningfulFrames.current = hasMeaningfulScene ? meaningfulFrames.current + 1 : 0;
|
||||
if (meaningfulFrames.current < 4) return;
|
||||
|
||||
fired.current = true;
|
||||
requestAnimationFrame(() => onReady?.());
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function Scene({ progress, reduced, isMobile, stage, active, perf, onReady }: { progress: React.RefObject<number>; reduced: boolean; isMobile: boolean; stage: number; active: boolean; perf: boolean; onReady?: () => void }) {
|
||||
// Districts stay MOUNTED for the whole scroll (GLB cloned once, shaders compiled
|
||||
// once → no remount hitch, no blank frames). `near` only toggles per-district
|
||||
// visibility + per-frame work + <Html> mounting, so off-screen districts render
|
||||
@@ -844,12 +889,13 @@ function Scene({ progress, reduced, isMobile, stage, active, perf }: { progress:
|
||||
{/* Compile every material/texture upfront (districts are all mounted) so the
|
||||
first time a district becomes visible there's no shader-compile stall. */}
|
||||
<Preload all />
|
||||
<SceneReadySignal onReady={onReady} />
|
||||
{perf && <PerfHud />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StrategyCanvas({ progress, reduced = false, isMobile = false, active = true, stage = 0 }: Props) {
|
||||
export default function StrategyCanvas({ progress, reduced = false, isMobile = false, active = true, stage = 0, ready = false, onReady }: Props) {
|
||||
// Opt-in dev metrics overlay: append ?perf to the URL.
|
||||
const perf = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("perf");
|
||||
return (
|
||||
@@ -857,9 +903,9 @@ export default function StrategyCanvas({ progress, reduced = false, isMobile = f
|
||||
dpr={[1, isMobile ? 1 : 1.25]}
|
||||
camera={{ position: [0, 4, 8.2], fov: 50, near: 0.1, far: 120 }}
|
||||
gl={{ antialias: false, powerPreference: "high-performance", alpha: false }}
|
||||
frameloop={active ? "always" : "never"}
|
||||
frameloop={active || !ready ? "always" : "never"}
|
||||
>
|
||||
<Scene progress={progress} reduced={reduced} isMobile={isMobile} stage={stage} active={active} perf={perf} />
|
||||
<Scene progress={progress} reduced={reduced} isMobile={isMobile} stage={stage} active={active} perf={perf} onReady={onReady} />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { motion, useMotionValue, useTransform, type MotionValue } from "framer-motion";
|
||||
import gsap from "gsap";
|
||||
@@ -83,6 +83,8 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before");
|
||||
const [active, setActive] = useState(0);
|
||||
const [mountScene, setMountScene] = useState(false);
|
||||
const [sceneReady, setSceneReady] = useState(false);
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
const [sceneActive, setSceneActive] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [reduced, setReduced] = useState(false);
|
||||
@@ -161,8 +163,30 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
<div className="dm-st-sticky">
|
||||
<div className="dm-st-card">
|
||||
{mountScene && (
|
||||
<div className="dm-st-canvas">
|
||||
<StrategyCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive && pinState === "pinned"} stage={active} />
|
||||
<div className={`dm-st-canvas${sceneReady ? " is-ready" : ""}`} aria-hidden={!sceneReady}>
|
||||
<StrategyCanvas
|
||||
progress={progressRef}
|
||||
reduced={reduced}
|
||||
isMobile={isMobile}
|
||||
active={sceneActive && pinState === "pinned"}
|
||||
stage={active}
|
||||
ready={sceneReady}
|
||||
onReady={() => setSceneReady(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLoader && (
|
||||
<div
|
||||
className={`dm-st-loader${sceneReady ? " is-hiding" : ""}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Loading MileTruth Strategy Engine"
|
||||
onTransitionEnd={(e) => {
|
||||
if (e.propertyName === "opacity" && sceneReady) setShowLoader(false);
|
||||
}}
|
||||
>
|
||||
<span className="dm-st-loader__ring" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -170,7 +194,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
can never be seen during the approach ("before"), where the sticky sits
|
||||
at the top of the tall section near the previous workflow's seam. */}
|
||||
{pinState !== "before" && (
|
||||
<div className="dm-st-ui">
|
||||
<div className={`dm-st-ui${sceneReady ? " is-ready" : ""}`} aria-hidden={!sceneReady}>
|
||||
{/* Persistent header */}
|
||||
<motion.div className="dm-st-top" style={{ opacity: headerOpacity }}>
|
||||
<div className="dm-st-eyebrow"><span className="dm-st-dot" /> MileTruth Strategy Engine</div>
|
||||
@@ -285,11 +309,28 @@ const styles = `
|
||||
}
|
||||
}
|
||||
|
||||
.dm-st-canvas { position: absolute; inset: 0; z-index: 1; }
|
||||
.dm-st-canvas { position: absolute; inset: 0; z-index: 1; opacity: 0; visibility: hidden;
|
||||
transition: opacity 0.42s cubic-bezier(0.22,1,0.36,1), visibility 0s linear 0.42s; }
|
||||
.dm-st-canvas.is-ready { opacity: 1; visibility: visible; transition-delay: 0s; }
|
||||
.dm-st-canvas canvas { display: block; }
|
||||
|
||||
.dm-st-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none;
|
||||
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; color: #0f172a; }
|
||||
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; color: #0f172a;
|
||||
opacity: 0; visibility: hidden;
|
||||
transition: opacity 0.42s cubic-bezier(0.22,1,0.36,1), visibility 0s linear 0.42s; }
|
||||
.dm-st-ui.is-ready { opacity: 1; visibility: visible; transition-delay: 0s; }
|
||||
|
||||
.dm-st-loader { position: absolute; inset: 0; z-index: 6; display: grid; place-items: center;
|
||||
background: transparent; opacity: 1; pointer-events: none;
|
||||
animation: dmStLoaderFadeIn 0.2s ease both;
|
||||
transition: opacity 0.3s ease; }
|
||||
.dm-st-loader.is-hiding { opacity: 0; pointer-events: none; }
|
||||
.dm-st-loader__ring { width: 18px; height: 18px; border-radius: 50%;
|
||||
border: 2px solid rgba(255,255,255,0.48); border-top-color: #ffffff;
|
||||
box-shadow: 0 8px 22px rgba(15,23,42,0.14);
|
||||
animation: dm-hiw-spin 0.8s linear infinite; }
|
||||
@keyframes dmStLoaderFadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes dm-hiw-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ---- Persistent header: title + 5-stage rail ---- */
|
||||
.dm-st-top { position: absolute; top: clamp(96px, 13vh, 128px); left: 0; right: 0; z-index: 5;
|
||||
|
||||
Reference in New Issue
Block a user