[Website] Resolve animations breaking due to renderer context being lost. (#19747)
Resolves the following two issues. The reason we had these issues was because the maxLimit of renderers was reached and the browser started losing context before restoring it. Now, we make sure the context that is lost belongs to visuals that are not currently in the viewport and when those visuals come back into viewport, they're loaded again. https://github.com/twentyhq/core-team-issues/issues/2372 https://github.com/twentyhq/core-team-issues/issues/2373
This commit is contained in:
Binary file not shown.
@@ -22,6 +22,7 @@ import type {
|
||||
import { styled } from '@linaria/react';
|
||||
import { type MutableRefObject, useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const passThroughVertexShader = `
|
||||
varying vec2 vUv;
|
||||
@@ -738,7 +739,7 @@ export function HalftoneCanvas({
|
||||
const getRenderWidth = () =>
|
||||
Math.max(Math.round(getVirtualWidth() * getRenderScale()), 1);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
|
||||
const renderer = createSiteWebGlRenderer({ antialias: false, alpha: true });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLayoutEffect, useRef, useState, type ReactNode } from 'react';
|
||||
|
||||
const VERTICAL_MARGIN_PX = 100;
|
||||
const ROOT_MARGIN = `${VERTICAL_MARGIN_PX}px 0px ${VERTICAL_MARGIN_PX}px 0px`;
|
||||
const OUT_OF_VIEW_DISPOSE_MS = 300;
|
||||
|
||||
const ObserverRoot = styled.div<{ detachFromLayout: boolean }>`
|
||||
height: 100%;
|
||||
min-height: 1px;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
|
||||
& > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
${({ detachFromLayout }) =>
|
||||
detachFromLayout
|
||||
? `
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
`
|
||||
: `
|
||||
position: relative;
|
||||
`}
|
||||
`;
|
||||
|
||||
function isLikelyInViewport(element: HTMLElement) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 0;
|
||||
const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 0;
|
||||
|
||||
return (
|
||||
rect.bottom > -VERTICAL_MARGIN_PX &&
|
||||
rect.top < viewportHeight + VERTICAL_MARGIN_PX &&
|
||||
rect.right > 0 &&
|
||||
rect.left < viewportWidth
|
||||
);
|
||||
}
|
||||
|
||||
type WebGlWhenInViewportProps = {
|
||||
children: ReactNode;
|
||||
detachFromLayout?: boolean;
|
||||
};
|
||||
|
||||
export function WebGlWhenInViewport({
|
||||
children,
|
||||
detachFromLayout = false,
|
||||
}: WebGlWhenInViewportProps) {
|
||||
const rootReference = useRef<HTMLDivElement>(null);
|
||||
const [shouldRender, setShouldRender] = useState(false);
|
||||
const disposeTimerReference = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const element = rootReference.current;
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clearDisposeTimer = () => {
|
||||
if (disposeTimerReference.current !== null) {
|
||||
clearTimeout(disposeTimerReference.current);
|
||||
disposeTimerReference.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (isLikelyInViewport(element)) {
|
||||
setShouldRender(true);
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
clearDisposeTimer();
|
||||
setShouldRender(true);
|
||||
return;
|
||||
}
|
||||
|
||||
clearDisposeTimer();
|
||||
disposeTimerReference.current = setTimeout(() => {
|
||||
setShouldRender(false);
|
||||
disposeTimerReference.current = null;
|
||||
}, OUT_OF_VIEW_DISPOSE_MS);
|
||||
},
|
||||
{ root: null, rootMargin: ROOT_MARGIN, threshold: 0 },
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
|
||||
return () => {
|
||||
clearDisposeTimer();
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ObserverRoot ref={rootReference} detachFromLayout={detachFromLayout}>
|
||||
{shouldRender ? children : null}
|
||||
</ObserverRoot>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const VIRTUAL_RENDER_HEIGHT = 768;
|
||||
|
||||
@@ -607,7 +608,7 @@ export function FaqBackground() {
|
||||
1,
|
||||
);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
|
||||
const renderer = createSiteWebGlRenderer({ antialias: false, alpha: true });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const VIRTUAL_RENDER_HEIGHT = 768;
|
||||
|
||||
@@ -638,7 +639,7 @@ export function FooterBackground() {
|
||||
1,
|
||||
);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
|
||||
const renderer = createSiteWebGlRenderer({ antialias: false, alpha: true });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as THREE from 'three';
|
||||
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const DRACO_DECODER_PATH =
|
||||
'https://www.gstatic.com/draco/versioned/decoders/1.5.6/';
|
||||
@@ -831,7 +832,7 @@ function HelpedHalftoneCanvas({
|
||||
1,
|
||||
);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
|
||||
const renderer = createSiteWebGlRenderer({ antialias: false, alpha: true });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { PartnerHalftoneOverlay } from '@/sections/Hero/components/PartnerVisual/PartnerHalftoneOverlay';
|
||||
|
||||
const HERO_PARTNER_IMAGE_URL = '/images/partner/hero/partners-hero.webp';
|
||||
|
||||
export function PartnerHeroHalftoneIllustration() {
|
||||
return <PartnerHalftoneOverlay imageUrl={HERO_PARTNER_IMAGE_URL} />;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const GLB_URL = '/illustrations/why-twenty/hero/hero.glb';
|
||||
|
||||
@@ -200,7 +201,7 @@ export function WhyTwenty() {
|
||||
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100);
|
||||
camera.position.set(0, 0, 5.2);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
|
||||
const renderer = createSiteWebGlRenderer({ alpha: true, antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(width, height);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { StepperBackgroundHalftone } from '@/sections/HomeStepper/components/Visual/StepperBackgroundHalftone';
|
||||
|
||||
const HOME_STEPPER_BACKGROUND_IMAGE_URL = '/images/home/stepper/gears.jpg';
|
||||
|
||||
export function HomeStepperBackgroundIllustration() {
|
||||
return (
|
||||
<StepperBackgroundHalftone imageUrl={HOME_STEPPER_BACKGROUND_IMAGE_URL} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Monolith from '@/sections/Problem/components/Visual/monolith';
|
||||
|
||||
export function ProblemMonolithIllustration() {
|
||||
return <Monolith />;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useLayoutEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const GLB_URL = '/illustrations/why-twenty/quote/quote.glb';
|
||||
|
||||
@@ -172,7 +173,7 @@ export function Quotes() {
|
||||
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100);
|
||||
camera.position.set(0, 0, 5.05);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
|
||||
const renderer = createSiteWebGlRenderer({ alpha: true, antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(width, height);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment
|
||||
import { styled } from '@linaria/react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
interface HourglassLightingSettings {
|
||||
intensity: number;
|
||||
@@ -348,7 +349,7 @@ export function HourglassCanvas({
|
||||
1,
|
||||
);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
|
||||
const renderer = createSiteWebGlRenderer({ antialias: false, alpha: true });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
@@ -594,8 +595,7 @@ export function HourglassCanvas({
|
||||
animationFrameId = window.requestAnimationFrame(renderFrame);
|
||||
|
||||
const delta = clock.getDelta();
|
||||
const elapsedTime =
|
||||
(initialPose?.timeElapsed ?? 0) + clock.elapsedTime;
|
||||
const elapsedTime = (initialPose?.timeElapsed ?? 0) + clock.elapsedTime;
|
||||
halftoneMaterial.uniforms.time.value = elapsedTime;
|
||||
|
||||
let baseRotationX = 0;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ import { useLayoutEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const GLB_URL = '/illustrations/why-twenty/stepper/logo.glb';
|
||||
|
||||
@@ -229,7 +230,7 @@ export function Logo() {
|
||||
|
||||
updateOrthoFrustum();
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
|
||||
const renderer = createSiteWebGlRenderer({ alpha: true, antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(width, height);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { VisibleWhenTabActive } from '@/components/VisibleWhenTabActive';
|
||||
import { WebGlWhenInViewport } from '@/components/WebGlWhenInViewport';
|
||||
import {
|
||||
ILLUSTRATIONS,
|
||||
type IllustrationId,
|
||||
@@ -12,10 +13,13 @@ type IllustrationMountProps = {
|
||||
|
||||
export function IllustrationMount({ illustration }: IllustrationMountProps) {
|
||||
const IllustrationComponent = ILLUSTRATIONS[illustration];
|
||||
const detachWebGlGateFromLayout = illustration === 'footerBackground';
|
||||
|
||||
return (
|
||||
<VisibleWhenTabActive>
|
||||
<IllustrationComponent />
|
||||
<WebGlWhenInViewport detachFromLayout={detachWebGlGateFromLayout}>
|
||||
<IllustrationComponent />
|
||||
</WebGlWhenInViewport>
|
||||
</VisibleWhenTabActive>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ import { Eye } from './ThreeCards/Eye';
|
||||
import { PartnerThreeCard } from './ThreeCards/PartnerThreeCard';
|
||||
import { SingleScreen } from './ThreeCards/SingleScreen';
|
||||
import { Speed } from './ThreeCards/Speed';
|
||||
import { PartnerHeroHalftoneIllustration } from './Hero/PartnerHeroHalftoneIllustration';
|
||||
import { HomeStepperBackgroundIllustration } from './HomeStepper/HomeStepperBackgroundIllustration';
|
||||
import { ProblemMonolithIllustration } from './Problem/ProblemMonolithIllustration';
|
||||
import { Logo as WhyTwentyStepperLogo } from './WhyTwentyStepper/Logo';
|
||||
|
||||
const DiamondIllustration = () => (
|
||||
@@ -38,9 +41,7 @@ const GrowIllustration = () => (
|
||||
);
|
||||
|
||||
const ProgrammingIllustration = () => (
|
||||
<PartnerThreeCard
|
||||
modelUrl="/illustrations/partner/three-cards/programming.glb"
|
||||
/>
|
||||
<PartnerThreeCard modelUrl="/illustrations/partner/three-cards/programming.glb" />
|
||||
);
|
||||
|
||||
export const THREE_CARDS_ILLUSTRATIONS = {
|
||||
@@ -61,6 +62,9 @@ export const ILLUSTRATIONS = {
|
||||
...THREE_CARDS_ILLUSTRATIONS,
|
||||
faqBackground: FaqBackground,
|
||||
footerBackground: FooterBackground,
|
||||
heroPartnerHalftone: PartnerHeroHalftoneIllustration,
|
||||
homeStepperBackgroundHalftone: HomeStepperBackgroundIllustration,
|
||||
problemMonolith: ProblemMonolithIllustration,
|
||||
quoteQuotes: Quotes,
|
||||
whyTwentyStepperLogo: WhyTwentyStepperLogo,
|
||||
heroProduct: Product,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export type SiteWebGlRendererParameters = THREE.WebGLRendererParameters;
|
||||
|
||||
// Single construction site for all Twenty website WebGL canvases.
|
||||
// Keeps renderer policy (limits, logging, pooling) in one module.
|
||||
// Context count is still bounded by the browser; pair with viewport gating
|
||||
// (`WebGlWhenInViewport` via `IllustrationMount`) and avoid extra canvases.
|
||||
|
||||
export function createSiteWebGlRenderer(
|
||||
parameters?: SiteWebGlRendererParameters,
|
||||
): THREE.WebGLRenderer {
|
||||
return new THREE.WebGLRenderer(parameters);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
createSiteWebGlRenderer,
|
||||
type SiteWebGlRendererParameters,
|
||||
} from './create-site-webgl-renderer';
|
||||
@@ -1,50 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useGSAP } from '@gsap/react';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/dist/ScrollTrigger';
|
||||
import { ReactNode, useRef } from 'react';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
gsap.registerPlugin(useGSAP);
|
||||
|
||||
export function ScrollReveal({
|
||||
children,
|
||||
stagger = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
stagger?: boolean;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useGSAP(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Select the direct children or just use the container if stagger isn't needed.
|
||||
const targets = stagger ? containerRef.current.children : containerRef.current;
|
||||
|
||||
gsap.fromTo(
|
||||
targets,
|
||||
{
|
||||
opacity: 0,
|
||||
y: 40,
|
||||
scale: 0.98,
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
duration: 1,
|
||||
ease: 'power3.out',
|
||||
stagger: stagger ? 0.15 : 0,
|
||||
scrollTrigger: {
|
||||
trigger: containerRef.current,
|
||||
start: 'top 85%',
|
||||
toggleActions: 'play none none reverse',
|
||||
},
|
||||
}
|
||||
);
|
||||
}, { scope: containerRef });
|
||||
|
||||
return <div ref={containerRef}>{children}</div>;
|
||||
}
|
||||
+2
-1
@@ -3,6 +3,7 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const PREVIEW_DISTANCE = 3.2;
|
||||
const SOURCE_PREVIEW_DISTANCE = 6.1;
|
||||
@@ -442,7 +443,7 @@ async function mountHalftoneOverlay({
|
||||
1,
|
||||
);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
const renderer = createSiteWebGlRenderer({
|
||||
alpha: true,
|
||||
antialias: false,
|
||||
powerPreference: 'high-performance',
|
||||
|
||||
+2
-8
@@ -1,11 +1,7 @@
|
||||
import { VisibleWhenTabActive } from '@/components/VisibleWhenTabActive';
|
||||
import { IllustrationMount } from '@/illustrations';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { PartnerHalftoneOverlay } from './PartnerHalftoneOverlay';
|
||||
|
||||
const HERO_IMAGE_URL = '/images/partner/hero/partners-hero.webp';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
background-color: ${theme.colors.secondary.background[100]};
|
||||
border-radius: ${theme.radius(1)};
|
||||
@@ -23,9 +19,7 @@ const StyledContainer = styled.div`
|
||||
export function PartnerVisual() {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<VisibleWhenTabActive>
|
||||
<PartnerHalftoneOverlay imageUrl={HERO_IMAGE_URL} />
|
||||
</VisibleWhenTabActive>
|
||||
<IllustrationMount illustration="heroPartnerHalftone" />
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-1
@@ -9,6 +9,9 @@ const FRAME_MASK_PATH =
|
||||
|
||||
const frameMask = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 672 705' preserveAspectRatio='none'%3E%3Cpath d='${encodeURIComponent(FRAME_MASK_PATH)}' fill='black'/%3E%3C/svg%3E")`;
|
||||
|
||||
export const STEPPER_VISUAL_POINTER_ROOT_SELECTOR =
|
||||
'[data-stepper-visual-pointer-root]';
|
||||
|
||||
const FrameRoot = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -101,7 +104,7 @@ export function StepperVisualFrame({
|
||||
shapeSrc,
|
||||
}: StepperVisualFrameProps) {
|
||||
return (
|
||||
<FrameRoot>
|
||||
<FrameRoot data-stepper-visual-pointer-root="">
|
||||
<MaskedBackdrop
|
||||
style={{
|
||||
...getMaskStyle(),
|
||||
|
||||
+27
-19
@@ -6,8 +6,10 @@ import {
|
||||
getImagePreviewZoom,
|
||||
} from '@/app/halftone/_lib/footprint';
|
||||
import { styled } from '@linaria/react';
|
||||
import { STEPPER_VISUAL_POINTER_ROOT_SELECTOR } from '../StepperVisualFrame/StepperVisualFrame';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const PREVIEW_DISTANCE = 4;
|
||||
const HOVER_FADE_IN = 18;
|
||||
@@ -305,7 +307,7 @@ async function mountHalftoneCanvas({
|
||||
return;
|
||||
}
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
const renderer = createSiteWebGlRenderer({
|
||||
alpha: true,
|
||||
antialias: false,
|
||||
powerPreference: 'high-performance',
|
||||
@@ -323,7 +325,13 @@ async function mountHalftoneCanvas({
|
||||
canvas.style.width = '100%';
|
||||
container.appendChild(canvas);
|
||||
|
||||
const interactionTarget = container.parentElement?.parentElement ?? container;
|
||||
const interactionElement =
|
||||
container.closest(STEPPER_VISUAL_POINTER_ROOT_SELECTOR) ??
|
||||
container.parentElement?.parentElement ??
|
||||
container;
|
||||
|
||||
const interactionTarget: HTMLElement =
|
||||
interactionElement instanceof HTMLElement ? interactionElement : container;
|
||||
|
||||
const imageTexture = new THREE.Texture(image);
|
||||
imageTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
@@ -521,23 +529,23 @@ async function mountHalftoneCanvas({
|
||||
pointerState.pointerVelocityY *= 0.82;
|
||||
|
||||
halftoneMaterial.uniforms.footprintScale.value = getHalftoneScale();
|
||||
halftoneMaterial.uniforms.hoverHalftoneActive.value =
|
||||
HALFTONE_SETTINGS.animation.hoverHalftoneEnabled
|
||||
? pointerState.hoverStrength
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverHalftonePowerShift.value =
|
||||
HALFTONE_SETTINGS.animation.hoverHalftoneEnabled
|
||||
? HALFTONE_SETTINGS.animation.hoverHalftonePowerShift
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverHalftoneWidthShift.value =
|
||||
HALFTONE_SETTINGS.animation.hoverHalftoneEnabled
|
||||
? HALFTONE_SETTINGS.animation.hoverHalftoneWidthShift
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverLightStrength.value =
|
||||
HALFTONE_SETTINGS.animation.hoverLightEnabled
|
||||
? HALFTONE_SETTINGS.animation.hoverLightIntensity *
|
||||
pointerState.hoverStrength
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverHalftoneActive.value = HALFTONE_SETTINGS
|
||||
.animation.hoverHalftoneEnabled
|
||||
? pointerState.hoverStrength
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverHalftonePowerShift.value = HALFTONE_SETTINGS
|
||||
.animation.hoverHalftoneEnabled
|
||||
? HALFTONE_SETTINGS.animation.hoverHalftonePowerShift
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverHalftoneWidthShift.value = HALFTONE_SETTINGS
|
||||
.animation.hoverHalftoneEnabled
|
||||
? HALFTONE_SETTINGS.animation.hoverHalftoneWidthShift
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverLightStrength.value = HALFTONE_SETTINGS
|
||||
.animation.hoverLightEnabled
|
||||
? HALFTONE_SETTINGS.animation.hoverLightIntensity *
|
||||
pointerState.hoverStrength
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.interactionUv.value.set(
|
||||
pointerState.smoothedMouseX,
|
||||
1 - pointerState.smoothedMouseY,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { VisibleWhenTabActive } from '@/components/VisibleWhenTabActive';
|
||||
|
||||
import { IllustrationMount } from '@/illustrations';
|
||||
import { StepperVisualFrame } from '../StepperVisualFrame/StepperVisualFrame';
|
||||
import { StepperBackgroundHalftone } from './StepperBackgroundHalftone';
|
||||
import { StepperLottie } from './StepperLottie';
|
||||
|
||||
const HOME_STEPPER_BACKGROUND =
|
||||
@@ -20,9 +18,7 @@ export function Visual({ scrollProgress }: VisualProps) {
|
||||
backgroundColor="#424242"
|
||||
backgroundSrc={HOME_STEPPER_BACKGROUND}
|
||||
backgroundOverlay={
|
||||
<VisibleWhenTabActive>
|
||||
<StepperBackgroundHalftone imageUrl={HOME_STEPPER_BACKGROUND} />
|
||||
</VisibleWhenTabActive>
|
||||
<IllustrationMount illustration="homeStepperBackgroundHalftone" />
|
||||
}
|
||||
borderColor="#DBDBDB"
|
||||
showBackgroundImage={false}
|
||||
|
||||
+8
-4
@@ -4,12 +4,16 @@ function clampUnit(value: number): number {
|
||||
return Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
const STEP_1_TRANSITION_END = 415;
|
||||
const STEP_2_TRANSITION_END = 1045;
|
||||
// Lottie timeline (frames): step 1 hold ends → transition ends → step 2 hold ends → transition ends → step 3.
|
||||
const STEP_1_HOLD_END = 240;
|
||||
const STEP_1_TRANSITION_END = 285;
|
||||
const STEP_2_HOLD_END = 880;
|
||||
const STEP_2_TRANSITION_END = 925;
|
||||
|
||||
// Fraction of each scroll third spent in “hold” before UI / Lottie moves to the next step (see LeftColumn).
|
||||
export const HOME_STEPPER_HOLD_FRACTIONS = [
|
||||
345 / STEP_1_TRANSITION_END,
|
||||
(933 - STEP_1_TRANSITION_END) /
|
||||
STEP_1_HOLD_END / STEP_1_TRANSITION_END,
|
||||
(STEP_2_HOLD_END - STEP_1_TRANSITION_END) /
|
||||
(STEP_2_TRANSITION_END - STEP_1_TRANSITION_END),
|
||||
1,
|
||||
] as const;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { VisibleWhenTabActive } from '@/components/VisibleWhenTabActive';
|
||||
import { IllustrationMount } from '@/illustrations';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import Monolith from './monolith';
|
||||
|
||||
const DESKTOP_PATH =
|
||||
'M672 395.498V701a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4h664a4 4 0 0 1 4 4v65.155c0 2.363-.837 4.65-2.361 6.454l-23.603 27.936a10 10 0 0 0-2.362 6.453v242.614c0 2.245.756 4.424 2.145 6.188l24.036 30.51a10 10 0 0 1 2.145 6.188';
|
||||
@@ -54,9 +53,7 @@ export function Visual() {
|
||||
<StyledVisual>
|
||||
<StyledMasked>
|
||||
<StyledHalftoneLayer>
|
||||
<VisibleWhenTabActive>
|
||||
<Monolith />
|
||||
</VisibleWhenTabActive>
|
||||
<IllustrationMount illustration="problemMonolith" />
|
||||
</StyledHalftoneLayer>
|
||||
</StyledMasked>
|
||||
</StyledVisual>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
import { useEffect, useRef, type CSSProperties } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { createSiteWebGlRenderer } from '@/lib/webgl';
|
||||
|
||||
const IMAGE_SRC = '/images/home/problem/monolith-problem.webp';
|
||||
const PREVIEW_DISTANCE = 4;
|
||||
@@ -495,7 +492,7 @@ async function mountHalftoneCanvas(options: MountHalftoneCanvasOptions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: false });
|
||||
const renderer = createSiteWebGlRenderer({ alpha: true, antialias: false });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
renderer.setPixelRatio(1);
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { IllustrationMount } from '@/illustrations';
|
||||
|
||||
export function MountedTestimonialsHomeVisual() {
|
||||
return <IllustrationMount illustration="testimonialsHourglass" />;
|
||||
}
|
||||
|
||||
export function MountedTestimonialsPartnerVisual() {
|
||||
return <IllustrationMount illustration="testimonialsPartner" />;
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Hourglass } from '@/illustrations/Testimonials/Hourglass';
|
||||
import { Partner as PartnerVisual } from '@/illustrations/Testimonials/Partner';
|
||||
import { Carousel } from './Carousel/Carousel';
|
||||
import {
|
||||
MountedTestimonialsHomeVisual,
|
||||
MountedTestimonialsPartnerVisual,
|
||||
} from './MountedTestimonialsVisuals';
|
||||
import { PartnerCarousel } from './PartnerCarousel/PartnerCarousel';
|
||||
import { Root } from './Root/Root';
|
||||
import { Separator } from './Separator/Separator';
|
||||
|
||||
export const Testimonials = {
|
||||
Carousel,
|
||||
HomeVisual: Hourglass,
|
||||
HomeVisual: MountedTestimonialsHomeVisual,
|
||||
PartnerCarousel,
|
||||
PartnerVisual,
|
||||
PartnerVisual: MountedTestimonialsPartnerVisual,
|
||||
Root,
|
||||
Separator,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user