@@ -12,12 +12,21 @@ import { RestBaker } from "../engine/rest-baker.ts";
1212import { getMorphBlend , type MorphBlend } from "../engine/morph.ts" ;
1313import { createModelTexture } from "../engine/model-texture.ts" ;
1414import type { ModelData } from "../engine/model-loader.ts" ;
15- import type { Preset , ShaderId , SystemSettings } from "../engine/types.ts" ;
15+ import { presets } from "../engine/presets.ts" ;
16+ import {
17+ DEFAULT_SETTINGS ,
18+ type Preset ,
19+ type ShaderId ,
20+ type SystemSettings ,
21+ } from "../engine/types.ts" ;
1622import { clamp , clamp01 , lerp } from "../utils/math.ts" ;
1723import { reducedMotion } from "../utils/reduced-motion.ts" ;
1824
19- // Must match `LOADING_SCREEN_MIN_MS` in `landing-enhancements.tsx`.
20- const PARTICLE_INTRO_DELAY_S = 1 ;
25+ const PARTICLE_INTRO_DURATION_S = 3.5 ;
26+ const BRAND_MODE_SETTINGS : SystemSettings = {
27+ ...DEFAULT_SETTINGS ,
28+ colorMode : 2 ,
29+ } ;
2130const DEFAULT_CAM_POS : [ number , number , number ] = [ 0 , 30 , 80 ] ;
2231const DEFAULT_CAM_TARGET : [ number , number , number ] = [ 0 , 0 , 0 ] ;
2332const CAM_LERP_SPEED = 0.025 ;
@@ -35,10 +44,6 @@ const SHADER_ID_TO_INT: Record<ShaderId, number> = {
3544 racetrackCar : 4 ,
3645} ;
3746
38- function resolveShaderInt ( preset : Preset ) : number {
39- return SHADER_ID_TO_INT [ preset . shaderId ] ;
40- }
41-
4247const shellStyles = css ( {
4348 position : "fixed" ,
4449 inset : "0" ,
@@ -52,15 +57,6 @@ const canvasStyles = css({
5257 height : "100%" ,
5358} ) ;
5459
55- type PresetRuntimeData = {
56- presets : Preset [ ] ;
57- controls : number [ ] [ ] ;
58- shaderInts : number [ ] ;
59- racetrackIndex : number ;
60- driveIndex : number ;
61- driveCarPosY : number ;
62- } ;
63-
6460function setDesiredCameraInto (
6561 presets : Preset [ ] ,
6662 morphValue : number ,
@@ -117,39 +113,31 @@ function buildInitialControls(preset: Preset): number[] {
117113 return controls ;
118114}
119115
120- function getControlInitial ( preset : Preset , id : string , fallback = 0 ) : number {
121- return (
122- preset . controls . find ( ( control ) => control . id === id ) ?. initial ?? fallback
123- ) ;
124- }
116+ const DRIVE_INDEX = presets . findIndex ( ( preset ) => preset . name === "Drive" ) ;
117+ const PRESET_RUNTIME_DATA = {
118+ controls : presets . map ( buildInitialControls ) ,
119+ shaderInts : presets . map ( ( preset ) => SHADER_ID_TO_INT [ preset . shaderId ] ) ,
120+ racetrackIndex : presets . findIndex ( ( preset ) => preset . name === "Racetrack" ) ,
121+ driveIndex : DRIVE_INDEX ,
122+ driveCarPosY :
123+ DRIVE_INDEX >= 0
124+ ? ( presets [ DRIVE_INDEX ] . controls . find (
125+ ( control ) => control . id === "_carPosY" ,
126+ ) ?. initial ?? 0 )
127+ : 0 ,
128+ } ;
125129
126- function buildPresetRuntimeData ( presets : Preset [ ] ) : PresetRuntimeData {
127- const driveIndex = presets . findIndex ( ( preset ) => preset . name === "Drive" ) ;
128- return {
129- presets,
130- controls : presets . map ( buildInitialControls ) ,
131- shaderInts : presets . map ( resolveShaderInt ) ,
132- racetrackIndex : presets . findIndex ( ( preset ) => preset . name === "Racetrack" ) ,
133- driveIndex,
134- driveCarPosY :
135- driveIndex >= 0
136- ? getControlInitial ( presets [ driveIndex ] , "_carPosY" , 0 )
137- : 0 ,
138- } ;
139- }
130+ type ParticleCanvasProps = {
131+ brandGradientMode : boolean ;
132+ morphValueRef : { current : number } ;
133+ modelData : ( ModelData | undefined ) [ ] ;
134+ labelsRef : { current : ProjectedLabel [ ] } ;
135+ labelOpacityRef : { current : number } ;
136+ onFirstFrame : ( ) => Promise < void > ;
137+ onError : ( error : unknown ) => void ;
138+ } ;
140139
141- export function ParticleCanvas (
142- handle : Handle < {
143- settings : SystemSettings ;
144- presets : Preset [ ] ;
145- morphValueRef : { current : number } ;
146- modelData : ( ModelData | undefined ) [ ] ;
147- labelsRef : { current : ProjectedLabel [ ] } ;
148- labelOpacityRef : { current : number } ;
149- onReady : ( ) => void ;
150- onError : ( error : unknown ) => void ;
151- } > ,
152- ) {
140+ export function ParticleCanvas ( handle : Handle < ParticleCanvasProps > ) {
153141 let containerEl : HTMLDivElement | undefined ;
154142 let canvasEl : HTMLCanvasElement | undefined ;
155143 let engine : Engine | null = null ;
@@ -159,9 +147,11 @@ export function ParticleCanvas(
159147 const appliedModelSlots = new Set < number > ( ) ;
160148 let frameId = 0 ;
161149 let startTime = 0 ;
150+ let reveal : Promise < void > | null = null ;
151+ let introStartTime : number | null = null ;
152+ let introFinished = false ;
162153 let frozenTime : number | null = null ;
163154 let previousNearest = - 1 ;
164- let hasReportedReady = false ;
165155 let initFailed = false ;
166156 const labelControlMgr = new ControlManager ( ) ;
167157 const desiredCameraPos = new Vector3 ( ) ;
@@ -174,19 +164,6 @@ export function ParticleCanvas(
174164 const scratchControlsB = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ;
175165 const scratchLabelControls = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ;
176166 const morphBlend : MorphBlend = { fromIndex : 0 , toIndex : 0 , blend : 0 } ;
177- let presetRuntimeData : PresetRuntimeData | null = null ;
178- let currentProps :
179- | {
180- settings : SystemSettings ;
181- presets : Preset [ ] ;
182- morphValueRef : { current : number } ;
183- modelData : ( ModelData | undefined ) [ ] ;
184- labelsRef : { current : ProjectedLabel [ ] } ;
185- labelOpacityRef : { current : number } ;
186- onReady : ( ) => void ;
187- onError : ( error : unknown ) => void ;
188- }
189- | undefined ;
190167
191168 let mouseNormX = 0 ;
192169 let mouseNormY = 0 ;
@@ -266,12 +243,6 @@ export function ParticleCanvas(
266243 } ,
267244 } ) ;
268245
269- function getPresetRuntimeData ( presets : Preset [ ] ) {
270- if ( presetRuntimeData ?. presets === presets ) return presetRuntimeData ;
271- presetRuntimeData = buildPresetRuntimeData ( presets ) ;
272- return presetRuntimeData ;
273- }
274-
275246 function disposeScene ( ) {
276247 cancelAnimationFrame ( frameId ) ;
277248 if ( particles && engine ) {
@@ -295,18 +266,17 @@ export function ParticleCanvas(
295266 } ) ;
296267
297268 function syncModelTextures ( ) {
298- if ( ! restBaker || ! currentProps ) return ;
269+ if ( ! restBaker ) return ;
299270
300- for ( const preset of currentProps . presets ) {
271+ for ( const preset of presets ) {
301272 if (
302273 preset . modelUrl == null ||
303274 preset . modelSlot == null ||
304275 appliedModelSlots . has ( preset . modelSlot )
305276 )
306277 continue ;
307278
308- const model =
309- currentProps . modelData [ currentProps . presets . indexOf ( preset ) ] ;
279+ const model = handle . props . modelData [ presets . indexOf ( preset ) ] ;
310280 if ( ! model ) continue ;
311281
312282 restBaker . setModelTexture (
@@ -319,26 +289,20 @@ export function ParticleCanvas(
319289 }
320290
321291 function maybeInit ( ) {
322- if ( initFailed || engine || ! containerEl || ! canvasEl || ! currentProps ) {
323- return ;
324- }
292+ if ( initFailed || engine || ! containerEl || ! canvasEl ) return ;
325293
326294 try {
295+ const settings = handle . props . brandGradientMode
296+ ? BRAND_MODE_SETTINGS
297+ : DEFAULT_SETTINGS ;
327298 engine = new Engine ( ) ;
328- engine . init ( canvasEl , containerEl , currentProps . settings ) ;
299+ engine . init ( canvasEl , containerEl , settings ) ;
329300
330- restBaker = new RestBaker (
331- engine . renderer ,
332- currentProps . settings . particleCount ,
333- ) ;
334- restBaker . setCount ( currentProps . settings . particleCount ) ;
301+ restBaker = new RestBaker ( engine . renderer , settings . particleCount ) ;
302+ restBaker . setCount ( settings . particleCount ) ;
335303
336304 particles = new ParticleSystem ( ) ;
337- particles . init (
338- engine . scene ,
339- currentProps . settings . particleCount ,
340- currentProps . settings . pointSize ,
341- ) ;
305+ particles . init ( engine . scene , settings . particleCount , settings . pointSize ) ;
342306 // Bind the baker's MRT texture refs to the draw material once. Three
343307 // caches the references; subsequent bake() calls update the GL backing
344308 // in place.
@@ -350,10 +314,7 @@ export function ParticleCanvas(
350314 ) ;
351315 syncModelTextures ( ) ;
352316
353- mouseSim = new MouseSim (
354- engine . renderer ,
355- currentProps . settings . particleCount ,
356- ) ;
317+ mouseSim = new MouseSim ( engine . renderer , settings . particleCount ) ;
357318 mouseSim . setRestTextures (
358319 restBaker . getPosTexture ( 0 ) ,
359320 restBaker . getPosTexture ( 1 ) ,
@@ -366,8 +327,8 @@ export function ParticleCanvas(
366327
367328 startTime = performance . now ( ) / 1000 ;
368329 setDesiredCameraInto (
369- currentProps . presets ,
370- currentProps . morphValueRef . current ,
330+ presets ,
331+ handle . props . morphValueRef . current ,
371332 desiredCameraPos ,
372333 desiredCameraTarget ,
373334 ) ;
@@ -377,18 +338,17 @@ export function ParticleCanvas(
377338 // samples populated rest textures. This also forces the baker's
378339 // RawShaderMaterial to compile here, hiding the link/upload cost from
379340 // the first animate() frame.
380- const initialPresetData = getPresetRuntimeData ( currentProps . presets ) ;
381341 const initialIndex = Math . min (
382- Math . max ( 0 , Math . floor ( currentProps . morphValueRef . current ) ) ,
383- initialPresetData . presets . length - 1 ,
342+ Math . max ( 0 , Math . floor ( handle . props . morphValueRef . current ) ) ,
343+ presets . length - 1 ,
384344 ) ;
385345 copyControlsInto (
386- initialPresetData . controls [ initialIndex ] ,
346+ PRESET_RUNTIME_DATA . controls [ initialIndex ] ,
387347 scratchControlsA ,
388348 ) ;
389349 restBaker . bake (
390350 0 ,
391- initialPresetData . shaderInts [ initialIndex ] ,
351+ PRESET_RUNTIME_DATA . shaderInts [ initialIndex ] ,
392352 scratchControlsA ,
393353 0 ,
394354 ) ;
@@ -400,14 +360,12 @@ export function ParticleCanvas(
400360 } catch ( error ) {
401361 initFailed = true ;
402362 disposeScene ( ) ;
403- currentProps . onError ( error ) ;
363+ handle . props . onError ( error ) ;
404364 return ;
405365 }
406366
407367 const animate = ( ) => {
408- if ( ! engine || ! particles || ! restBaker || ! mouseSim || ! currentProps ) {
409- return ;
410- }
368+ if ( ! engine || ! particles || ! restBaker || ! mouseSim ) return ;
411369
412370 const now = performance . now ( ) ;
413371 const time = now / 1000 - startTime ;
@@ -417,14 +375,15 @@ export function ParticleCanvas(
417375 const dtSeconds =
418376 lastFrameNow === 0 ? 1 / 60 : ( now - lastFrameNow ) / 1000 ;
419377 lastFrameNow = now ;
420- const settings = currentProps . settings ;
421- const presets = currentProps . presets ;
422- const presetData = getPresetRuntimeData ( presets ) ;
423- const morphValue = currentProps . morphValueRef . current ;
378+ const settings = handle . props . brandGradientMode
379+ ? BRAND_MODE_SETTINGS
380+ : DEFAULT_SETTINGS ;
381+ const presetData = PRESET_RUNTIME_DATA ;
382+ const morphValue = handle . props . morphValueRef . current ;
424383 const reduceMotion = reducedMotion . current ;
425384
426385 if ( reduceMotion ) {
427- frozenTime ??= Math . max ( time , PARTICLE_INTRO_DELAY_S + 3.5 ) ;
386+ frozenTime ??= Math . max ( time , PARTICLE_INTRO_DURATION_S ) ;
428387 } else {
429388 frozenTime = null ;
430389 }
@@ -471,10 +430,19 @@ export function ParticleCanvas(
471430
472431 particles . setColorMode ( settings . colorMode ) ;
473432 particles . setDof ( settings . dofAmount , settings . dofFocus ) ;
474- const introTime = Math . max ( 0 , visualTime - PARTICLE_INTRO_DELAY_S ) ;
475- particles . setIntroProgress (
476- reduceMotion ? 1.5 : Math . min ( introTime / 3.5 , 1.5 ) ,
477- ) ;
433+ if ( reduceMotion && introStartTime !== null ) {
434+ // Once reduced motion has skipped the intro, do not replay it if the
435+ // preference changes while this component is mounted.
436+ introFinished = true ;
437+ }
438+ const introTime =
439+ introStartTime === null ? 0 : Math . max ( 0 , visualTime - introStartTime ) ;
440+ const introProgress =
441+ reduceMotion || introFinished
442+ ? 1.5
443+ : Math . min ( introTime / PARTICLE_INTRO_DURATION_S , 1.5 ) ;
444+ introFinished ||= introProgress >= 1.5 ;
445+ particles . setIntroProgress ( introProgress ) ;
478446 particles . setTime ( visualTime ) ;
479447
480448 const maxValue = presets . length - 1 ;
@@ -689,7 +657,7 @@ export function ParticleCanvas(
689657 }
690658
691659 projectLabelsInto (
692- currentProps . labelsRef . current ,
660+ handle . props . labelsRef . current ,
693661 nearestPreset ,
694662 labelControlMgr ,
695663 activeCtrls ,
@@ -700,29 +668,28 @@ export function ParticleCanvas(
700668 ) ;
701669
702670 const distFromNearest = Math . abs ( morphValue - nearest ) ;
703- currentProps . labelOpacityRef . current = Math . max (
671+ handle . props . labelOpacityRef . current = Math . max (
704672 0 ,
705673 1 - distFromNearest * 4 ,
706674 ) ;
707675 } else {
708- currentProps . labelsRef . current . length = 0 ;
709- currentProps . labelOpacityRef . current = 0 ;
676+ handle . props . labelsRef . current . length = 0 ;
677+ handle . props . labelOpacityRef . current = 0 ;
710678 }
711679
712680 engine . render ( time ) ;
713- if ( ! hasReportedReady ) {
714- hasReportedReady = true ;
715- currentProps . onReady ( ) ;
716- }
681+ reveal ??= handle . props . onFirstFrame ( ) . then ( ( ) => {
682+ if ( ! handle . signal . aborted ) {
683+ introStartTime = performance . now ( ) / 1000 - startTime ;
684+ }
685+ } ) ;
717686 frameId = requestAnimationFrame ( animate ) ;
718687 } ;
719688
720689 frameId = requestAnimationFrame ( animate ) ;
721690 }
722691
723692 return ( ) => {
724- currentProps = handle . props ;
725-
726693 if ( engine ) {
727694 syncModelTextures ( ) ;
728695 }
0 commit comments