create-definedmotion 0.1.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 (98) hide show
  1. package/bin/index.js +3 -0
  2. package/package.json +37 -0
  3. package/src/cli.js +100 -0
  4. package/template/.editorconfig +9 -0
  5. package/template/.prettierignore +6 -0
  6. package/template/.prettierrc.yaml +10 -0
  7. package/template/_gitignore +10 -0
  8. package/template/build/entitlements.mac.plist +12 -0
  9. package/template/build/icon.icns +0 -0
  10. package/template/build/icon.ico +0 -0
  11. package/template/build/icon.png +0 -0
  12. package/template/electron-builder.yml +43 -0
  13. package/template/electron.vite.config.ts +50 -0
  14. package/template/eslint.config.mjs +24 -0
  15. package/template/package-lock.json +10299 -0
  16. package/template/package.json +64 -0
  17. package/template/resources/icon.png +0 -0
  18. package/template/src/assets/audio/fadeSound.mp3 +0 -0
  19. package/template/src/assets/audio/fadeSound2.mp3 +0 -0
  20. package/template/src/assets/audio/interstellar.mp3 +0 -0
  21. package/template/src/assets/audio/keyboard1.mp3 +0 -0
  22. package/template/src/assets/audio/keyboard2.mp3 +0 -0
  23. package/template/src/assets/audio/keyboard3.mp3 +0 -0
  24. package/template/src/assets/audio/tick_sound.mp3 +0 -0
  25. package/template/src/assets/base.css +67 -0
  26. package/template/src/assets/electron.svg +10 -0
  27. package/template/src/assets/fonts/Geo-Regular.woff +0 -0
  28. package/template/src/assets/fonts/Montserrat-Italic-VariableFont_wght.woff2 +0 -0
  29. package/template/src/assets/fonts/Montserrat-Medium.ttf +0 -0
  30. package/template/src/assets/fonts/Montserrat-Medium.woff +0 -0
  31. package/template/src/assets/fonts/Montserrat-VariableFont_wght.woff2 +0 -0
  32. package/template/src/assets/fonts/glitch.ttf +0 -0
  33. package/template/src/assets/hdri/indoor1.hdr +0 -0
  34. package/template/src/assets/hdri/metro1.hdr +0 -0
  35. package/template/src/assets/hdri/outdoor1.hdr +0 -0
  36. package/template/src/assets/hdri/photo-studio1.hdr +0 -0
  37. package/template/src/assets/hdri/photo-studio2.hdr +0 -0
  38. package/template/src/assets/hdri/photo-studio3.hdr +0 -0
  39. package/template/src/assets/objects/keyboardScene/ibm-keyboard.glb +0 -0
  40. package/template/src/assets/wavy-lines.svg +25 -0
  41. package/template/src/entry.ts +20 -0
  42. package/template/src/example_scenes/alternativesScene.ts +88 -0
  43. package/template/src/example_scenes/dependencyScene.ts +116 -0
  44. package/template/src/example_scenes/fourierMachineScene.ts +108 -0
  45. package/template/src/example_scenes/fourierSeriesScene.ts +678 -0
  46. package/template/src/example_scenes/keyboardScene.ts +447 -0
  47. package/template/src/example_scenes/surfaceScene.ts +88 -0
  48. package/template/src/example_scenes/tutorials/easy1.ts +59 -0
  49. package/template/src/example_scenes/tutorials/easy2.ts +141 -0
  50. package/template/src/example_scenes/tutorials/easy3.ts +133 -0
  51. package/template/src/example_scenes/tutorials/medium1.ts +154 -0
  52. package/template/src/example_scenes/vectorField.ts +209 -0
  53. package/template/src/example_scenes/visulizingFunctions.ts +246 -0
  54. package/template/src/main/index.ts +101 -0
  55. package/template/src/main/rendering.ts +219 -0
  56. package/template/src/main/storage.ts +35 -0
  57. package/template/src/preload/index.d.ts +8 -0
  58. package/template/src/preload/index.ts +36 -0
  59. package/template/src/renderer/index.html +17 -0
  60. package/template/src/renderer/src/App.svelte +130 -0
  61. package/template/src/renderer/src/app.css +24 -0
  62. package/template/src/renderer/src/env.d.ts +2 -0
  63. package/template/src/renderer/src/lib/animation/animations.ts +214 -0
  64. package/template/src/renderer/src/lib/animation/captureCanvas.ts +85 -0
  65. package/template/src/renderer/src/lib/animation/helpers.ts +7 -0
  66. package/template/src/renderer/src/lib/animation/interpolations.ts +155 -0
  67. package/template/src/renderer/src/lib/animation/protocols.ts +79 -0
  68. package/template/src/renderer/src/lib/audio/loader.ts +104 -0
  69. package/template/src/renderer/src/lib/fonts/Roboto_Regular.json +1 -0
  70. package/template/src/renderer/src/lib/fonts/montserrat-medium.json +1 -0
  71. package/template/src/renderer/src/lib/fonts/montserrat.json +1 -0
  72. package/template/src/renderer/src/lib/general/helpers.ts +77 -0
  73. package/template/src/renderer/src/lib/general/onDestory.ts +10 -0
  74. package/template/src/renderer/src/lib/mathHelpers/vectors.ts +18 -0
  75. package/template/src/renderer/src/lib/rendering/bumpMaps/noise.ts +84 -0
  76. package/template/src/renderer/src/lib/rendering/helpers.ts +35 -0
  77. package/template/src/renderer/src/lib/rendering/lighting3d.ts +387 -0
  78. package/template/src/renderer/src/lib/rendering/materials.ts +6 -0
  79. package/template/src/renderer/src/lib/rendering/objects/import.ts +148 -0
  80. package/template/src/renderer/src/lib/rendering/objects2d.ts +489 -0
  81. package/template/src/renderer/src/lib/rendering/objects3d.ts +89 -0
  82. package/template/src/renderer/src/lib/rendering/protocols.ts +21 -0
  83. package/template/src/renderer/src/lib/rendering/setup.ts +71 -0
  84. package/template/src/renderer/src/lib/rendering/svg/drawing.ts +213 -0
  85. package/template/src/renderer/src/lib/rendering/svg/parsing.ts +717 -0
  86. package/template/src/renderer/src/lib/rendering/svg/rastered.ts +42 -0
  87. package/template/src/renderer/src/lib/rendering/svgObjects.ts +1137 -0
  88. package/template/src/renderer/src/lib/scene/helpers.ts +89 -0
  89. package/template/src/renderer/src/lib/scene/sceneClass.ts +648 -0
  90. package/template/src/renderer/src/lib/shaders/background_gradient/frag.glsl +12 -0
  91. package/template/src/renderer/src/lib/shaders/background_gradient/vert.glsl +6 -0
  92. package/template/src/renderer/src/lib/shaders/hdri_blur/frag.glsl +45 -0
  93. package/template/src/renderer/src/lib/shaders/hdri_blur/vert.glsl +5 -0
  94. package/template/src/renderer/src/main.ts +9 -0
  95. package/template/svelte.config.mjs +7 -0
  96. package/template/tsconfig.json +4 -0
  97. package/template/tsconfig.node.json +10 -0
  98. package/template/tsconfig.web.json +32 -0
