create-definedmotion 0.1.3 → 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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/template/package-lock.json +313 -59
  3. package/template/package.json +1 -0
  4. package/template/src/entry.ts +3 -4
  5. package/template/src/example_scenes/alternativesScene.ts +1 -1
  6. package/template/src/example_scenes/fourierSeriesScene.ts +3 -3
  7. package/template/src/example_scenes/keyboardScene.ts +11 -11
  8. package/template/src/example_scenes/tests/animations/test_updater.ts +24 -0
  9. package/template/src/example_scenes/tests/deferred_anims/testing_deferredAnims.ts +71 -0
  10. package/template/src/example_scenes/tests/deferred_anims/testing_deferredAnims2.ts +65 -0
  11. package/template/src/example_scenes/tutorials/easy1.ts +4 -4
  12. package/template/src/example_scenes/tutorials/easy3.ts +1 -1
  13. package/template/src/example_scenes/visulizingFunctions.ts +2 -2
  14. package/template/src/main/index.ts +59 -3
  15. package/template/src/preload/index.ts +15 -1
  16. package/template/src/renderer/index.html +1 -1
  17. package/template/src/renderer/src/App.svelte +178 -23
  18. package/template/src/renderer/src/application_assets/360.svg +39 -0
  19. package/template/src/renderer/src/application_assets/move.svg +37 -0
  20. package/template/src/renderer/src/lib/animation/captureCanvas.ts +2 -2
  21. package/template/src/renderer/src/lib/animation/interpolations.ts +2 -1
  22. package/template/src/renderer/src/lib/general/helpers.ts +16 -47
  23. package/template/src/renderer/src/lib/scene/sceneClass.ts +100 -33
  24. package/template/src/renderer/src/lib/animation/helpers.ts +0 -7
  25. package/template/src/scenes/.gitignore +0 -5
@@ -27,6 +27,7 @@
27
27
  "@tailwindcss/vite": "^4.1.3",
28
28
  "@types/three": "^0.175.0",
29
29
  "alea": "^1.0.1",
30
+ "electron-store": "^8.2.0",
30
31
  "flubber": "^0.4.2",
31
32
  "mathjax": "^3.2.2",
32
33
  "mathjax-full": "^3.2.2",
@@ -11,9 +11,8 @@ import { tutorial_easy2 } from './example_scenes/tutorials/easy2'
11
11
  import { tutorial_medium1 } from './example_scenes/tutorials/medium1'
12
12
  import { tutorial_easy3 } from './example_scenes/tutorials/easy3'
13
13
 
14
- export const screenFps = 120 //Your screen fps
15
- export const renderSkip = 2 //Will divide your screenFps with this for render output fps
16
- export const animationFPSThrottle = 1 // Use to change preview fps, will divide your fps with this value
17
14
 
18
- export const renderOutputFps = () => screenFps / renderSkip
15
+ export const renderSkip = 1 // Must be an integer. Will only render only N:th frame
16
+ export const animationFPSDivider = 1 //Must be an integer. Will change the fundamental animation FPS, how many ticks/frames that are played in a certain time
17
+
19
18
  export const entryScene: () => AnimatedScene = () => tutorial_easy1()
@@ -82,7 +82,7 @@ export const alternativesScene = (): AnimatedScene => {
82
82
  }
83
83
  })
84
84
 
85
- scene.addAnim(switchAnimation)
85
+ scene.addAnims(switchAnimation)
86
86
  }
87
87
  )
88
88
  }
@@ -369,7 +369,7 @@ export const fourierSeriesScene = (): AnimatedScene => {
369
369
 
370
370
  if (mode2 === 2) {
371
371
  for (let i = 0; i < relationGroups.length; i++) {
372
- scene.insertAnimAt(tick, zoomOut(relationGroups[i].latexText, 200))
372
+ scene.insertAnimsAt(tick, zoomOut(relationGroups[i].latexText, 200))
373
373
  }
374
374
  }
375
375
  }
