create-definedmotion 0.1.4 → 0.3.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 (81) hide show
  1. package/package.json +2 -2
  2. package/template/package-lock.json +313 -59
  3. package/template/package.json +1 -0
  4. package/template/src/assets/audio/testing_shadow_glow_song.mp3 +0 -0
  5. package/template/src/assets/for_tests/svg/gravity_text.svg +38 -0
  6. package/template/src/assets/for_tests/svg/grip_figure.svg +28 -0
  7. package/template/src/entry.ts +4 -5
  8. package/template/src/example_scenes/alternativesScene.ts +1 -1
  9. package/template/src/example_scenes/dependencyScene.ts +2 -4
  10. package/template/src/example_scenes/fourierSeriesScene.ts +10 -11
  11. package/template/src/example_scenes/keyboardScene.ts +14 -16
  12. package/template/src/example_scenes/latex_text_transitions_scene.ts +146 -0
  13. package/template/src/example_scenes/tests/animations/camera_movements/test_2d_camera_centers_labels.ts +53 -0
  14. package/template/src/example_scenes/tests/animations/camera_movements/test_2d_camera_hits_markers.ts +40 -0
  15. package/template/src/example_scenes/tests/animations/camera_movements/test_camera_rotate_quaternion.ts +17 -0
  16. package/template/src/example_scenes/tests/animations/camera_movements/test_camera_waypoints_sequential.ts +29 -0
  17. package/template/src/example_scenes/tests/animations/camera_movements/test_fly_camera_waypoints_verifiable.ts +87 -0
  18. package/template/src/example_scenes/tests/animations/camera_movements/test_zoom_perspective_sequential.ts +17 -0
  19. package/template/src/example_scenes/tests/animations/latex/test_latex_blue_particle_transition.ts +82 -0
  20. package/template/src/example_scenes/tests/animations/latex/test_latex_highlight_animation.ts +64 -0
  21. package/template/src/example_scenes/tests/animations/latex/test_latex_mark_animation.ts +42 -0
  22. package/template/src/example_scenes/tests/animations/latex/test_latex_particle_transition.ts +48 -0
  23. package/template/src/example_scenes/tests/animations/latex/test_latex_particle_transition_complex.ts +65 -0
  24. package/template/src/example_scenes/tests/animations/latex/test_latex_particle_transition_super_complex.ts +86 -0
  25. package/template/src/example_scenes/tests/animations/latex/test_with_environment_latex_particle_transition.ts +80 -0
  26. package/template/src/example_scenes/tests/animations/latex/test_write_latex_animation.ts +28 -0
  27. package/template/src/example_scenes/tests/animations/latex/test_write_latex_animation_2.ts +34 -0
  28. package/template/src/example_scenes/tests/animations/latex/test_write_latex_animation_3.ts +34 -0
  29. package/template/src/example_scenes/tests/animations/test_updater.ts +24 -0
  30. package/template/src/example_scenes/tests/audio/test_long_audio.ts +11 -0
  31. package/template/src/example_scenes/tests/audio/test_many_short_sounds.ts +50 -0
  32. package/template/src/example_scenes/tests/deferred_anims/testing_deferredAnims.ts +71 -0
  33. package/template/src/example_scenes/tests/deferred_anims/testing_deferredAnims2.ts +65 -0
  34. package/template/src/example_scenes/tests/environment/test_hdri_performance.ts +14 -0
  35. package/template/src/example_scenes/tests/svg/test_basic_latex_query.ts +59 -0
  36. package/template/src/example_scenes/tests/svg/test_basic_svg.ts +11 -0
  37. package/template/src/example_scenes/tests/svg/test_colored_latex_to_svg.ts +42 -0
  38. package/template/src/example_scenes/tests/svg/test_complex_latex_to_svg.ts +22 -0
  39. package/template/src/example_scenes/tests/svg/test_latex_to_svg.ts +17 -0
  40. package/template/src/example_scenes/tests/svg/test_material_on_latex.ts +43 -0
  41. package/template/src/example_scenes/tests/svg/test_query_latex_variables.ts +66 -0
  42. package/template/src/example_scenes/tests/svg/test_regular_text_latex.ts +21 -0
  43. package/template/src/example_scenes/tests/svg/test_super_complex_latex_to_svg.ts +98 -0
  44. package/template/src/example_scenes/tests/svg/test_transition_svgs.ts +33 -0
  45. package/template/src/example_scenes/tests/svg/test_update_svg_object.ts +19 -0
  46. package/template/src/example_scenes/tests/svg/test_yellow_grip_symbol_svg.ts +11 -0
  47. package/template/src/example_scenes/tutorials/easy1.ts +4 -4
  48. package/template/src/example_scenes/tutorials/easy3.ts +1 -1
  49. package/template/src/example_scenes/tutorials/medium1.ts +3 -5
  50. package/template/src/example_scenes/vectorField.ts +2 -4
  51. package/template/src/example_scenes/visulizingFunctions.ts +5 -7
  52. package/template/src/main/index.ts +59 -3
  53. package/template/src/main/rendering.ts +38 -21
  54. package/template/src/preload/index.ts +15 -1
  55. package/template/src/renderer/index.html +1 -1
  56. package/template/src/renderer/src/App.svelte +215 -32
  57. package/template/src/renderer/src/application_assets/360.svg +39 -0
  58. package/template/src/renderer/src/application_assets/move.svg +37 -0
  59. package/template/src/renderer/src/lib/animation/animations.ts +141 -88
  60. package/template/src/renderer/src/lib/animation/captureCanvas.ts +3 -17
  61. package/template/src/renderer/src/lib/animation/interpolations.ts +2 -1
  62. package/template/src/renderer/src/lib/animation/latexMarkAndHighlight.ts +349 -0
  63. package/template/src/renderer/src/lib/animation/latexTransitionsAndWrite.ts +558 -0
  64. package/template/src/renderer/src/lib/audio/manager.ts +185 -0
  65. package/template/src/renderer/src/lib/general/helpers.ts +16 -47
  66. package/template/src/renderer/src/lib/rendering/hdri.ts +273 -0
  67. package/template/src/renderer/src/lib/rendering/lighting3d.ts +0 -105
  68. package/template/src/renderer/src/lib/rendering/setup.ts +7 -1
  69. package/template/src/renderer/src/lib/rendering/svg/latexSVGQueries.ts +44 -0
  70. package/template/src/renderer/src/lib/rendering/svg/latexToSVG.ts +132 -0
  71. package/template/src/renderer/src/lib/rendering/svg/svgObjectHelpers.ts +59 -0
  72. package/template/src/renderer/src/lib/rendering/svg/svgRendering.ts +120 -0
  73. package/template/src/renderer/src/lib/scene/sceneClass.ts +180 -62
  74. package/template/src/renderer/src/lib/animation/helpers.ts +0 -7
  75. package/template/src/renderer/src/lib/audio/loader.ts +0 -104
  76. package/template/src/renderer/src/lib/rendering/materials.ts +0 -6
  77. package/template/src/renderer/src/lib/rendering/protocols.ts +0 -21
  78. package/template/src/renderer/src/lib/rendering/svg/drawing.ts +0 -213
  79. package/template/src/renderer/src/lib/rendering/svg/parsing.ts +0 -717
  80. package/template/src/renderer/src/lib/rendering/svg/rastered.ts +0 -42
  81. package/template/src/renderer/src/lib/rendering/svgObjects.ts +0 -1137
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { spawnSync } from 'child_process'
4
+ import { fileURLToPath } from 'url'
4
5
 