@@ -0,0 +1,246 @@
1
+ import { COLORS } from '../renderer/src/lib/rendering/helpers'
2
+ import {
3
+ addBackgroundGradient,
4
+ addHDRI,
5
+ addSceneLighting,
6
+ HDRIs,
7
+ loadHDRIData
8
+ } from '../renderer/src/lib/rendering/lighting3d'
9
+ import { AnimatedScene, HotReloadSetting, SpaceSetting } from '../renderer/src/lib/scene/sceneClass'
10
+ import * as THREE from 'three'
11
+ import { MeshLine, MeshLineMaterial } from 'three.meshline'
12
+ import { linspace } from '../renderer/src/lib/mathHelpers/vectors'
13
+ import { createAnim, UserAnimation } from '../renderer/src/lib/animation/protocols'
14
+ import { easeInOutQuad } from '../renderer/src/lib/animation/interpolations'
15
+ import {
16
+ fadeIn,
17
+ fadeOut,
18
+ moveCameraAnimation3D,
19
+ setOpacity
20
+ } from '../renderer/src/lib/animation/animations'
21
+ import { createFastText, updateText } from '../renderer/src/lib/rendering/objects2d'
22
+
23
+ const functions: [string, (x: number) => number][] = [
24
+ // ──────────── Start Simple & Recognizable ────────────
25
+ ['Constant', (_) => 0],
26
+ ['Linear', (x) => x],
27
+ ['Exponential', (x) => Math.exp(x / 3.3)],
28
+ ['Cubic', (x) => Math.pow(x / 10, 3) * 10],
29
+ ['Absolute Value', (x) => Math.abs(x)],
30
+ ['Square Root of |x|', (x) => Math.sqrt(Math.abs(x))],
31
+
32
+ // ──────────── Introduce Waves & Periodicity ────────────
33
+ ['Sine', (x) => Math.sin(x) * 5],
34
+ ['Cosine', (x) => Math.cos(x) * 5],
35
+ ['Chirp', (x) => Math.sin(0.5 * x * x) * 5],
36
+ ['Sawtooth Wave', (x) => 2 * (x / Math.PI - Math.floor(x / Math.PI + 0.5)) * 5],
37
+ [
38
+ 'Triangle Wave',
39
+ (x) => (2 * Math.abs(2 * (x / Math.PI - Math.floor(x / Math.PI + 0.5))) - 1) * 5
40
+ ],
41
+ ['Square Wave', (x) => Math.sign(Math.sin(x)) * 10],
42
+
43
+ // ──────────── Smooth Curves & Transitions ────────────
44
+ ['Gaussian Curve', (x) => Math.exp(-0.5 * x * x) * 10],
45
+ ['ReLU', (x) => Math.max(0, x)],
46
+ ['Sigmoid', (x) => (1 / (1 + Math.exp(-x))) * 10],
47
+ ['Damped Sine Wave', (x) => Math.sin(x) * Math.exp(-0.05 * x * x) * 10],
48
+
49
+ // ──────────── Chaotic/Unpredictable Climax ────────────
50
+ ['Chaotic Oscillator', (x) => Math.sin(10 * x) * Math.sin(0.3 * x * x) * 5]
51
+ ]
52
+ const vectorizeFunctions = (
53
+ functions: [string, (x: number) => number][],
54
+ threshold: number = Infinity
55
+ ): [number, number][][] => {
56
+ return functions.map(([label, fn]) => {
57
+ const xs = linspace(-10, 10, 200)
58
+ return xs.map((x) => {
59
+ const raw = fn(x)
60
+ // zero out anything non‐finite or exceeding threshold
61
+ const y = Number.isFinite(raw) && Math.abs(raw) <= threshold ? raw : 0
62
+ return [x, y] as [number, number]
63
+ })
64
+ })
65
+ }
66
+
67
+ const interpolate = (
68
+ vectorFunc1: [number, number][],
69
+ vectorFunc2: [number, number][],
70
+ progress: number
71
+ ): [number, number][] => {
72
+ if (vectorFunc1.length !== vectorFunc2.length) {
73
+ throw new Error('Both point‑lists must have the same length for interpolation.')
74
+ }
75
+
76
+ // clamp progress into [0,1]
77
+ const t = Math.max(0, Math.min(1, progress))
78
+
79
+ return vectorFunc1.map(([x1, y1], i) => {
80
+ const [x2, y2] = vectorFunc2[i]
81
+ // interpolate x and y
82
+ const x = x1 + (x2 - x1) * t
83
+ const y = y1 + (y2 - y1) * t
84
+ return [x, y] as [number, number]
85
+ })
86
+ }
87
+
88
+ const morphAnimation = (
89
+ line: PlotLine,
90
+ vecFunc1: [number, number][],
91
+ vecFunc2: [number, number][],
92
+ duration: number = 500
93
+ ) => {
94
+ return createAnim(easeInOutQuad(0, 1, duration), (value) => {
95
+ line.setPoints(interpolate(vecFunc1, vecFunc2, value))
96
+ })
97
+ }
98
+ const hdriData = await loadHDRIData(HDRIs.outdoor1, 1, 1)
99
+
100
+ export const functionsAnimation = (): AnimatedScene => {
101
+ return new AnimatedScene(
102
+ 1080,
103
+ 2160,
104
+ SpaceSetting.ThreeDim,
105
+ HotReloadSetting.TraceFromStart,
106
+ async (scene) => {
107
+ // scene.registerAudio(fadeSound)
108
+ scene.add(
109
+ create2DAxis({ xmin: -10, xmax: 10, ymin: -10, ymax: 10, ticks: 10, tickSize: 0.5 })
110
+ )
111
+ addSceneLighting(scene.scene)
112
+
113
+ await addHDRI(scene, hdriData, 1)
114
+ addBackgroundGradient({
115
+ scene,
116
+ topColor: '#00639d',
117
+ bottomColor: COLORS.black,
118
+ backgroundOpacity: 0.5
119
+ })
120
+ scene.camera.position.set(0, 0, 10)
121
+ const informationTextNode = await createFastText('Some of the functions are scaled.', 0.6)
122
+ informationTextNode.position.y = 13.5
123
+ setOpacity(informationTextNode, 0.4)
124
+ scene.add(informationTextNode)
125
+ const textNode = await createFastText('', 1.5)
126
+ textNode.position.y = 12
127
+ scene.add(textNode)
128
+
129
+ const plotLine = new PlotLine(scene.scene, 0xffffff)
130
+
131
+ const vecFuncs = vectorizeFunctions(functions)
132
+
133
+ scene.camera.position.set(9.82015, 4.347805, 29.81918)
134
+ scene.camera.quaternion.set(-0.07633829, 0.3762715, 0.03112576, 0.9228345)
135
+ scene.camera.zoom = 0.8
136
+
137
+ const moveAnimation = moveCameraAnimation3D(
138
+ scene.camera,
139
+ scene.camera.position.clone(),
140
+ new THREE.Vector3(-9.625222, 4.32878, 29.57185),
141
+ 2500
142
+ )
143
+
144
+ scene.onEachTick(() => {
145
+ scene.camera.lookAt(0, 0, -2)
146
+ })
147
+
148
+ scene.addSequentialBackgroundAnims(
149
+ ...Array(20)
150
+ .fill(0)
151
+ .flatMap(() => [moveAnimation, moveAnimation.copy().reverse()])
152
+ )
153
+
154
+ for (let i = 0; i < vecFuncs.length - 1; i++) {
155
+ // scene.playAudio(fadeSound, 0.03)
156
+
157
+ scene.addSequentialBackgroundAnims(
158
+ morphAnimation(plotLine, vecFuncs[i], vecFuncs[i + 1], 300)
159
+ )
160
+ scene.addAnim(fadeOut(textNode, 150))
161
+ scene.do(async () => {
162
+ await updateText(textNode, functions[i + 1][0])
163
+ })
164
+ scene.addAnim(fadeIn(textNode, 150))
165
+ scene.addWait(800)
166
+ }
167
+
168
+ scene.addWait(1600)
169
+ }
170
+ )
171
+ }
172
+
173
+ class PlotLine {
174
+ private meshLine: MeshLine
175
+ private mesh: THREE.Mesh
176
+
177
+ constructor(scene: THREE.Scene, color: THREE.ColorRepresentation) {
178
+ // create MeshLine + material + mesh
179
+ this.meshLine = new MeshLine()
180
+ const material = new MeshLineMaterial({
181
+ lineWidth: 0.1,
182
+ color,
183
+ transparent: false
184
+ })
185
+ this.mesh = new THREE.Mesh(this.meshLine.geometry, material)
186
+ scene.add(this.mesh)
187
+ }
188
+
189
+ setPoints(points: [number, number][]) {
190
+ // flatten into [x0, y0, z0, x1, y1, z1, …]
191
+ const flatPoints: number[] = []
192
+ for (const [x, y] of points) {
193
+ flatPoints.push(x, y, 0)
194
+ }
195
+ // false = don't close the loop
196
+ this.meshLine.setPoints(flatPoints, false)
197
+ }
198
+ }
199
+
200
+ export function create2DAxis({
201
+ xmin = -10,
202
+ xmax = 10,
203
+ ymin = -10,
204
+ ymax = 10,
205
+ ticks = 10,
206
+ tickSize = 0.2
207
+ } = {}) {
208
+ const group = new THREE.Group()
209
+ const mat = new THREE.LineBasicMaterial({ color: 0xffffff })
210
+
211
+ // main X axis
212
+ let geo = new THREE.BufferGeometry().setFromPoints([
213
+ new THREE.Vector3(xmin, 0, 0),
214
+ new THREE.Vector3(xmax, 0, 0)
215
+ ])
216
+ group.add(new THREE.Line(geo, mat))
217
+
218
+ // main Y axis
219
+ geo = new THREE.BufferGeometry().setFromPoints([
220
+ new THREE.Vector3(0, ymin, 0),
221
+ new THREE.Vector3(0, ymax, 0)
222
+ ])
223
+ group.add(new THREE.Line(geo, mat))
224
+
225
+ // tick marks
226
+ const dx = (xmax - xmin) / ticks
227
+ for (let i = 0; i <= ticks; i++) {
228
+ const x = xmin + i * dx
229
+ const tgeo = new THREE.BufferGeometry().setFromPoints([
230
+ new THREE.Vector3(x, -tickSize / 2, 0),
231
+ new THREE.Vector3(x, tickSize / 2, 0)
232
+ ])
233
+ group.add(new THREE.Line(tgeo, mat))
234
+ }
235
+ const dy = (ymax - ymin) / ticks
236
+ for (let j = 0; j <= ticks; j++) {
237
+ const y = ymin + j * dy
238
+ const tgeo = new THREE.BufferGeometry().setFromPoints([
239
+ new THREE.Vector3(-tickSize / 2, y, 0),
240
+ new THREE.Vector3(tickSize / 2, y, 0)
241
+ ])
242
+ group.add(new THREE.Line(tgeo, mat))
243
+ }
244
+
245
+ return group
246
+ }
@@ -0,0 +1,101 @@
1
+ import { app, shell, BrowserWindow, ipcMain } from 'electron'
2
+ import { join } from 'path'
3
+ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
4
+ import icon from '../../resources/icon.png?asset'
5
+ import { renderVideo } from './rendering'
6
+ import { deleteRenderedContent } from './storage'
7
+
8
+ let mainWindow: BrowserWindow
9
+
10
+ function createWindow(): void {
11
+ // Create the browser window.
12
+ mainWindow = new BrowserWindow({
13
+ width: 1000,
14
+ height: 1300,
15
+ show: false,
16
+ autoHideMenuBar: true,
17
+ ...(process.platform === 'linux' ? { icon } : {}),
18
+ webPreferences: {
19
+ nodeIntegration: true,
20
+ contextIsolation: false,
21
+ nodeIntegrationInWorker: true,
22
+ webSecurity: true,
23
+ allowRunningInsecureContent: false,
24
+ preload: join(__dirname, '../preload/index.js'),
25
+ sandbox: false
26
+ }
27
+ })
28
+
29
+ mainWindow.on('ready-to-show', () => {
30
+ mainWindow.show()
31
+ })
32
+
33
+ mainWindow.webContents.setWindowOpenHandler((details) => {
34
+ shell.openExternal(details.url)
35
+ return { action: 'deny' }
36
+ })
37
+
38
+ // HMR for renderer base on electron-vite cli.
39
+ // Load the remote URL for development or the local html file for production.
40
+ if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
41
+ mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
42
+ } else {
43
+ mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
44
+ }
45
+ }
46
+
47
+ // This method will be called when Electron has finished
48
+ // initialization and is ready to create browser windows.
49
+ // Some APIs can only be used after this event occurs.
50
+ app.whenReady().then(() => {
51
+ // Set app user model id for windows
52
+ electronApp.setAppUserModelId('com.electron')
53
+
54
+ // Default open or close DevTools by F12 in development
55
+ // and ignore CommandOrControl + R in production.
56
+ // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
57
+ app.on('browser-window-created', (_, window) => {
58
+ optimizer.watchWindowShortcuts(window)
59
+ })
60
+
61
+ // IPC test
62
+ ipcMain.on('ping', () => console.log('pong'))
63
+
64
+ // Listen for resize requests from the renderer
65
+ ipcMain.on('resize-window', (event, { width, height }) => {
66
+ if (mainWindow) {
67
+ mainWindow.setSize(width, height)
68
+ }
69
+ })
70
+
71
+ createWindow()
72
+
73
+ app.on('activate', function () {
74
+ // On macOS it's common to re-create a window in the app when the
75
+ // dock icon is clicked and there are no other windows open.
76
+ if (BrowserWindow.getAllWindows().length === 0) createWindow()
77
+ })
78
+ })
79
+
80
+ ipcMain.handle('start-video-render', async (event, options) => {
81
+ try {
82
+ const outputFile = await renderVideo(options)
83
+ return { success: true, outputFile }
84
+ } catch (error: any) {
85
+ return { success: false, error: error.message }
86
+ }
87
+ })
88
+
89
+ deleteRenderedContent()
90
+
91
+ // Quit when all windows are closed, except on macOS. There, it's common
92
+ // for applications and their menu bar to stay active until the user quits
93
+ // explicitly with Cmd + Q.
94
+ app.on('window-all-closed', () => {
95
+ if (process.platform !== 'darwin') {
96
+ app.quit()
97
+ }
98
+ })
99
+
100
+ // In this file you can include the rest of your app's specific main process
101
+ // code. You can also put them in separate files and require them here.
@@ -0,0 +1,219 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { spawnSync } from 'child_process'
4
+
5
+ export interface AudioInScene {
6
+ audioPath: string
7
+ volume: number
8
+ atFrame: number
9
+ }
10
+
11
+ export interface RenderOptions {
12
+ fps: number
13
+ width: number
14
+ height: number
15
+ renderingAudioGather: AudioInScene[]
16
+ }
17
+
18
+ export const generateID = (numCharacters: number = 10) =>
19
+ Math.random().toString(numCharacters).substr(2, 9)
20
+
21
+ /**
22
+ * Renders a video from image frames found in the latest "render" directory.
23
+ * @param options - Configuration options for rendering.
24
+ * @param options.fps - Frames per second to use (default is 30).
25
+ * @returns A promise that resolves to the output file path.
26
+ */
27
+ export function renderVideo(options: RenderOptions): Promise<string> {
28
+ return new Promise((resolve, reject) => {
29
+ try {
30
+ console.log(`Converting frames to video at ${options.fps} fps`)
31
+
32
+ // console.log('THE AUDIO DATA IS', options.renderingAudioGather)
33
+
34
+ // Define directories
35
+ const rootDir = './image_renders'
36
+ const audioRendersDir = './audio_renders'
37
+ const outputDir = './rendered_videos'
38
+
39
+ // Ensure output directory exists.
40
+ if (!fs.existsSync(outputDir)) {
41
+ fs.mkdirSync(outputDir, { recursive: true })
42
+ }
43
+
44
+ // Ensure output directory exists.
45
+ if (!fs.existsSync(audioRendersDir)) {
46
+ fs.mkdirSync(audioRendersDir, { recursive: true })
47
+ }
48
+ const audioID = generateID(10)
49
+ const includeAudio = options.renderingAudioGather.length > 0
50
+ if (includeAudio) {
51
+ const audioCommand = buildAudioMixCommand(options, audioRendersDir, audioID)
52
+
53
+ console.log(`Building audio...`)
54
+ const audioResult = spawnSync(audioCommand, { stdio: 'inherit', shell: true })
55
+ if (audioResult.status !== 0) {
56
+ return reject(new Error('FFmpeg command failed'))
57
+ }
58
+ }
59
+
60
+ // Find the latest render directory.
61
+ const latestDir = findLatestDir(rootDir)
62
+ const dirName = path.basename(latestDir)
63
+ console.log(`Processing directory: ${dirName}`)
64
+
65
+ // Define the frame pattern and output file path.
66
+ const framePattern = path.join(latestDir, 'frame_%05d.jpeg')
67
+ const outputFile = path.join(outputDir, `${dirName}.mp4`)
68
+
69
+ // Start building the ffmpeg arguments array.
70
+ const ffmpegArgs = [
71
+ '-y', // Overwrite output if it exists.
72
+ '-framerate',
73
+ options.fps.toString(), // Frame rate for the video.
74
+ '-i',
75
+ framePattern // Input frames pattern.
76
+ ]
77
+
78
+ // Conditionally add the audio input if needed.
79
+ if (includeAudio) {
80
+ ffmpegArgs.push(
81
+ '-i',
82
+ `${audioRendersDir}/${audioID}.mp3` // Input audio file.
83
+ )
84
+ }
85
+
86
+ // Common encoding parameters for video.
87
+ ffmpegArgs.push(
88
+ '-c:v',
89
+ 'libx264', // Video codec.
90
+ '-pix_fmt',
91
+ 'yuv420p' // Pixel format.
92
+ )
93
+
94
+ // Conditionally add audio codec settings only if audio is included.
95
+ if (includeAudio) {
96
+ ffmpegArgs.push(
97
+ '-c:a',
98
+ 'aac', // Audio codec.
99
+ '-af',
100
+ 'apad', // <-- pad audio with silence
101
+ '-shortest' // <-- cut output to shortest input (i.e. video)
102
+ )
103
+ }
104
+
105
+ // Additional encoding options for both scenarios.
106
+ ffmpegArgs.push(
107
+ '-preset',
108
+ 'fast', // Preset for encoding speed/quality.
109
+ '-crf',
110
+ '23', // Quality/size balance.
111
+ outputFile // Output file.
112
+ )
113
+
114
+ console.log('Executing FFmpeg command:')
115
+ console.log(`ffmpeg ${ffmpegArgs.join(' ')}`)
116
+
117
+ // Execute FFmpeg synchronously.
118
+ const result = spawnSync('ffmpeg', ffmpegArgs, { stdio: 'inherit' })
119
+ if (result.status !== 0) {
120
+ return reject(new Error('FFmpeg command failed'))
121
+ }
122
+
123
+ console.log(`Video created successfully: ${outputFile}`)
124
+
125
+ // Delete the render directory with all its images.
126
+ fs.rmSync(latestDir, { recursive: true, force: true })
127
+ fs.readdirSync(audioRendersDir).forEach((item) => {
128
+ const itemPath = path.join(audioRendersDir, item)
129
+ fs.rmSync(itemPath, { recursive: true, force: true })
130
+ })
131
+ console.log(`Deleted render folder: ${latestDir}`)
132
+
133
+ resolve(outputFile)
134
+ } catch (err) {
135
+ reject(err)
136
+ }
137
+ })
138
+ }
139
+ /*
140
+ function generateFFmpegAudioCommand(
141
+ options: RenderOptions,
142
+ outputFolder: string,
143
+ id: string
144
+ ): string {
145
+ const inputs: string[] = []
146
+ const filterChains: string[] = []
147
+ const amixInputs: string[] = []
148
+
149
+ options.renderingAudioGather.forEach((audio, index) => {
150
+ const startTime = audio.atFrame / options.fps
151
+ inputs.push(`-i ./src/renderer${audio.audioPath}`)
152
+ filterChains.push(`[${index}:a]asetpts=PTS+${startTime}/TB,volume=${audio.volume}[a${index}]`)
153
+ amixInputs.push(`[a${index}]`)
154
+ })
155
+
156
+ const amixFilter = `${amixInputs.join('')}amix=inputs=${amixInputs.length}:duration=longest[aout]`
157
+ const filterComplex = [...filterChains, amixFilter].join('; ')
158
+
159
+ return `ffmpeg ${inputs.join(' ')} -filter_complex "${filterComplex}" -map "[aout]" -c:a libmp3lame -q:a 2 ${outputFolder}/${id}.mp3`
160
+ }*/
161
+
162
+ function buildAudioMixCommand(renderOptions: RenderOptions, outputFolder: string, id: string) {
163
+ const { fps, renderingAudioGather } = renderOptions
164
+ let inputs: string[] = []
165
+ let filters: string[] = []
166
+ let inputIndexes: string[] = []
167
+
168
+ renderingAudioGather.forEach((audio, index) => {
169
+ inputs.push(`-i ./src/renderer${audio.audioPath}`)
170
+ const delayMs = Math.floor((audio.atFrame / fps) * 1000)
171
+ // adelay syntax: "adelay=delay_in_ms|delay_in_ms"
172
+ filters.push(`[${index}:a]adelay=${delayMs}|${delayMs},volume=${audio.volume}[a${index}]`)
173
+ inputIndexes.push(`[a${index}]`)
174
+ })
175
+
176
+ const filterComplex =
177
+ filters.join('; ') +
178
+ '; ' +
179
+ inputIndexes.join('') +
180
+ `amix=inputs=${renderingAudioGather.length}:duration=longest:normalize=0[out]`
181
+
182
+ // Adding -hide_banner and -loglevel error to reduce terminal output.
183
+ const command = `ffmpeg -hide_banner -loglevel error ${inputs.join(' ')} -filter_complex "${filterComplex}" -map "[out]" ${outputFolder}/${id}.mp3`
184
+ return command
185
+ }
186
+
187
+ /**
188
+ * Finds the most recent directory in the given path that starts with "render".
189
+ * @param dirPath - The root directory to search.
190
+ * @returns The full path of the latest render directory.
191
+ * @throws If the directory does not exist or no render directories are found.
192
+ */
193
+ function findLatestDir(dirPath: string): string {
194
+ if (!fs.existsSync(dirPath)) {
195
+ throw new Error(`Directory not found: ${dirPath}`)
196
+ }
197
+
198
+ const entries = fs.readdirSync(dirPath)
199
+ let newestDir: string | null = null
200
+ let newestTime = 0 // milliseconds
201
+
202
+ entries.forEach((entry) => {
203
+ const fullPath = path.join(dirPath, entry)
204
+ const stat = fs.statSync(fullPath)
205
+ if (stat.isDirectory() && entry.startsWith('render')) {
206
+ // Prefer creation time if available; fallback to modified time.
207
+ const time = stat.birthtimeMs || stat.mtimeMs
208
+ if (time > newestTime) {
209
+ newestTime = time
210
+ newestDir = fullPath
211
+ }
212
+ }
213
+ })
214
+
215
+ if (!newestDir) {
216
+ throw new Error('No render directories found')
217
+ }
218
+ return newestDir
219
+ }
@@ -0,0 +1,35 @@
1
+ import path from 'path'
2
+ import fs from 'fs/promises'
3
+ import fsSync from 'fs'
4
+
5
+ export async function deleteRenderedContent() {
6
+ const folderPath = './image_renders'
7
+ const audioRendersDir = './audio_renders'
8
+
9
+ try {
10
+ // Read all entries in the folder.
11
+ const entries = await fs.readdir(folderPath, { withFileTypes: true })
12
+
13
+ // Filter for directories starting with "render_".
14
+ const renderDirs = entries.filter(
15
+ (entry) => entry.isDirectory() && entry.name.startsWith('render_')
16
+ )
17
+
18
+ // Loop over the filtered directories and remove each.
19
+ await Promise.all(
20
+ renderDirs.map(async (entry) => {
21
+ const fullPath = path.join(folderPath, entry.name)
22
+ await fs.rm(fullPath, { recursive: true, force: true })
23
+ })
24
+ )
25
+
26
+ fsSync.readdirSync(audioRendersDir).forEach((item) => {
27
+ const itemPath = path.join(audioRendersDir, item)
28
+ fsSync.rmSync(itemPath, { recursive: true, force: true })
29
+ })
30
+
31
+ console.log('All render cache have been deleted.')
32
+ } catch (error) {
33
+ console.error(`Error while deleting render folders: ${(error as any).message}`)
34
+ }
35
+ }
@@ -0,0 +1,8 @@
1
+ import { ElectronAPI } from '@electron-toolkit/preload'
2
+
3
+ declare global {
4
+ interface Window {
5
+ electron: ElectronAPI
6
+ api: unknown
7
+ }
8
+ }
@@ -0,0 +1,36 @@
1
+ import { contextBridge, ipcRenderer } from 'electron'
2
+ import { electronAPI } from '@electron-toolkit/preload'
3
+ import { RenderOptions } from '../main/rendering'
4
+
5
+ const customAPI = {
6
+ startVideoRender: (options: RenderOptions) => ipcRenderer.invoke('start-video-render', options)
7
+ }
8
+
9
+ // Custom APIs for renderer
10
+ const api = {
11
+ ...customAPI
12
+ }
13
+
14
+ // Use `contextBridge` APIs to expose Electron APIs to
15
+ // renderer only if context isolation is enabled, otherwise
16
+ // just add to the DOM global.
17
+ if (process.contextIsolated) {
18
+ try {
19
+ contextBridge.exposeInMainWorld('electron', electronAPI)
20
+ contextBridge.exposeInMainWorld('api', api)
21
+ } catch (error) {
22
+ console.error(error)
23
+ }
24
+ } else {
25
+ // @ts-ignore (define in dts)
26
+ window.electron = electronAPI
27
+ // @ts-ignore (define in dts)
28
+ window.api = api
29
+ }
30
+
31
+ /*
32
+ contextBridge.exposeInMainWorld('api', {
33
+ // Expose a method to start the video render
34
+ startVideoRender: (options: RenderOptions) => ipcRenderer.invoke('start-video-render', options)
35
+ })
36
+ */
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Electron</title>
6
+ <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
7
+ <!-- <meta
8
+ http-equiv="Content-Security-Policy"
9
+ content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
10
+ />-->
11
+ </head>
12
+
13
+ <body>
14
+ <div id="app"></div>
15
+ <script type="module" src="/src/main.ts"></script>
16
+ </body>
17
+ </html>