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.
- package/package.json +1 -1
- package/template/package-lock.json +313 -59
- package/template/package.json +1 -0
- package/template/src/entry.ts +3 -4
- package/template/src/example_scenes/alternativesScene.ts +1 -1
- package/template/src/example_scenes/fourierSeriesScene.ts +3 -3
- package/template/src/example_scenes/keyboardScene.ts +11 -11
- package/template/src/example_scenes/tests/animations/test_updater.ts +24 -0
- package/template/src/example_scenes/tests/deferred_anims/testing_deferredAnims.ts +71 -0
- package/template/src/example_scenes/tests/deferred_anims/testing_deferredAnims2.ts +65 -0
- package/template/src/example_scenes/tutorials/easy1.ts +4 -4
- package/template/src/example_scenes/tutorials/easy3.ts +1 -1
- package/template/src/example_scenes/visulizingFunctions.ts +2 -2
- package/template/src/main/index.ts +59 -3
- package/template/src/preload/index.ts +15 -1
- package/template/src/renderer/index.html +1 -1
- package/template/src/renderer/src/App.svelte +178 -23
- package/template/src/renderer/src/application_assets/360.svg +39 -0
- package/template/src/renderer/src/application_assets/move.svg +37 -0
- package/template/src/renderer/src/lib/animation/captureCanvas.ts +2 -2
- package/template/src/renderer/src/lib/animation/interpolations.ts +2 -1
- package/template/src/renderer/src/lib/general/helpers.ts +16 -47
- package/template/src/renderer/src/lib/scene/sceneClass.ts +100 -33
- package/template/src/renderer/src/lib/animation/helpers.ts +0 -7
- package/template/src/scenes/.gitignore +0 -5
package/template/package.json
CHANGED
package/template/src/entry.ts
CHANGED
|
@@ -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
|
|
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()
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
398
|
+
scene.addAnims(typeAnimation(scene, line1, text, typeSpeed))
|
|
399
399
|
scene.addWait(1000)
|
|
400
|
-
scene.
|
|
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.
|
|
406
|
+
scene.addAnims(typeAnimation(scene, line2, text, typeSpeed))
|
|
407
407
|
scene.addWait(1000)
|
|
408
|
-
scene.
|
|
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.
|
|
414
|
+
scene.addAnims(typeAnimation(scene, line3, text, typeSpeed))
|
|
415
415
|
scene.addWait(1000)
|
|
416
|
-
scene.
|
|
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.
|
|
423
|
+
scene.addAnims(typeAnimation(scene, line4, text, typeSpeed))
|
|
424
424
|
scene.addWait(1000)
|
|
425
|
-
scene.
|
|
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.
|
|
431
|
+
scene.addAnims(typeAnimation(scene, line5, text, typeSpeed))
|
|
432
432
|
scene.addWait(1000)
|
|
433
|
-
scene.
|
|
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 "
|
|
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.
|
|
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.
|
|
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 "
|
|
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) => {
|
|
@@ -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.
|
|
160
|
+
scene.addAnims(fadeOut(textNode, 150))
|
|
161
161
|
scene.do(async () => {
|
|
162
162
|
await updateText(textNode, functions[i + 1][0])
|
|
163
163
|
})
|
|
164
|
-
scene.
|
|
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:
|
|
14
|
-
height:
|
|
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
|