5
6
  export interface AudioInScene {
6
7
  audioPath: string
@@ -136,28 +137,42 @@ export function renderVideo(options: RenderOptions): Promise<string> {
136
137
  }
137
138
  })
138
139
  }
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
140
 
156
- const amixFilter = `${amixInputs.join('')}amix=inputs=${amixInputs.length}:duration=longest[aout]`
157
- const filterComplex = [...filterChains, amixFilter].join('; ')
158
141
 
159
- return `ffmpeg ${inputs.join(' ')} -filter_complex "${filterComplex}" -map "[aout]" -c:a libmp3lame -q:a 2 ${outputFolder}/${id}.mp3`
160
- }*/
142
+ function toFsPath(p: string): string {
143
+ if (!p) throw new Error('empty path')
144
+
145
+ // Handle Vite dev absolute path
146
+ if (p.startsWith('/@fs/')) {
147
+ // Keep the leading slash before "Users"
148
+ return decodeURIComponent(p.slice(4)) // results in "/Users/…"
149
+ // Alternatively:
150
+ // return decodeURIComponent(p.replace(/^\/@fs/, ''))
151
+ }
152
+
153
+ // 2) file:// URL
154
+ try {
155
+ const u = new URL(p)
156
+ if (u.protocol === 'file:') return fileURLToPath(u)
157
+ } catch {/* not a URL */}
158
+
159
+ // 3) already absolute on disk
160
+ if (path.isAbsolute(p)) return p
161
+
162
+ // 4) fallback: try typical asset roots
163
+ const cleaned = p.replace(/^\/?assets\//, '') // allow "assets/foo.mp3" or "/assets/foo.mp3"
164
+ const guesses = [
165
+ path.join(process.cwd(), 'src', 'renderer', 'assets', cleaned), // dev
166
+ path.join(process.resourcesPath, 'assets', cleaned), // prod packaged
167
+ path.join(process.cwd(), cleaned) // last resort
168
+ ]
169
+ for (const g of guesses) {
170
+ if (fs.existsSync(g)) return g
171
+ }
172
+
173
+ // final fallback: return as-is (FFmpeg will error, but at least we see the attempted path)
174
+ return p
175
+ }
161
176
 
162
177
  function buildAudioMixCommand(renderOptions: RenderOptions, outputFolder: string, id: string) {
163
178
  const { fps, renderingAudioGather } = renderOptions
@@ -166,7 +181,9 @@ function buildAudioMixCommand(renderOptions: RenderOptions, outputFolder: string
166
181
  let inputIndexes: string[] = []
167
182
 
168
183
  renderingAudioGather.forEach((audio, index) => {
169
- inputs.push(`-i ./src/renderer${audio.audioPath}`)
184
+ // Prefer an already-provided absolute fsPath (if you later add it); else normalize here
185
+ const inputPath = (audio as any).fsPath ?? toFsPath(audio.audioPath)
186
+ inputs.push(`-i "${inputPath}"`)
170
187
  const delayMs = Math.floor((audio.atFrame / fps) * 1000)
171
188
  // adelay syntax: "adelay=delay_in_ms|delay_in_ms"
172
189
  filters.push(`[${index}:a]adelay=${delayMs}|${delayMs},volume=${audio.volume}[a${index}]`)
@@ -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"
@@ -2,16 +2,45 @@
2
2
  import './app.css'
3
3
  import { generateID, setStateInScene, updateStateInUrl } from './lib/general/helpers'
4
4
  import { onDestroy, onMount } from 'svelte'
5
- import { setGlobalContainerRef, type AnimatedScene } from './lib/scene/sceneClass'
5
+ import { hotreloadNameLookup, screenFPS, setGlobalContainerRef, type AnimatedScene } from './lib/scene/sceneClass'
6
6
  import { loadFonts } from './lib/rendering/objects2d'
7
- import { entryScene } from '../../entry'
7
+ import { animationFPSDivider, entryScene, renderSkip } from '../../entry'
8
8
  import { callAllDestroyFunctions } from './lib/general/onDestory'
9
+ import rotateIcon from "./application_assets/360.svg"
10
+ import moveIcon from "./application_assets/move.svg"
11
+
12
+
13
+ let frameValueElement: HTMLParagraphElement
14
+ let timeValueElement: HTMLParagraphElement
15
+ let sliderElement: HTMLInputElement
16
+
17
+ const TEXT_FRAME_MS_LIMIT = 0.90*1000 / 30; // A little lower to avoid skipping frames when timing is unfourtunate
18
+ const SLIDER_FRAME_MS_LIMIT = 0.90*1000 / 60; // A little lower to avoid skipping frames when timing is unfourtunate
19
+
20
+ let lastTextUpdate = 0;
21
+ let lastSliderUpdate = 0;
22
+
23
+ let screenRefreshRate = $state(0)
24
+ let isRendering = $state(false)
25
+
26
+ let isScrubbing = false
27
+ let wasPlayingBeforeScrub = false
28
+
29
+
30
+ function formatMs(ms: number) {
31
+ const sign = ms < 0 ? '-' : ''
32
+ ms = Math.abs(ms)
33
+ const minutes = Math.floor(ms / 60000)
34
+ const seconds = Math.floor((ms % 60000) / 1000)
35
+ const millis = Math.floor(ms % 1000)
36
+ return `${sign}${String(minutes).padStart(2,'0')}:${String(seconds).padStart(2,'0')}.${String(millis).padStart(3,'0')}`
37
+ }
9
38
 
10
- //const ipcHandle = (): void => window.electron.ipcRenderer.send('ping')
11
39
 
12
40
  const animationWindowID = generateID()
13
41
 
14
42
  let scene: AnimatedScene
43
+ let hasInitScene = $state(false)
15
44
 
16
45
  let isPlayingStateVar = $state(false)
17
46
 
@@ -25,29 +54,70 @@
25
54
  const frame = Math.round((sliderValue / maxSliderValue) * (scene.totalSceneTicks - 1))
26
55
  if (frame !== lastSetFrame) {
27
56
  await scene.jumpToFrameAtIndex(frame)
57
+ updateUIImmediate();
28
58
  lastSetFrame = frame
29
59
  }
30
60
  }
31
61
  }
32
62
 
63
+ function updateSliderOnly() {
64
+ if (!scene || !sliderElement) return
65
+ if (isScrubbing) return // <-- single guard
66
+ const denom = Math.max(1, (scene.totalSceneTicks - 1))
67
+ ;(sliderElement as any).value = (scene.sceneRenderTick / denom) * maxSliderValue
68
+ }
69
+
70
+ function updateTextsOnly() {
71
+ if (!scene) return;
72
+ if (frameValueElement) frameValueElement.textContent = `Frame: ${scene.sceneRenderTick}`;
73
+ if (timeValueElement) timeValueElement.textContent = `Time: ${formatMs(scene.getCurrentTimeMs())}`;
74
+ }
75
+
76
+ function updateUIImmediate() {
77
+ updateSliderOnly();
78
+ updateTextsOnly();
79
+ }
80
+
81
+
82
+ function maybeUpdateUI() {
83
+ const now = performance.now();
84
+
85
+ // ~60 Hz slider
86
+ if (now - lastSliderUpdate >= SLIDER_FRAME_MS_LIMIT) {
87
+ lastSliderUpdate = now;
88
+ updateSliderOnly();
89
+ }
90
+
91
+ // ~30 Hz texts
92
+ if (now - lastTextUpdate >= TEXT_FRAME_MS_LIMIT) {
93
+ lastTextUpdate = now;
94
+ updateTextsOnly();
95
+ }
96
+ }
97
+
33
98
  onMount(async () => {
34
99
  if (!entryScene) return
35
100
  await loadFonts()
36
101
  const animationWindow = document.getElementById(animationWindowID)
37
- const sliderElement = document.getElementById('playerSliderID')
102
+
38
103
  if (!animationWindow || !sliderElement) return
39
104
 
40
105
  setGlobalContainerRef(animationWindow)
41
106
 
42
107
  scene = entryScene()
108
+ hasInitScene = true
43
109
 
44
110
  scene.playEffectFunction = () => {
45
- ;(sliderElement as any).value =
46
- (scene.sceneRenderTick / (scene.totalSceneTicks - 1)) * maxSliderValue
111
+ maybeUpdateUI();
47
112
  }
48
- const currentWidth = animationWindow.clientWidth
49
- animationWindow.style.height = `${currentWidth / scene.getAspectRatio()}px`
50
-
113
+ scene.renderingEventFunction = (isStart) => {
114
+ isRendering = isStart
115
+ if (!isStart) {
116
+ // render just finished; force UI to reflect the reset frame
117
+ updateUIImmediate()
118
+ }
119
+ }
120
+
51
121
  setStateInScene(scene)
52
122
  lastSetFrame = scene.sceneRenderTick
53
123
 
@@ -55,11 +125,9 @@
55
125
  updateStateInUrl(scene.sceneRenderTick)
56
126
  }, 500)
57
127
 
58
- // Add listener to handle window resize events
59
- window.addEventListener('resize', () => {
60
- const currentWidth = animationWindow.clientWidth
61
- animationWindow.style.height = `${currentWidth / scene.getAspectRatio()}px`
62
- })
128
+
129
+
130
+ screenRefreshRate = screenFPS
63
131
 
64
132
  // ipcRenderer.send('resize-window', { width: 1000, height: 1000 })
65
133
  })
@@ -68,23 +136,67 @@
68
136
  clearInterval(urlUpdaterInterval)
69
137
  callAllDestroyFunctions()
70
138
  })
