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,678 @@
1
+ import { createFastText, createLine, PaddedLine } from '../renderer/src/lib/rendering/objects2d'
2
+ import { createSVGShape } from '../renderer/src/lib/rendering/svg/parsing'
3
+ import { AnimatedScene, HotReloadSetting, SpaceSetting } from '../renderer/src/lib/scene/sceneClass'
4
+ import * as THREE from 'three'
5
+ import { addBackgroundGradient } from '../renderer/src/lib/rendering/lighting3d'
6
+ import { COLORS } from '../renderer/src/lib/rendering/helpers'
7
+ import { fade, setOpacity, zoomOut } from '../renderer/src/lib/animation/animations'
8
+ import tickSound from '$assets/audio/tick_sound.mp3'
9
+ import { linspace } from '../renderer/src/lib/mathHelpers/vectors'
10
+ import { latexToSVG } from '../renderer/src/lib/rendering/svg/rastered'
11
+
12
+ const getCircleSVG = (color: string, percentageStrokeWidth: number = 4) => `
13
+ <svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
14
+ <circle
15
+ cx="0" cy="0" r="100"
16
+ stroke="${color}"
17
+ stroke-width="${percentageStrokeWidth}"
18
+ fill="none" />
19
+ </svg>
20
+ `
21
+
22
+ export const neonColors: string[] = [
23
+ '#39FF14',
24
+ '#CCFF00',
25
+ '#FFFF33',
26
+ '#FF6EC7',
27
+ '#FE4164',
28
+ '#FF3131',
29
+ '#FF9933',
30
+ '#FC0FC0',
31
+ '#6F00FF',
32
+ '#BF00FF',
33
+ '#7DF9FF',
34
+ '#00FFFF',
35
+ '#08E8DE',
36
+ '#00FF7F',
37
+ '#FFFF00',
38
+ '#00FFEF',
39
+ '#7FFF00',
40
+ '#FF00FF',
41
+ '#FF1E56',
42
+ '#1AE1F2'
43
+ ]
44
+
45
+ class CircleGroup {
46
+ public group = new THREE.Group()
47
+ public rotationAngle: number = 0
48
+ private anchorVector: THREE.Vector3
49
+ private rotationAxis = new THREE.Vector3(0, 0, 1)
50
+ constructor(center: THREE.Vector3, radius: number, strokePercentage: number, color: string) {
51
+ const circle = createSVGShape(getCircleSVG(color, strokePercentage), radius * 2)
52
+ const anchorNode = createSVGShape(getCircleSVG('white', 50), radius * 0.15)
53
+ this.anchorVector = new THREE.Vector3(radius, 0, 0)
54
+ anchorNode.position.copy(this.anchorVector)
55
+ const line = createLine({
56
+ point1: circle.position,
57
+ point2: this.anchorVector,
58
+ color: new THREE.Color('white')
59
+ })
60
+ this.group.add(circle, line, anchorNode)
61
+ this.group.position.copy(center)
62
+ }
63
+
64
+ setCenter(worldPos: THREE.Vector3) {
65
+ this.group.position.copy(this.group.parent!.worldToLocal(worldPos.clone()))
66
+ }
67
+
68
+ setRotation(angle: number) {
69
+ this.rotationAngle = angle
70
+ this.group.setRotationFromAxisAngle(this.rotationAxis, angle)
71
+ }
72
+
73
+ getAnchorPoint(): THREE.Vector3 {
74
+ // make sure your world‐matrices are fresh
75
+ this.group.updateMatrixWorld(true)
76
+ // transform the “radius,0,0” vector to WORLD space in one go
77
+ return this.group.localToWorld(this.anchorVector.clone())
78
+ }
79
+ }
80
+
81
+ interface Relation {
82
+ name: string
83
+ radius: (n: number) => number
84
+ k: (n: number) => number
85
+ phase: (n: number) => number
86
+ latexString: string
87
+ }
88
+
89
+ interface RelationGroup {
90
+ group: THREE.Group
91
+ circleGroups: CircleGroup[]
92
+ plotLines: PlotLine[]
93
+ connectionLines: PaddedLine[]
94
+ topGroup: THREE.Group
95
+ relation: Relation
96
+ latexText: THREE.Mesh
97
+ opacity: number
98
+ }
99
+
100
+ const N = 20
101
+ const colors = [...neonColors] //interpolateHexColors('#0062ff', '#ff0000', N)
102
+ const plotYOffset = -21
103
+ const baseRadius = 5
104
+
105
+ const relations: Relation[] = [
106
+ {
107
+ name: 'Square Wave',
108
+ radius: (n) => {
109
+ const m = 2 * n - 1
110
+ return ((4 / Math.PI) * baseRadius) / m
111
+ },
112
+ k: (n) => 2 * n - 1,
113
+ phase: (_) => 0,
114
+ latexString: String.raw`\sum_{n=1}^{N} \frac{4}{\pi (2n-1)} \sin\left( (2n-1)t \right)`
115
+ },
116
+
117
+ // 2. Sawtooth Wave
118
+ {
119
+ name: 'Sawtooth Wave',
120
+ // all harmonics, amplitude ∝1/n with alternating sign
121
+ radius: (n) => ((2 * baseRadius) / (Math.PI * n)) * (-1) ** (n + 1),
122
+ k: (n) => n,
123
+ phase: () => 0,
124
+ latexString: String.raw`\sum_{n=1}^{N} \frac{2(-1)^{n+1}}{\pi n} \sin(nt)`
125
+ },
126
+
127
+ // 3. Hann‑Windowed Series
128
+ {
129
+ name: 'Hann Window',
130
+ // smooth taper ≈½(1−cos(2πn/N))
131
+ radius: (n) => {
132
+ const raw = ((2 / Math.PI) * baseRadius) / n
133
+ const w = 0.5 * (1 - Math.cos((2 * Math.PI * (n - 1)) / (N - 1)))
134
+ return raw * w
135
+ },
136
+ k: (n) => n,
137
+ phase: () => 0,
138
+ latexString: String.raw`\sum_{n=1}^{N} \frac{2}{\pi n} \left(\frac{1 - \cos\left(\frac{2\pi(n-1)}{N-1}\right)}{2}\right) \sin(nt)`
139
+ },
140
+
141
+ // 6. Random‑Phase Wave
142
+ {
143
+ name: 'Quadratic Chirp',
144
+ // standard 1/n amplitude fall‑off
145
+ radius: (n) => ((2 / Math.PI) * baseRadius) / n,
146
+ // quadratic frequency scaling → high harmonics whirl away!
147
+ k: (n) => n * n,
148
+ // no extra phase offset
149
+ phase: () => 0,
150
+ latexString: String.raw`\sum_{n=1}^{N} \frac{2}{\pi n} \sin(n^2 t)`
151
+ },
152
+ // 7. Blackman Window
153
+ {
154
+ name: 'Envelope Ripple',
155
+ // 1/n decay × a cosine ripple over n
156
+ radius: (n) => {
157
+ const raw = ((2 / Math.PI) * baseRadius) / n
158
+ // ripple period ≈ every 5 harmonics
159
+ const ripple = 0.5 + 0.5 * Math.cos((2 * Math.PI * n) / 5)
160
+ return raw * ripple
161
+ },
162
+ k: (n) => n,
163
+ phase: () => 0,
164
+ latexString: String.raw`\sum_{n=1}^{N} \frac{2}{\pi n} \left(\frac{1 + \cos\left(\frac{2\pi n}{5}\right)}{2}\right) \sin(nt)`
165
+ }
166
+ ]
167
+
168
+ async function svgStringToTexture(
169
+ svgString: string,
170
+ scaleFactor: number = 2 // Default 2x resolution
171
+ ): Promise<THREE.Texture> {
172
+ return new Promise((resolve, reject) => {
173
+ const blob = new Blob([svgString], { type: 'image/svg+xml' })
174
+ const url = URL.createObjectURL(blob)
175
+
176
+ const img = new Image()
177
+ img.onload = () => {
178
+ URL.revokeObjectURL(url)
179
+
180
+ // Calculate scaled dimensions
181
+ const baseWidth = img.naturalWidth
182
+ const baseHeight = img.naturalHeight
183
+ const scaledWidth = baseWidth * scaleFactor
184
+ const scaledHeight = baseHeight * scaleFactor
185
+
186
+ // Create high-res canvas
187
+ const canvas = document.createElement('canvas')
188
+ canvas.width = scaledWidth
189
+ canvas.height = scaledHeight
190
+
191
+ const ctx = canvas.getContext('2d')!
192
+ ctx.setTransform(scaleFactor, 0, 0, scaleFactor, 0, 0)
193
+ ctx.drawImage(img, 0, 0)
194
+
195
+ // 2) now re‑color everything white:
196
+ // switch to “source‐in” so our fill only appears where the SVG is opaque
197
+ ctx.globalCompositeOperation = 'source-in'
198
+ ctx.fillStyle = '#ffffff'
199
+ ctx.fillRect(0, 0, scaledWidth, scaledHeight)
200
+
201
+ // 3) reset composite so nothing else is affected
202
+ ctx.globalCompositeOperation = 'source-over'
203
+
204
+ const texture = new THREE.CanvasTexture(canvas)
205
+ texture.colorSpace = THREE.SRGBColorSpace
206
+ texture.anisotropy = 16 // Improve texture filtering
207
+ resolve(texture)
208
+ }
209
+
210
+ img.onerror = reject
211
+ img.src = url
212
+ })
213
+ }
214
+
215
+ async function createSVGPlane(svgString: string, size: number = 5, resolutionScale: number = 2) {
216
+ const texture = await svgStringToTexture(svgString, resolutionScale)
217
+ texture.colorSpace = THREE.SRGBColorSpace
218
+
219
+ // Maintain aspect ratio
220
+ const aspect = texture.image.width / texture.image.height
221
+ const geometry = new THREE.PlaneGeometry(size * aspect, size)
222
+ const material = new THREE.MeshBasicMaterial({
223
+ map: texture,
224
+ transparent: true,
225
+ color: '#ffffff',
226
+ side: THREE.DoubleSide
227
+ })
228
+
229
+ return new THREE.Mesh(geometry, material)
230
+ }
231
+
232
+ export const fourierSeriesScene = (): AnimatedScene => {
233
+ return new AnimatedScene(
234
+ 1080,
235
+ 2160,
236
+ SpaceSetting.ThreeDim,
237
+ HotReloadSetting.TraceFromStart,
238
+ async (scene) => {
239
+ scene.registerAudio(tickSound)
240
+ //scene.registerAudio(interstellar)
241
+ //scene.playAudio(interstellar)
242
+ //await addHDRI({ scene, hdriPath: HDRIs.outdoor1, useAsBackground: true, blurAmount: 2 })
243
+ addBackgroundGradient({
244
+ scene,
245
+ topColor: '#000000',
246
+ bottomColor: COLORS.black,
247
+ backgroundOpacity: 0.5
248
+ })
249
+
250
+ const relationGroups: RelationGroup[] = []
251
+ const relationsZ = linspace(-25, 25, relations.length)
252
+
253
+ for (let i = 0; i < relations.length; i++) {
254
+ const relation = relations[i]
255
+ const relationGroup = new THREE.Group()
256
+ const topGroup = new THREE.Group()
257
+ const circlesAxes = create2DAxis({ tickSpacing: baseRadius })
258
+
259
+ topGroup.add(circlesAxes)
260
+
261
+ const circleGroups: CircleGroup[] = []
262
+ for (let i = 0; i < N; i++) {
263
+ const n = i + 1
264
+ const circleGroup = new CircleGroup(
265
+ new THREE.Vector3(0, 0, 0),
266
+ relation.radius(n),
267
+ 5 + i * 1,
268
+ colors[i]
269
+ )
270
+ topGroup.add(circleGroup.group)
271
+ circleGroups.push(circleGroup)
272
+ }
273
+
274
+ relationGroup.add(topGroup)
275
+
276
+ const plotAxes = create2DAxis({
277
+ ymin: -baseRadius * 1.5,
278
+ ymax: baseRadius * 1.5,
279
+ xmax: Math.PI * 27,
280
+ tickSpacing: Math.PI / 2
281
+ })
282
+ plotAxes.position.y = plotYOffset
283
+ relationGroup.add(plotAxes)
284
+
285
+ const plotLines: PlotLine[] = []
286
+ const connectionLines: PaddedLine[] = []
287
+
288
+ for (let i = 0; i < N; i++) {
289
+ const plotLine = new PlotLine(relationGroup, colors[i], 100_000)
290
+ plotLines.push(plotLine)
291
+ const line = createLine({ color: colors[i], width: 1 })
292
+ line.frustumCulled = false
293
+ relationGroup.add(line)
294
+ connectionLines.push(line)
295
+ }
296
+
297
+ /* const fourierTextNode = await createFastText(relation.name, 1)
298
+ fourierTextNode.position.y = 13
299
+
300
+ topGroup.add(fourierTextNode) */
301
+
302
+ let svgString = latexToSVG(relation.latexString) // 1.5x scaling
303
+
304
+ const svgImage = await createSVGPlane(svgString, 3, 6)
305
+ svgImage.position.set(-9, -11, -1)
306
+
307
+ topGroup.add(svgImage)
308
+
309
+ relationGroup.position.z = relationsZ[i]
310
+ scene.add(relationGroup)
311
+
312
+ relationGroups.push({
313
+ group: relationGroup,
314
+ circleGroups,
315
+ plotLines,
316
+ connectionLines,
317
+ topGroup,
318
+ relation,
319
+ latexText: svgImage,
320
+ opacity: 1
321
+ })
322
+ }
323
+
324
+ const startCameraPos = new THREE.Vector3(-42.1133, 6.217837, 52.55059)
325
+ const startCameraRot = new THREE.Quaternion(-0.1144444, -0.3540472, -0.04370152, 0.9271695)
326
+ scene.camera.position.copy(startCameraPos)
327
+ scene.camera.quaternion.copy(startCameraRot)
328
+
329
+ const textsGroup = new THREE.Group()
330
+
331
+ const fourierTextNode = await createFastText('Fourier Series', 3)
332
+ fourierTextNode.position.y = 3
333
+
334
+ textsGroup.add(fourierTextNode)
335
+
336
+ const textNode = await createFastText('Sawtooth Wave', 1.8)
337
+ textNode.position.y = 0
338
+ setOpacity(textNode, 0.4)
339
+ textsGroup.add(textNode)
340
+
341
+ textsGroup.position.y = 20
342
+
343
+ //scene.add(textsGroup)
344
+
345
+ let mode: string = 'wide'
346
+ let mode2: number = 0
347
+ let mode2List = [0, 2, 2]
348
+ let mode2Index = 0
349
+
350
+ const POS_SNAP_TIME = 10000
351
+ const ROT_SNAP_TIME = 8000
352
+ let lastTransition = 0
353
+
354
+ let cameraLineIndex = 0
355
+ scene.onEachTick((tick, time) => {
356
+ const x = time / 700
357
+
358
+ if (tick % 480 === 0 && tick !== 0) {
359
+ console.log(cameraLineIndex, mode, mode2)
360
+ if (cameraLineIndex < relations.length) {
361
+ mode = cameraLineIndex.toString()
362
+ cameraLineIndex++
363
+ } else {
364
+ cameraLineIndex = 0
365
+ mode = cameraLineIndex.toString()
366
+ mode2Index++
367
+ mode2 = mode2List[mode2Index % mode2List.length]
368
+ cameraLineIndex++
369
+
370
+ if (mode2 === 2) {
371
+ for (let i = 0; i < relationGroups.length; i++) {
372
+ scene.insertAnimAt(tick, zoomOut(relationGroups[i].latexText, 200))
373
+ }
374
+ }
375
+ }
376
+
377
+ lastTransition = time
378
+
379
+ for (let i = 0; i < relationGroups.length; i++) {
380
+ if (i !== Number(mode)) {
381
+ scene.insertAnimAt(
382
+ tick,
383
+ fade(relationGroups[i].group, 200, relationGroups[i].opacity, 0.1)
384
+ )
385
+ relationGroups[i].opacity = 0.1
386
+ } else {
387
+ scene.insertAnimAt(
388
+ tick,
389
+ fade(relationGroups[i].group, 200, relationGroups[i].opacity, 1)
390
+ )
391
+ relationGroups[i].opacity = 1
392
+ }
393
+ }
394
+ }
395
+
396
+ for (const relationGroup of relationGroups) {
397
+ const circleGroups = relationGroup.circleGroups
398
+ const plotLines = relationGroup.plotLines
399
+ const connectionLines = relationGroup.connectionLines
400
+ const topGroup = relationGroup.topGroup
401
+ for (let i = 0; i < N; i++) {
402
+ const n = i + 1
403
+ const k = relationGroup.relation.k(n)
404
+ circleGroups[i].setRotation(k * x)
405
+ }
406
+ for (let i = 1; i < N; i++) {
407
+ circleGroups[i].setCenter(circleGroups[i - 1].getAnchorPoint())
408
+ }
409
+
410
+ for (let i = 0; i < N; i++) {
411
+ const worldTip = circleGroups[i].getAnchorPoint()
412
+ plotLines[i].addPoint([x, worldTip.y + plotYOffset])
413
+
414
+ const p1 = worldTip.clone()
415
+ p1.z = 0
416
+ connectionLines[i].updatePositions(
417
+ p1,
418
+ new THREE.Vector3(x, worldTip.y + plotYOffset, 0)
419
+ )
420
+ }
421
+
422
+ topGroup.position.x = x
423
+ }
424
+
425
+ //scene.camera.position.x = x + startCameraPos.x
426
+ textsGroup.position.x = x
427
+
428
+ let targetPosition: THREE.Vector3
429
+ let targetRotation: THREE.Quaternion
430
+
431
+ if (mode === 'wide') {
432
+ targetPosition = startCameraPos.clone()
433
+ targetPosition.x = x + startCameraPos.x
434
+
435
+ targetRotation = startCameraRot.clone()
436
+ } else if (mode2 === 0) {
437
+ const index = Number(mode)
438
+ const zPos = relationsZ[index] + 18
439
+ targetPosition = new THREE.Vector3(-28.81272 + x, -15.44788, zPos)
440
+ targetRotation = new THREE.Quaternion(0.03540297, -0.4387143, 0.01730056, 0.8977623)
441
+ } else if (mode2 == 1) {
442
+ const index = Number(mode)
443
+ const zPos = relationsZ[index] + 18
444
+ targetPosition = new THREE.Vector3(15.89088 + x, 23.76457, zPos)
445
+ targetRotation = new THREE.Quaternion(-0.3922925, 0.3045554, 0.1394623, 0.8566813)
446
+ } else {
447
+ const index = Number(mode)
448
+ const followGroup = relationGroups[index].circleGroups[1]
449
+ const tip = followGroup.getAnchorPoint()
450
+
451
+ // place the camera relative to the circle
452
+ targetPosition = tip.clone().add(new THREE.Vector3(0, 0, 10)) // 30 units “in front”
453
+ targetRotation = new THREE.Quaternion(0, 0, 0, 1)
454
+ }
455
+ const deltaTime = time - lastTransition
456
+
457
+ const posLerp = 1 - Math.exp(-deltaTime / POS_SNAP_TIME)
458
+ const rotLerp = 1 - Math.exp(-deltaTime / ROT_SNAP_TIME)
459
+
460
+ scene.camera.position.lerp(targetPosition, posLerp)
461
+ scene.camera.quaternion.slerp(targetRotation, rotLerp)
462
+ })
463
+
464
+ scene.addWait(45000)
465
+ }
466
+ )
467
+ }
468
+
469
+ class PlotLine {
470
+ private geometry: THREE.BufferGeometry
471
+ private positions: THREE.BufferAttribute
472
+ public line: THREE.Line
473
+ private count: number
474
+ private maxPoints: number
475
+ private needsFullUpdate: boolean
476
+
477
+ constructor(
478
+ group: THREE.Scene | THREE.Group,
479
+ color: THREE.ColorRepresentation,
480
+ maxPoints: number
481
+ ) {
482
+ this.maxPoints = maxPoints
483
+ this.count = 0
484
+ this.needsFullUpdate = false
485
+
486
+ // Create pre-initialized buffer (xyz coordinates)
487
+ const buffer = new Float32Array(3 * maxPoints)
488
+ this.positions = new THREE.BufferAttribute(buffer, 3)
489
+ this.positions.setUsage(THREE.DynamicDrawUsage)
490
+
491
+ // Configure geometry
492
+ this.geometry = new THREE.BufferGeometry()
493
+ this.geometry.setAttribute('position', this.positions)
494
+ this.geometry.setDrawRange(0, 0) // Start with empty draw range
495
+
496
+ // Create line material (note: lineWidth might be limited by WebGL implementation)
497
+ const material = new THREE.LineBasicMaterial({
498
+ color,
499
+ transparent: true
500
+ })
501
+
502
+ this.line = new THREE.Line(this.geometry, material)
503
+
504
+ this.line.frustumCulled = false
505
+ group.add(this.line)
506
+ }
507
+
508
+ addPoint(point: [number, number]) {
509
+ const [x, y] = point
510
+
511
+ if (this.count >= this.maxPoints) {
512
+ // Shift buffer left by one point (optimized)
513
+ this.positions.array.copyWithin(0, 3, 3 * this.maxPoints)
514
+ this.count = this.maxPoints - 1
515
+ this.needsFullUpdate = true
516
+ }
517
+
518
+ const index = this.count * 3
519
+ this.positions.array[index] = x
520
+ this.positions.array[index + 1] = y
521
+ this.positions.array[index + 2] = 0
522
+ this.count++
523
+
524
+ this.updateGeometry()
525
+ }
526
+
527
+ private updateGeometry() {
528
+ if (this.needsFullUpdate) {
529
+ // Update entire buffer
530
+ this.positions.needsUpdate = true
531
+ this.geometry.setDrawRange(0, this.count)
532
+ this.needsFullUpdate = false
533
+ } else {
534
+ // Partial update (only update new points)
535
+ const start = Math.max(0, (this.count - 1) * 3)
536
+ ;(this.positions as any).updateRange = {
537
+ offset: start,
538
+ count: 3 // Only update the last added point
539
+ }
540
+ this.positions.needsUpdate = true
541
+ this.geometry.setDrawRange(0, this.count)
542
+ }
543
+ }
544
+
545
+ // Clear/reset the line
546
+ clear() {
547
+ this.count = 0
548
+ this.geometry.setDrawRange(0, 0)
549
+ this.positions.needsUpdate = true
550
+ }
551
+ }
552
+ /**
553
+ * Creates a 2D axis with evenly spaced ticks and independent arrow size.
554
+ * @param {object} options
555
+ * @param {number} options.xmin - Minimum x value
556
+ * @param {number} options.xmax - Maximum x value
557
+ * @param {number} options.ymin - Minimum y value
558
+ * @param {number} options.ymax - Maximum y value
559
+ * @param {number} options.tickSpacing - World‐space distance between ticks
560
+ * @param {number} options.tickLength - Length of each tick mark
561
+ * @param {number} options.arrowLength - Length of the axis arrowhead
562
+ * @param {number} options.arrowRadius - Base radius of the axis arrowhead
563
+ * @param {number} options.axisColor - Color of axis lines and ticks (hex)
564
+ * @param {number} options.arrowColor - Color of arrowheads (hex)
565
+ * @returns {THREE.Group} Group containing the axis
566
+ */
567
+ export function create2DAxis({
568
+ xmin = -10,
569
+ xmax = 10,
570
+ ymin = -10,
571
+ ymax = 10,
572
+ tickSpacing = 1,
573
+ tickLength = 0.4,
574
+ arrowLength = 0.8,
575
+ arrowRadius = 0.2,
576
+ axisColor = 0xffffff,
577
+ arrowColor = 0xffffff
578
+ } = {}) {
579
+ const group = new THREE.Group()
580
+ const lineMat = new THREE.LineBasicMaterial({ color: axisColor, transparent: true })
581
+ const arrowMat = new THREE.MeshBasicMaterial({ color: arrowColor, transparent: true })
582
+
583
+ // Helper: create a line between two 2D points
584
+ const makeLine = ([x1, y1], [x2, y2]) => {
585
+ const pts = [new THREE.Vector3(x1, y1, 0), new THREE.Vector3(x2, y2, 0)]
586
+ const geo = new THREE.BufferGeometry().setFromPoints(pts)
587
+ return new THREE.Line(geo, lineMat)
588
+ }
589
+
590
+ // Draw main axes
591
+ group.add(makeLine([xmin, 0], [xmax, 0]))
592
+ group.add(makeLine([0, ymin], [0, ymax]))
593
+
594
+ // Draw ticks along an axis
595
+ const halfTick = tickLength / 2
596
+ const drawTicks = (start, end, isXAxis = true) => {
597
+ // first multiple of tickSpacing ≥ start
598
+ const first = Math.ceil(start / tickSpacing) * tickSpacing
599
+ for (let v = first; v <= end + 1e-8; v += tickSpacing) {
600
+ if (isXAxis) {
601
+ group.add(makeLine([v, -halfTick], [v, halfTick]))
602
+ } else {
603
+ group.add(makeLine([-halfTick, v], [halfTick, v]))
604
+ }
605
+ }
606
+ }
607
+
608
+ drawTicks(xmin, xmax, true) // X ticks
609
+ drawTicks(ymin, ymax, false) // Y ticks
610
+
611
+ // Create arrowheads
612
+ const coneGeo = new THREE.ConeGeometry(arrowRadius, arrowLength, 8)
613
+
614
+ const arrowX = new THREE.Mesh(coneGeo, arrowMat)
615
+ arrowX.position.set(xmax + arrowLength / 2, 0, 0)
616
+ arrowX.rotation.z = -Math.PI / 2
617
+ group.add(arrowX)
618
+
619
+ const arrowY = new THREE.Mesh(coneGeo, arrowMat)
620
+ arrowY.position.set(0, ymax + arrowLength / 2, 0)
621
+ // default orientation points +Y
622
+ group.add(arrowY)
623
+
624
+ return group
625
+ }
626
+
627
+ export function interpolateHexColors(color1: string, color2: string, steps: number): string[] {
628
+ if (steps < 2) {
629
+ throw new Error('steps must be at least 2')
630
+ }
631
+
632
+ // Strip "#" if present, and expand short form (#abc → aabbcc)
633
+ const normalize = (hex: string): string => {
634
+ hex = hex.replace(/^#/, '')
635
+ if (hex.length === 3) {
636
+ hex = hex
637
+ .split('')
638
+ .map((c) => c + c)
639
+ .join('')
640
+ }
641
+ if (!/^[0-9a-fA-F]{6}$/.test(hex)) {
642
+ throw new Error(`Invalid hex colour: "${hex}"`)
643
+ }
644
+ return hex.toLowerCase()
645
+ }
646
+
647
+ const hexToRgb = (hex: string) => {
648
+ const normalized = normalize(hex)
649
+ return {
650
+ r: parseInt(normalized.slice(0, 2), 16),
651
+ g: parseInt(normalized.slice(2, 4), 16),
652
+ b: parseInt(normalized.slice(4, 6), 16)
653
+ }
654
+ }
655
+
656
+ const rgbToHex = ({ r, g, b }: { r: number; g: number; b: number }) =>
657
+ '#' +
658
+ [r, g, b]
659
+ .map((v) => {
660
+ const h = v.toString(16)
661
+ return h.length === 1 ? '0' + h : h
662
+ })
663
+ .join('')
664
+
665
+ const c1 = hexToRgb(color1)
666
+ const c2 = hexToRgb(color2)
667
+ const result: string[] = []
668
+
669
+ for (let i = 0; i < steps; i++) {
670
+ const t = i / (steps - 1)
671
+ const r = Math.round(c1.r + (c2.r - c1.r) * t)
672
+ const g = Math.round(c1.g + (c2.g - c1.g) * t)
673
+ const b = Math.round(c1.b + (c2.b - c1.b) * t)
674
+ result.push(rgbToHex({ r, g, b }))
675
+ }
676
+
677
+ return result
678
+ }