@@ -378,13 +378,13 @@ export const fourierSeriesScene = (): AnimatedScene => {
378
378
 
379
379
  for (let i = 0; i < relationGroups.length; i++) {
380
380
  if (i !== Number(mode)) {
381
- scene.insertAnimAt(
381
+ scene.insertAnimsAt(
382
382
  tick,
383
383
  fade(relationGroups[i].group, 200, relationGroups[i].opacity, 0.1)
384
384
  )
385
385
  relationGroups[i].opacity = 0.1
386
386
  } else {
387
- scene.insertAnimAt(
387
+ scene.insertAnimsAt(
388
388
  tick,
389
389
  fade(relationGroups[i].group, 200, relationGroups[i].opacity, 1)
390
390
  )
@@ -329,7 +329,7 @@ export const keyboardScene = (): AnimatedScene => {
329
329
  )
330
330
  const targetRot = new THREE.Quaternion(-0.6683053, -0.001480137, -0.001329754, 0.7438844)
331
331
 
332
- scene.addAnim(
332
+ scene.addAnims(
333
333
  moveRotateCameraAnimation3D(
334
334
  scene.camera,
335
335
  scene.camera.position,
@@ -395,42 +395,42 @@ export const keyboardScene = (): AnimatedScene => {
395
395
  const deleteSpeed = 30
396
396
 
397
397
  const line1 = 'Hello Instagram!'
398
- scene.addAnim(typeAnimation(scene, line1, text, typeSpeed))
398
+ scene.addAnims(typeAnimation(scene, line1, text, typeSpeed))
399
399
  scene.addWait(1000)
400
- scene.addAnim(
400
+ scene.addAnims(
401
401
  typeAnimation(scene, [...line1].map(() => backCharacter).join(''), text, deleteSpeed)
402
402
  )
403
403
 
404
404
  scene.addWait(300)
405
405
  const line2 = 'I am just testing my programmatic animation library!'
406
- scene.addAnim(typeAnimation(scene, line2, text, typeSpeed))
406
+ scene.addAnims(typeAnimation(scene, line2, text, typeSpeed))
407
407
  scene.addWait(1000)
408
- scene.addAnim(
408
+ scene.addAnims(
409
409
  typeAnimation(scene, [...line2].map(() => backCharacter).join(''), text, deleteSpeed)
410
410
  )
411
411
 
412
412
  scene.addWait(300)
413
413
  const line3 = `It is inspired by 3Blue1Brown's Manim and Motion Canvas. It is meant for technical and mathematical animations!`
414
- scene.addAnim(typeAnimation(scene, line3, text, typeSpeed))
414
+ scene.addAnims(typeAnimation(scene, line3, text, typeSpeed))
415
415
  scene.addWait(1000)
416
- scene.addAnim(
416
+ scene.addAnims(
417
417
  typeAnimation(scene, [...line3].map(() => backCharacter).join(''), text, deleteSpeed)
418
418
  )
419
419
 
420
420
  scene.addWait(300)
421
421
  const line4 =
422
422
  'One of its features is that when you save your code, the animation updates immediately in the viewport. No need to render the video, open the file and then see the result!'
423
- scene.addAnim(typeAnimation(scene, line4, text, typeSpeed))
423
+ scene.addAnims(typeAnimation(scene, line4, text, typeSpeed))
424
424
  scene.addWait(1000)
425
- scene.addAnim(
425
+ scene.addAnims(
426
426
  typeAnimation(scene, [...line4].map(() => backCharacter).join(''), text, deleteSpeed)
427
427
  )
428
428
 
429
429
  scene.addWait(300)
430
430
  const line5 = `Use the project by visiting "DefinedMotion" by Hugo Olsson on GitHub, thanks!`
431
- scene.addAnim(typeAnimation(scene, line5, text, typeSpeed))
431
+ scene.addAnims(typeAnimation(scene, line5, text, typeSpeed))
432
432
  scene.addWait(1000)
433
- scene.addAnim(
433
+ scene.addAnims(
434
434
  typeAnimation(scene, [...line5].map(() => backCharacter).join(''), text, deleteSpeed)
435
435
  )
436
436
 
@@ -0,0 +1,24 @@
1
+ import { easeInOutQuad } from "$renderer/lib/animation/interpolations";
2
+ import { createAnim } from "$renderer/lib/animation/protocols";
3
+ import { createRectangle } from "$renderer/lib/rendering/objects2d";
4
+ import { AnimatedScene, HotReloadSetting, SpaceSetting } from "$renderer/lib/scene/sceneClass";
5
+
6
+
7
+ // Spec: The updater should overwrite the animation, so the square should not move.
8
+
9
+ export const test_updater1 = (): AnimatedScene => {
10
+ return new AnimatedScene(1000, 1000, SpaceSetting.TwoDim, HotReloadSetting.TraceFromStart, async (dm) => {
11
+ const square = createRectangle(4, 4)
12
+ dm.add(square)
13
+
14
+ const moveAnimation = createAnim(easeInOutQuad(0,5, 500), (value) => {
15
+ square.position.x = value
16
+ })
17
+
18
+ moveAnimation.updater = () => {}
19
+
20
+
21
+ dm.addAnims(moveAnimation)
22
+ dm.addAnims(moveAnimation.copy().reverse())
23
+ })
24
+ }
@@ -0,0 +1,71 @@
1
+ // tutorial_deferred_closure_test.ts
2
+ import * as THREE from 'three'
3
+
4
+ import { AnimatedScene, HotReloadSetting, SpaceSetting } from '$renderer/lib/scene/sceneClass'
5
+ import { createRectangle } from '$renderer/lib/rendering/objects2d'
6
+ import { moveRotateCameraAnimation3D } from '$renderer/lib/animation/animations'
7
+ import { easeInOutQuad } from '$renderer/lib/animation/interpolations'
8
+ import { createAnim } from '$renderer/lib/animation/protocols'
9
+
10
+ /**
11
+ * Demo: proves that addDeferredAnims (closure-based) captures runtime state.
12
+ * - You can drag/orbit before pressing Play.
13
+ * - When playback reaches the deferred blocks, the move starts from the *live* camera pose.
14
+ * - Also shows an eager animation (card rocking) that’s precomputed with addAnims.
15
+ */
16
+ export function test_deferred_anims(): AnimatedScene {
17
+ return new AnimatedScene(
18
+ 1080,
19
+ 1920,
20
+ SpaceSetting.ThreeDim,
21
+ HotReloadSetting.TraceFromStart,
22
+ async (scene) => {
23
+ // Visual anchors
24
+ scene.add(new THREE.GridHelper(30, 30))
25
+ const card = createRectangle(6, 4)
26
+ card.position.set(0, 2, 0)
27
+ scene.add(card)
28
+
29
+ // Initial camera (user can move it freely before Play)
30
+ scene.camera.position.set(10, 6, 12)
31
+ scene.camera.lookAt(new THREE.Vector3(0, 1, 0))
32
+
33
+ // Eager anim (pure, precomputable) — keeps the scene alive regardless of camera
34
+ const rock = createAnim(easeInOutQuad(-0.3, 0.3, 180), (v) => (card.rotation.z = v))
35
+ scene.addAnims(rock)
36
+ scene.addAnims(rock.copy().reverse()) // 360 ticks total
37
+ scene.addWait(800)
38
+
39
+ // -------- Deferred camera move #1 (uses your addDeferredAnims signature) --------
40
+ // NOTE: Builders have no args; they close over `scene` to access live camera state.
41
+ scene.addDeferredAnims(() =>
42
+ moveRotateCameraAnimation3D(
43
+ scene.camera,
44
+ scene.camera.position, // captured at runtime
45
+ scene.camera.quaternion, // captured at runtime
46
+ new THREE.Vector3(8, 8, 8),
47
+ new THREE.Quaternion().setFromEuler(new THREE.Euler(-0.6, 0.6, 0)),
48
+ 2000
49
+ )
50
+ )
51
+
52
+ scene.addWait(600)
53
+
54
+ // -------- Deferred camera move #2 (chained) --------
55
+ // Starts from the end pose of move #1, because it’s captured at that runtime tick.
56
+ scene.addDeferredAnims(() =>
57
+ moveRotateCameraAnimation3D(
58
+ scene.camera,
59
+ scene.camera.position,
60
+ scene.camera.quaternion,
61
+ new THREE.Vector3(0, 3, 16),
62
+ new THREE.Quaternion().setFromEuler(new THREE.Euler(0.02, 0, 0)),
63
+ 2000
64
+ )
65
+ )
66
+
67
+ // Tail for export
68
+ scene.addWait(1200)
69
+ }
70
+ )
71
+ }
@@ -0,0 +1,65 @@
1
+ // tutorial_deferred_minimal.ts
2
+ import * as THREE from 'three'
3
+
4
+ import { AnimatedScene, HotReloadSetting, SpaceSetting } from '$renderer/lib/scene/sceneClass'
5
+ import { createRectangle } from '$renderer/lib/rendering/objects2d'
6
+ import { easeInOutQuad } from '$renderer/lib/animation/interpolations'
7
+ import { createAnim } from '$renderer/lib/animation/protocols'
8
+
9
+ /**
10
+ * Minimal proof of deferred usefulness:
11
+ * 1) Eager move: x: -3 → 0 (precomputed)
12
+ * 2) Deferred move: from whatever x is *at runtime* → x + 4
13
+ * 3) Deferred move: from the new live x → 0 (return)
14
+ *
15
+ * If you hot-reload or change earlier timing, the deferred steps still start from
16
+ * the correct, current position because they capture runtime state via closure.
17
+ */
18
+ export function test_deferred_anims2(): AnimatedScene {
19
+ return new AnimatedScene(
20
+ 1000,
21
+ 1000,
22
+ SpaceSetting.TwoDim,
23
+ HotReloadSetting.TraceFromStart,
24
+ async (scene) => {
25
+ // A simple square (centered), easy to see
26
+ const box = createRectangle(4, 4) as THREE.Mesh
27
+ scene.add(box)
28
+
29
+ // Start a bit left so we can see the eager move clearly
30
+ box.position.set(-3, 0, 0)
31
+
32
+ // --- 1) Eager (precomputed) move: -3 → 0 over 600 ticks ---
33
+ const moveToCenter = createAnim(
34
+ easeInOutQuad(-3, 0, 600),
35
+ (x) => (box.position.x = x)
36
+ )
37
+ scene.addAnims(moveToCenter)
38
+
39
+ // Give a short beat
40
+ scene.addWait(300)
41
+
42
+ // --- 2) Deferred move: from *current* x → x + 4 over 400 ticks ---
43
+ // Captures box.position.x at runtime, not planning time.
44
+ scene.addDeferredAnims(() =>
45
+ createAnim(
46
+ easeInOutQuad(box.position.x, box.position.x + 4, 400),
47
+ (x) => (box.position.x = x)
48
+ )
49
+ )
50
+
51
+ scene.addWait(200)
52
+
53
+ // --- 3) Deferred move back: from *current* x → 0 over 400 ticks ---
54
+ scene.addDeferredAnims(() =>
55
+ createAnim(
56
+ easeInOutQuad(box.position.x, 0, 400),
57
+ (x) => (box.position.x = x)
58
+ )
59
+ )
60
+
61
+ // Tail for renders
62
+ scene.addWait(600)
63
+ }
64
+ )
65
+ }
@@ -37,18 +37,18 @@ export function tutorial_easy1(): AnimatedScene {
37
37
  // And give a function that is called for each frame with the current interpolation value
38
38
  const anim = createAnim(easeInOutQuad(-5, 5, 500), (value) => (circle.position.x = value))
39
39
 
40
- // We use "addAnim" to schedule an animation, it will run from the frame (tick) it was added at
40
+ // We use "addAnims" to schedule an animation, it will run from the frame (tick) it was added at
41
41
  // Since this is our first added animation in this scene, we are currently at tick 0, So it will just add to the start.
42
42
  // But say that we are in a complex animation and our previous buildings would mean that we are at frame 49878 for example (we wouldn't know this)
43
43
  // Then it just adds the animation with that offset
44
- scene.addAnim(anim)
44
+ scene.addAnims(anim)
45
45
 
46
46
  // To make the circle also go back, we can reverse the entire animation and add it again
47
47
  // Notice that we are copying it, this is so that the reverse() doesn't affect the original variable "anim"
48
- scene.addAnim(anim.copy().reverse())
48
+ scene.addAnims(anim.copy().reverse())
49
49
 
50
50
  // We now finally add a function that will be called at each frame (tick) in our animation
51
- // This doesn't push the tick forward like the "addAnim" does.
51
+ // This doesn't push the tick forward like the "addAnims" does.
52
52
  // It just declares a function that should be run at each frame
53
53
  // For this animation, we want to set a color to the circle at each frame.
54
54
  scene.onEachTick((tick) => {
@@ -124,7 +124,7 @@ export function tutorial_easy3(): AnimatedScene {
124
124
  })
125
125
 
126
126
  // Start the slide show
127
- scene.addAnim(switcher)
127
+ scene.addAnims(switcher)
128
128
 
129
129
  // Let it run a bit after the last change (nice tail for render/export)
130
130
  scene.addWait(1_000)
@@ -157,11 +157,11 @@ export const functionsAnimation = (): AnimatedScene => {
157
157
  scene.addSequentialBackgroundAnims(
158
158
  morphAnimation(plotLine, vecFuncs[i], vecFuncs[i + 1], 300)
159
159
  )
160
- scene.addAnim(fadeOut(textNode, 150))
160
+ scene.addAnims(fadeOut(textNode, 150))
161
161
  scene.do(async () => {
162
162
  await updateText(textNode, functions[i + 1][0])
163
163
  })
164
- scene.addAnim(fadeIn(textNode, 150))
164
+ scene.addAnims(fadeIn(textNode, 150))
165
165
  scene.addWait(800)
166
166
  }
167
167
 
@@ -1,18 +1,41 @@
1
- import { app, shell, BrowserWindow, ipcMain } from 'electron'
1
+ import { app, shell, BrowserWindow, ipcMain, nativeTheme, screen } from 'electron'
2
2
  import { join } from 'path'
3
3
  import { electronApp, optimizer, is } from '@electron-toolkit/utils'
4
4
  import icon from '../../resources/icon.png?asset'
5
5
  import { renderVideo } from './rendering'
6
6
  import { deleteRenderedContent } from './storage'
7
+ import ElectronStore from 'electron-store'
8
+
9
+ const store = new ElectronStore()
10
+
11
+ // Force light mode
12
+ nativeTheme.themeSource = 'light'
7
13
 
8
14
  let mainWindow: BrowserWindow
9
15
 
16
+ function getHzForWebContents(wc: Electron.WebContents): number {
17
+ const win = BrowserWindow.fromWebContents(wc)
18
+ if (win) {
19
+ const b = win.getBounds()
20
+ const nearest = screen.getDisplayNearestPoint({ x: b.x, y: b.y })
21
+ return nearest.displayFrequency || 60
22
+ }
23
+ // Fallback to primary display
24
+ return screen.getPrimaryDisplay().displayFrequency || 60
25
+ }
26
+
10
27
  function createWindow(): void {
11
28
  // Create the browser window.
29
+
30
+ const defaultBounds = { width: 1000, height: 1300 }
31
+ const savedBounds: any = store.get('windowBounds', defaultBounds)
12
32
  mainWindow = new BrowserWindow({
13
- width: 1000,
14
- height: 1300,
33
+ width: savedBounds.width,
34
+ height: savedBounds.height,
35
+ x: savedBounds.x,
36
+ y: savedBounds.y,
15
37
  show: false,
38
+ title: "DefinedMotion",
16
39
  autoHideMenuBar: true,
17
40
  ...(process.platform === 'linux' ? { icon } : {}),
18
41
  webPreferences: {
@@ -26,6 +49,14 @@ function createWindow(): void {
26
49
  }
27
50
  })
28
51
 
52
+ mainWindow.on('resize', () => {
53
+ store.set('windowBounds', mainWindow.getBounds())
54
+ })
55
+
56
+ mainWindow.on('move', () => {
57
+ store.set('windowBounds', mainWindow.getBounds())
58
+ })
59
+
29
60
  mainWindow.on('ready-to-show', () => {
30
61
  mainWindow.show()
31
62
  })
@@ -75,6 +106,31 @@ app.whenReady().then(() => {
75
106
  // dock icon is clicked and there are no other windows open.
76
107
  if (BrowserWindow.getAllWindows().length === 0) createWindow()
77
108
  })
109
+
110
+ // RPC: renderer asks main for the current display refresh rate (Hz)
111
+ ipcMain.handle('get-display-hz', (event) => {
112
+ return getHzForWebContents(event.sender)
113
+ })
114
+
115
+ function broadcastHzToAllWindows() {
116
+ for (const w of BrowserWindow.getAllWindows()) {
117
+ const hz = getHzForWebContents(w.webContents)
118
+ w.webContents.send('display-hz-changed', hz)
119
+ }
120
+ }
121
+
122
+ // Display geometry / metrics changed (resolution/scale/mode changes, some moves)
123
+ screen.on('display-metrics-changed', () => {
124
+ broadcastHzToAllWindows()
125
+ })
126
+
127
+ // Displays added/removed (dock/undock, hot-plug)
128
+ screen.on('display-added', () => {
129
+ broadcastHzToAllWindows()
130
+ })
131
+ screen.on('display-removed', () => {
132
+ broadcastHzToAllWindows()
133
+ })
78
134
  })
79
135
 
80
136
  ipcMain.handle('start-video-render', async (event, options) => {
@@ -2,8 +2,22 @@ import { contextBridge, ipcRenderer } from 'electron'
2
2
  import { electronAPI } from '@electron-toolkit/preload'
3
3
  import { RenderOptions } from '../main/rendering'
4
4
 
5
+
6
+ // ---- NEW helpers for event subscription
7
+ function onDisplayHzChanged(cb: (hz: number) => void) {
8
+ const channel = 'display-hz-changed'
9
+ const listener = (_: unknown, hz: number) => cb(hz)
10
+ ipcRenderer.on(channel, listener)
11
+ // return an unsubscribe fn
12
+ return () => ipcRenderer.removeListener(channel, listener)
13
+ }
14
+
5
15
  const customAPI = {
6
- startVideoRender: (options: RenderOptions) => ipcRenderer.invoke('start-video-render', options)
16
+ startVideoRender: (options: RenderOptions) => ipcRenderer.invoke('start-video-render', options),
17
+
18
+ getDisplayHz: (): Promise<number> => ipcRenderer.invoke('get-display-hz'),
19
+
20
+ onDisplayHzChanged
7
21
  }
8
22
 
9
23
  // Custom APIs for renderer
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <title>Electron</title>
5
+ <title>DefinedMotion</title>
6
6
  <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
7
7
  <!-- <meta
8
8
  http-equiv="Content-Security-Policy"