139
+
140
+ function fmt(n: number) {
141
+ return Number(n).toPrecision(7);
142
+ }
143
+
144
+ function cameraPositionCode() {
145
+ const p = scene.camera.position;
146
+ return `scene.camera.position.set(
147
+ ${fmt(p.x)},
148
+ ${fmt(p.y)},
149
+ ${fmt(p.z)}
150
+ );`;
151
+ }
152
+
153
+ function cameraRotationCode() {
154
+ const q = scene.camera.quaternion;
155
+ return `scene.camera.quaternion.set(
156
+ ${fmt(q.x)},
157
+ ${fmt(q.y)},
158
+ ${fmt(q.z)},
159
+ ${fmt(q.w)}
160
+ );`;
161
+ }
162
+
163
+ export async function copyToClipboard(text: string): Promise<void> {
164
+ await navigator.clipboard.writeText(text);
165
+ }
71
166
  </script>
72
167
 
73
- <div class=" flex flex-col p-4">
74
- <div id={animationWindowID} class="w-full"></div>
75
- <div class="flex justify-between mt-2 font-bold text-sm">
168
+ <div class=" flex flex-col p-2">
169
+ <div id={animationWindowID} class="w-full rounded-sm overflow-clip"></div>
170
+ {#if isRendering}
171
+ <p class="text-[17px] self-center p-6 pb-1">Do <strong>not save</strong> code during rendering</p>
172
+ <p class="text-xs self-center pt-0 p-2">The viewer might hot reload and affect the result</p>
173
+ {/if}
174
+
175
+ <div class="flex justify-between mt-2 font-bold text-sm items-center">
176
+
177
+
178
+
76
179
  <button
77
- onclick={() => {
78
- if (scene.isPlaying) {
79
- scene.pause()
80
- isPlayingStateVar = false
81
- } else {
82
- scene.playSequenceOfAnimation(scene.sceneRenderTick, scene.totalSceneTicks - 1)
83
- isPlayingStateVar = true
84
- }
85
- }}>{isPlayingStateVar ? 'Pause' : 'Play'}</button
86
- >
180
+ class="w-[70px] text-xs cursor-pointer bg-black/5 rounded-full p-1 hover:bg-black/10 transition"
181
+ onclick={() => {
182
+ if (scene.isPlaying) {
183
+ scene.pause()
184
+ updateUIImmediate();
185
+ isPlayingStateVar = false
186
+ } else {
187
+ scene.playSequenceOfAnimation(scene.sceneRenderTick, scene.totalSceneTicks - 1)
188
+ isPlayingStateVar = true
189
+ }
190
+ }}>{isPlayingStateVar ? 'Pause' : 'Play'}</button
191
+ >
192
+
193
+
194
+ <div class="flex ">
195
+ <p bind:this={frameValueElement} class="font-normal text-[0.7rem] leading-none mr-2 w-[83px]">Frame:</p>
196
+ <p bind:this={timeValueElement} class="font-normal text-[0.7rem] leading-none w-[93px] ">Time:</p>
197
+ </div>
87
198
  <button
199
+ class="w-[70px] text-xs cursor-pointer bg-black/5 rounded-full p-1 hover:bg-black/10 transition"
88
200
  onclick={() => {
89
201
  scene.render()
90
202
  }}>Render</button
@@ -92,16 +204,77 @@
92
204
  </div>
93
205
  <div class="w-full px-0 mx-0">
94
206
  <input
207
+ bind:this={sliderElement}
95
208
  type="range"
96
209
  min="0"
97
210
  max={maxSliderValue}
98
- oninput={(e: any) => handleSliderChange(Number(e.target.value))}
211
+
99
212
  class="w-full focus:outline-none"
100
- id="playerSliderID"
213
+ onpointerdown={() => {
214
+ if (!scene) return
215
+ isScrubbing = true
216
+ wasPlayingBeforeScrub = scene.isPlaying
217
+ if (scene.isPlaying) {
218
+ scene.pause() // silences audio and stops RAF
219
+ isPlayingStateVar = false
220
+ }
221
+ }}
222
+ oninput={(e: any) => {
223
+ // while scrubbing: jump visuals quietly; when not scrubbing, behaves like before
224
+ const v = Number(e.target.value)
225
+ handleSliderChange(v)
226
+ }}
227
+ onpointerup={(e: any) => {
228
+ if (!scene) return
229
+ isScrubbing = false
230
+ const v = Number((e.target as HTMLInputElement).value)
231
+ // ensure we’re at the dropped frame
232
+ handleSliderChange(v)
233
+ if (wasPlayingBeforeScrub) {
234
+ // resume cleanly from here
235
+ scene.playSequenceOfAnimation(scene.sceneRenderTick, scene.totalSceneTicks - 1)
236
+ isPlayingStateVar = true
237
+ }
238
+ }}
101
239
  />
102
240
  </div>
241
+
242
+ <div class="h-4"></div>
243
+ {#if hasInitScene && scene}
244
+
245
+ <p class="font-bold text-sm">Helpers</p>
246
+ <div class="h-2"></div>
247
+ <div class="flex flex-wrap gap-2">
248
+ <button onclick={() => copyToClipboard(cameraPositionCode())} class="text-[0.65rem] font-medium cursor-pointer bg-black/5 rounded-full p-1 pl-4 pr-4 hover:bg-black/10 transition active:bg-blue-200 active:border-blue-200" >
249
+ <div class="flex gap-1 items-center">
250
+
251
+ <p>Copy camera <strong>position</strong></p>
252
+ <img src={moveIcon} alt="Rotation icon" class="w-[15px]"/></div>
253
+ </button>
254
+ <button onclick={() => copyToClipboard(cameraRotationCode())} class="text-[0.65rem] font-medium cursor-pointer bg-black/5 rounded-full p-1 pl-4 pr-4 hover:bg-black/10 transition active:bg-blue-200 active:border-blue-200" >
255
+ <div class="flex gap-1 items-center">
256
+
257
+ <p>Copy camera <strong>rotation</strong></p>
258
+ <img src={rotateIcon} alt="Rotation icon" class="w-[15px]"/></div>
259
+ </button>
260
+ </div>
261
+
262
+ <div class="h-6"></div>
263
+ <p class="font-bold text-sm">Details</p>
264
+ <div class="h-2"></div>
265
+ <p class="text-xs">Animation playback FPS: <strong>{(screenRefreshRate/animationFPSDivider).toFixed(2)}</strong> Hz, rendered video FPS: <strong>{(screenRefreshRate/animationFPSDivider/renderSkip).toFixed(2)}</strong> Hz</p>
266
+ <div class="h-2"></div>
267
+ <p class="text-[0.7rem] opacity-50">Hot reload mode: <strong>{hotreloadNameLookup(scene.hotReloadSetting)}</strong></p>
268
+ <p class="text-[0.7rem] opacity-50">Screen refresh rate: <strong>{screenRefreshRate.toFixed(2)}</strong> Hz</p>
269
+ <p class="text-[0.7rem] opacity-50">Animation FPS divider <strong>{animationFPSDivider}</strong></p>
270
+ <p class="text-[0.7rem] opacity-50">Render skip constant <strong>{renderSkip}</strong></p>
271
+ {/if}
272
+
273
+
274
+ <!--
103
275
  <p id="cameraPositionTextID" class="mt-2 text-xs"></p>
104
276
  <p id="cameraRotationTextID" class="mt-2 text-xs"></p>
277
+ -->
105
278
  </div>
106
279
 
107
280
  <style>
@@ -121,10 +294,20 @@
121
294
  /* Thumb style for Chrome */
122
295
  input[type='range']::-webkit-slider-thumb {
123
296
  -webkit-appearance: none;
124
- width: 16px;
297
+ width: 10px;
125
298
  height: 16px;
126
- border-radius: 50%;
127
- background: #3b82f6;
299
+ border-radius: 5px;
300
+ background: #c2c2c2;
301
+ border: 2px solid #616161;
128
302
  margin-top: -6px; /* Center the thumb on the track */
303
+ cursor: pointer;
304
+ transition: all 0.1s ease;
305
+
306
+ /* Larger hitbox using box-shadow trick */
307
+ box-shadow: 0 0 0 8px transparent;
308
+ }
309
+
310
+ input[type='range']::-webkit-slider-thumb:hover {
311
+ background: #616161;
129
312
  }
130
313
  </style>
@@ -0,0 +1,39 @@
1
+ <?xml version="1.0" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4
+ <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5
+ width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
6
+ preserveAspectRatio="xMidYMid meet">
7
+
8
+ <g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
9
+ fill="#000000" stroke="none">
10
+ <path d="M2455 5107 c-370 -95 -662 -590 -820 -1392 -21 -104 -39 -192 -41
11
+ -193 -1 -2 -62 -15 -136 -28 -679 -125 -1176 -363 -1363 -654 -70 -109 -89
12
+ -169 -89 -290 0 -90 4 -115 27 -173 89 -229 331 -423 714 -576 302 -120 714
13
+ -212 1141 -257 94 -10 172 -21 172 -25 0 -4 -20 -23 -44 -42 -56 -46 -80 -96
14
+ -73 -153 11 -105 124 -166 214 -116 18 9 99 81 181 160 l150 142 149 0 c144 0
15
+ 407 13 527 26 l59 6 -7 -38 c-103 -575 -330 -1064 -545 -1173 -68 -35 -120
16
+ -37 -185 -8 -106 49 -198 155 -303 352 -53 100 -63 112 -106 133 -63 32 -118
17
+ 24 -168 -27 -53 -53 -59 -101 -23 -183 130 -292 329 -508 531 -574 86 -28 238
18
+ -26 318 6 261 101 470 384 631 854 51 150 123 432 150 589 16 93 21 108 39
19
+ 112 11 2 72 14 135 25 323 59 642 161 878 279 272 137 445 299 519 488 23 58
20
+ 27 83 27 173 -1 87 -5 116 -25 170 -99 262 -398 477 -889 640 -100 33 -134 40
21
+ -167 35 -84 -12 -145 -105 -123 -186 17 -60 63 -93 190 -134 282 -93 488 -202
22
+ 611 -325 74 -74 103 -129 103 -200 1 -178 -261 -375 -689 -518 -186 -62 -527
23
+ -143 -542 -129 -3 3 0 70 6 149 50 611 2 1310 -125 1838 -31 129 -66 257 -81
24
+ 296 -3 9 16 5 59 -12 80 -32 148 -35 186 -7 87 64 96 170 20 240 -12 11 -112
25
+ 55 -222 96 -109 42 -199 77 -201 79 -1 2 -21 37 -45 78 -124 217 -280 368
26
+ -445 430 -73 27 -207 35 -280 17z m216 -317 c81 -42 194 -172 257 -296 l21
27
+ -42 -75 -173 c-41 -96 -79 -190 -85 -209 -24 -80 35 -170 120 -185 33 -5 51
28
+ -2 89 17 40 19 52 33 78 87 17 35 34 60 36 55 12 -19 78 -296 102 -424 30
29
+ -157 60 -384 77 -590 24 -272 15 -846 -18 -1127 l-6 -51 -46 -6 c-119 -16
30
+ -295 -27 -501 -32 l-225 -5 -139 161 c-143 167 -172 190 -238 190 -34 0 -93
31
+ -30 -115 -59 -7 -9 -17 -33 -23 -53 -16 -55 2 -107 56 -167 53 -59 59 -56 -97
32
+ -41 -804 81 -1465 332 -1610 611 -31 60 -31 138 0 198 76 147 315 297 658 415
33
+ 132 45 360 104 496 129 l79 15 -6 -47 c-14 -107 -26 -360 -26 -552 0 -197 1
34
+ -207 23 -239 36 -54 71 -74 127 -74 54 0 87 18 124 68 20 26 21 46 27 294 7
35
+ 268 26 561 39 581 15 26 509 56 787 49 161 -5 187 -3 222 13 53 23 81 70 81
36
+ 132 0 85 -42 131 -137 149 -69 13 -606 5 -787 -12 -65 -6 -120 -9 -122 -7 -6
37
+ 6 53 282 87 399 131 466 310 766 504 845 40 17 114 9 166 -17z"/>
38
+ </g>
39
+ </svg>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4
+ <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5
+ width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
6
+ preserveAspectRatio="xMidYMid meet">
7
+
8
+ <g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
9
+ fill="#000000" stroke="none">
10
+ <path d="M2485 4781 c-16 -10 -166 -155 -332 -322 -254 -257 -303 -310 -312
11
+ -344 -35 -134 112 -245 234 -177 11 6 88 79 170 161 l150 151 5 -554 c5 -540
12
+ 6 -555 26 -582 39 -53 71 -69 134 -69 63 0 95 16 134 69 20 27 21 42 26 582
13
+ l5 554 150 -151 c83 -82 159 -155 170 -161 64 -36 140 -25 191 26 30 30 57 94
14
+ 49 119 -2 7 -6 26 -10 42 -5 22 -89 113 -313 338 -169 170 -319 315 -334 323
15
+ -38 20 -107 18 -143 -5z"/>
16
+ <path d="M1035 3285 c-5 -2 -23 -6 -40 -10 -22 -5 -113 -89 -338 -313 -170
17
+ -169 -315 -319 -323 -334 -18 -35 -18 -101 0 -136 8 -15 153 -165 323 -334
18
+ 225 -224 316 -308 338 -313 17 -4 35 -8 42 -10 25 -8 89 19 119 49 51 51 62
19
+ 127 26 191 -6 11 -79 88 -161 170 l-151 150 554 5 c540 5 555 6 582 26 53 39
20
+ 69 71 69 134 0 63 -16 95 -69 134 -27 20 -42 21 -582 26 l-554 5 151 150 c82
21
+ 83 155 159 161 170 55 98 0 213 -112 238 -14 3 -29 4 -35 2z"/>
22
+ <path d="M4030 3278 c-96 -27 -142 -143 -92 -233 6 -11 79 -87 161 -170 l151
23
+ -150 -554 -5 c-540 -5 -555 -6 -582 -26 -53 -39 -69 -71 -69 -134 0 -63 16
24
+ -95 69 -134 27 -21 40 -21 584 -24 l556 -2 -161 -163 c-130 -131 -163 -170
25
+ -172 -202 -14 -54 2 -110 43 -151 30 -30 94 -57 119 -49 7 2 26 6 42 10 22 5
26
+ 113 89 338 313 170 169 315 319 323 334 8 15 14 45 14 68 0 23 -6 53 -14 68
27
+ -8 15 -153 165 -323 334 -224 223 -316 308 -338 313 -16 4 -37 8 -45 10 -8 2
28
+ -31 -1 -50 -7z"/>
29
+ <path d="M2495 2066 c-37 -17 -70 -52 -84 -89 -7 -19 -11 -211 -11 -570 l0
30
+ -541 -162 161 c-114 112 -172 163 -193 168 -16 4 -35 8 -41 10 -26 8 -94 -19
31
+ -122 -48 -29 -31 -55 -96 -47 -120 2 -7 6 -25 10 -42 5 -22 89 -113 313 -338
32
+ 169 -170 319 -315 334 -323 35 -18 101 -18 136 0 15 8 165 153 334 323 224
33
+ 225 308 316 313 338 4 17 8 35 10 42 8 25 -19 89 -49 119 -51 51 -127 62 -191
34
+ 26 -11 -6 -87 -79 -170 -161 l-150 -151 -5 554 c-5 540 -6 555 -26 582 -11 15
35
+ -32 37 -46 47 -34 25 -113 32 -153 13z"/>
36
+ </g>
37
+ </svg>