create-definedmotion 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,17 +5,29 @@ import {
5
5
  type InternalAnimation,
6
6
  type UserAnimation
7
7
  } from '../animation/protocols'
8
- import { generateID, logCameraState } from '../general/helpers'
8
+ import { generateID } from '../general/helpers'
9
9
  import { sleep } from '../rendering/helpers'
10
10
  import { createScene } from '../rendering/setup'
11
11
  import * as THREE from 'three'
12
12
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
13
13
  import { easeConstant } from '../animation/interpolations'
14
- import { animationFPSThrottle, renderSkip } from '../../../../entry'
14
+ import { animationFPSDivider, renderSkip } from '../../../../entry'
15
15
  import { addDestroyFunction } from '../general/onDestory'
16
- import { ticksToMillis } from '../animation/helpers'
17
16
  import { AudioInScene, loadAllAudio, playAudio, registerAudio } from '../audio/loader'
18
17
 
18
+ export const screenFPS = await (window.api as any).getDisplayHz(); //Your screen fps
19
+
20
+ const timelineFPS = screenFPS / animationFPSDivider;
21
+
22
+ // Convert ticks (frames) to milliseconds
23
+ export const ticksToMillis = (ticks: number) => (ticks / timelineFPS) * 1000
24
+
25
+ // Convert milliseconds to the closest whole number of ticks
26
+ export const millisToTicks = (ms: number) => Math.ceil((ms / 1000) * timelineFPS)
27
+
28
+ export const renderOutputFps = () => timelineFPS / renderSkip
29
+
30
+
19
31
  export enum SpaceSetting {
20
32
  ThreeDim,
21
33
  TwoDim
@@ -23,7 +35,19 @@ export enum SpaceSetting {
23
35
 
24
36
  export enum HotReloadSetting {
25
37
  TraceFromStart,
26
- BeginFromCurrent
38
+ BeginFromCurrent,
39
+ BeginFreshOnSave
40
+ }
41
+
42
+ export const hotreloadNameLookup = (mode: HotReloadSetting) => {
43
+ switch (mode) {
44
+ case HotReloadSetting.TraceFromStart:
45
+ return "Trace from start";
46
+ case HotReloadSetting.BeginFromCurrent:
47
+ return "Begin from current frame without trace";
48
+ case HotReloadSetting.BeginFreshOnSave:
49
+ return "Go to the beginning";
50
+ }
27
51
  }
28
52
 
29
53
  type SceneInstruction = (tick: number) => any
@@ -54,6 +78,8 @@ export class AnimatedScene {
54
78
 
55
79
  playEffectFunction: () => any = () => {}
56
80
 
81
+ renderingEventFunction: (start: boolean) => any = () => {}
82
+
57
83
  isPlaying = false
58
84
 
59
85
  private initialSceneChildren: THREE.Object3D[] = []
@@ -77,6 +103,7 @@ export class AnimatedScene {
77
103
 
78
104
  private buildFunction: (scene: this) => any
79
105
 
106
+ public hotReloadSetting: HotReloadSetting
80
107
  private traceFromStart: boolean
81
108
 
82
109
  private controlsAnimationFrameId: number | null = null
@@ -87,6 +114,9 @@ export class AnimatedScene {
87
114
  private doNotPlayAudio = false
88
115
  private renderingAudioGather: AudioInScene[] = []
89
116
 
117
+ private playbackTargetDistance: number | null = null
118
+
119
+
90
120
  constructor(
91
121
  pixelsWidth: number,
92
122
  pixelsHeight: number,
@@ -97,7 +127,8 @@ export class AnimatedScene {
97
127
  this.container = globalContainerRef
98
128
  this.pixelsHeight = pixelsHeight
99
129
  this.pixelsWidth = pixelsWidth
100
- this.traceFromStart = hotReloadSetting === HotReloadSetting.TraceFromStart
130
+ this.hotReloadSetting = hotReloadSetting
131
+ this.traceFromStart = hotReloadSetting !== HotReloadSetting.BeginFromCurrent
101
132
 
102
133
  const threeDim = spaceSetting === SpaceSetting.ThreeDim
103
134
 
@@ -130,6 +161,9 @@ export class AnimatedScene {
130
161
  this.renderer = renderer
131
162
  this.controls = controls
132
163
 
164
+ // Cap viewer pixel ratio, without this HDRIs become super slow on MacBooks for example
165
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1));
166
+
133
167
  this.startControls()
134
168
 
135
169
  addDestroyFunction(() => this.onDestroy())
@@ -148,7 +182,16 @@ export class AnimatedScene {
148
182
  this.appendInstruction(instruction, this.sceneCalculationTick)
149
183
  }
150
184
 
151
- addAnim(...animations: UserAnimation[]) {
185
+ doAt(tick: number, instruction: SceneInstruction) {
186
+ if (tick < 0) throw new Error('doAt: tick must be ≥ 0')
187
+ this.appendInstruction(instruction, tick)
188
+ }
189
+
190
+ getCurrentTimeMs() {
191
+ return ticksToMillis(this.sceneRenderTick)
192
+ }
193
+
194
+ addAnims(...animations: UserAnimation[]) {
152
195
  const longest = Math.max(...animations.map((a) => a.interpolation.length))
153
196
  for (const animation of animations) {
154
197
  this.appendAnimation(animation)
@@ -156,7 +199,7 @@ export class AnimatedScene {
156
199
  this.sceneCalculationTick += longest
157
200
  }
158
201
 
159
- insertAnimAt(tick: number, ...animations: UserAnimation[]) {
202
+ insertAnimsAt(tick: number, ...animations: UserAnimation[]) {
160
203
  for (const animation of animations) {
161
204
  const internalAnimation: InternalAnimation = {
162
205
  startTick: tick,
@@ -169,6 +212,21 @@ export class AnimatedScene {
169
212
  }
170
213
  }
171
214
 
215
+ addDeferredAnims(...futureAnimations: (() => UserAnimation)[]) {
216
+ // Execute once during planning just to get durations
217
+ const tempAnims = futureAnimations.map(fn => fn())
218
+ const longest = Math.max(...tempAnims.map((a) => a.interpolation.length))
219
+
220
+ this.do((tick) => {
221
+ const calculatedAnimations: UserAnimation[] = []
222
+ for (const futureAnimation of futureAnimations) {
223
+ calculatedAnimations.push(futureAnimation()) // Execute again at runtime
224
+ }
225
+ this.insertAnimsAt(tick, ...calculatedAnimations)
226
+ })
227
+ this.sceneCalculationTick += longest
228
+ }
229
+
172
230
  addSequentialBackgroundAnims(...sequentialAnimations: UserAnimation[]) {
173
231
  let padding = 0
174
232
  for (const animation of sequentialAnimations) {
@@ -182,7 +240,7 @@ export class AnimatedScene {
182
240
  }
183
241
 
184
242
  end() {
185
- this.totalSceneTicks = this.sceneCalculationTick + 1
243
+ this.totalSceneTicks = this.sceneCalculationTick
186
244
  }
187
245
 
188
246
  registerAudio(audioPath: string) {
@@ -221,7 +279,7 @@ export class AnimatedScene {
221
279
  }
222
280
 
223
281
  addWait(duration: number) {
224
- this.addAnim(createAnim(easeConstant(0, duration), () => {}))
282
+ this.addAnims(createAnim(easeConstant(0, duration), () => {}))
225
283
  }
226
284
 
227
285
  async jumpToFrameAtIndex(index: number, notSize: boolean = false) {
@@ -261,24 +319,17 @@ export class AnimatedScene {
261
319
  }
262
320
 
263
321
  private syncControlsWithCamera() {
264
- // Get the direction vector (works for both camera types)
265
- const direction = new THREE.Vector3(0, 0, -1)
322
+ const dir = new THREE.Vector3();
323
+ this.camera.getWorldDirection(dir); // works for both camera types
266
324
 
267
- // Use the appropriate transformation based on camera type
268
- if (this.camera.type === 'OrthographicCamera') {
269
- direction.transformDirection(this.camera.matrixWorld)
270
- } else {
271
- direction.applyQuaternion(this.camera.quaternion)
272
- }
325
+ const distance =
326
+ this.playbackTargetDistance ??
327
+ this.controls.target.distanceTo(this.camera.position);
273
328
 
274
- // Calculate the new target (same for both camera types)
275
- const targetDistance = this.controls.target.distanceTo(this.controls.object.position)
276
- const newTarget = this.camera.position.clone().add(direction.multiplyScalar(targetDistance))
277
- this.controls.target.copy(newTarget)
278
-
279
- // Reset the internal state
280
- this.controls.update()
281
- }
329
+ const newTarget = this.camera.position.clone().add(dir.multiplyScalar(distance));
330
+ this.controls.target.copy(newTarget);
331
+ this.controls.update();
332
+ }
282
333
 
283
334
  private startControls() {
284
335
  this.controls.enabled = true
@@ -317,10 +368,6 @@ export class AnimatedScene {
317
368
 
318
369
  this.renderCurrentFrame()
319
370
  animateCounter++
320
-
321
- if (animateCounter % 10 === 0) {
322
- logCameraState(this.camera)
323
- }
324
371
  }
325
372
  animate()
326
373
  }
@@ -357,12 +404,16 @@ export class AnimatedScene {
357
404
  this.isPlaying = false
358
405
  if (this.animationFrameId) cancelAnimationFrame(this.animationFrameId)
359
406
 
360
- this.syncControlsWithCamera()
361
407
 
362
- this.startControls()
408
+ // use the captured distance one last time
409
+ this.syncControlsWithCamera();
410
+ this.playbackTargetDistance = null;
411
+
412
+ this.startControls();
363
413
  }
364
414
 
365
415
  async render() {
416
+ this.renderingEventFunction(true)
366
417
  this.isRendering = true
367
418
  this.isPlaying = true
368
419
  this.stopControls()
@@ -425,6 +476,7 @@ export class AnimatedScene {
425
476
 
426
477
  this.isPlaying = false
427
478
  this.startControls()
479
+ this.renderingEventFunction(false)
428
480
  }
429
481
 
430
482
  play() {
@@ -435,17 +487,32 @@ export class AnimatedScene {
435
487
  this.isPlaying = true
436
488
  this.stopControls()
437
489
  await this.jumpToFrameAtIndex(fromFrame)
438
- logCameraState(this.camera)
490
+
491
+ // Capture a distance that OrbitControls will keep during play
492
+ this.playbackTargetDistance =
493
+ this.controls.target.distanceTo(this.camera.position)
439
494
 
440
495
  let currentFrame = fromFrame
441
496
  let numberCalledAnimate = 0
442
497
  const animate = async (trace: boolean) => {
443
498
  if (!this.isPlaying) return
444
499
  if (currentFrame <= toFrame) {
445
- if (numberCalledAnimate % animationFPSThrottle === 0) {
500
+ // Still modulus since the requestAnimationFrame runs at the screenFPS rate, not timelineFPS rate
501
+ if (numberCalledAnimate % animationFPSDivider === 0) {
446
502
  this.sceneRenderTick = currentFrame
447
503
  //To not apply trace twice if we just jumped to startframe (and thus tranced it)
448
504
  await this.traceCurrentFrame(this.sceneRenderTick, true, !trace)
505
+
506
+ // --- Keep controls.target aligned with the animated camera ---
507
+ if (this.playbackTargetDistance != null) {
508
+ const camDir = new THREE.Vector3()
509
+ this.camera.getWorldDirection(camDir) // forward (-Z in view space)
510
+ const target = this.camera.position.clone()
511
+ .add(camDir.multiplyScalar(this.playbackTargetDistance))
512
+ this.controls.target.copy(target)
513
+ this.controls.update() // ok to call while disabled; just updates internals
514
+ }
515
+
449
516
  this.renderCurrentFrame()
450
517
  currentFrame++
451
518
  await this.playEffectFunction()
@@ -1,7 +0,0 @@
1
- import { screenFps } from '../../../../entry'
2
-
3
- // Convert ticks (frames) to milliseconds
4
- export const ticksToMillis = (ticks: number) => (ticks / screenFps) * 1000
5
-
6
- // Convert milliseconds to the closest whole number of ticks
7
- export const millisToTicks = (ms: number) => Math.ceil((ms / 1000) * screenFps)