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.
- package/bin/index.js +3 -0
- package/package.json +37 -0
- package/src/cli.js +100 -0
- package/template/.editorconfig +9 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc.yaml +10 -0
- package/template/_gitignore +10 -0
- package/template/build/entitlements.mac.plist +12 -0
- package/template/build/icon.icns +0 -0
- package/template/build/icon.ico +0 -0
- package/template/build/icon.png +0 -0
- package/template/electron-builder.yml +43 -0
- package/template/electron.vite.config.ts +50 -0
- package/template/eslint.config.mjs +24 -0
- package/template/package-lock.json +10299 -0
- package/template/package.json +64 -0
- package/template/resources/icon.png +0 -0
- package/template/src/assets/audio/fadeSound.mp3 +0 -0
- package/template/src/assets/audio/fadeSound2.mp3 +0 -0
- package/template/src/assets/audio/interstellar.mp3 +0 -0
- package/template/src/assets/audio/keyboard1.mp3 +0 -0
- package/template/src/assets/audio/keyboard2.mp3 +0 -0
- package/template/src/assets/audio/keyboard3.mp3 +0 -0
- package/template/src/assets/audio/tick_sound.mp3 +0 -0
- package/template/src/assets/base.css +67 -0
- package/template/src/assets/electron.svg +10 -0
- package/template/src/assets/fonts/Geo-Regular.woff +0 -0
- package/template/src/assets/fonts/Montserrat-Italic-VariableFont_wght.woff2 +0 -0
- package/template/src/assets/fonts/Montserrat-Medium.ttf +0 -0
- package/template/src/assets/fonts/Montserrat-Medium.woff +0 -0
- package/template/src/assets/fonts/Montserrat-VariableFont_wght.woff2 +0 -0
- package/template/src/assets/fonts/glitch.ttf +0 -0
- package/template/src/assets/hdri/indoor1.hdr +0 -0
- package/template/src/assets/hdri/metro1.hdr +0 -0
- package/template/src/assets/hdri/outdoor1.hdr +0 -0
- package/template/src/assets/hdri/photo-studio1.hdr +0 -0
- package/template/src/assets/hdri/photo-studio2.hdr +0 -0
- package/template/src/assets/hdri/photo-studio3.hdr +0 -0
- package/template/src/assets/objects/keyboardScene/ibm-keyboard.glb +0 -0
- package/template/src/assets/wavy-lines.svg +25 -0
- package/template/src/entry.ts +20 -0
- package/template/src/example_scenes/alternativesScene.ts +88 -0
- package/template/src/example_scenes/dependencyScene.ts +116 -0
- package/template/src/example_scenes/fourierMachineScene.ts +108 -0
- package/template/src/example_scenes/fourierSeriesScene.ts +678 -0
- package/template/src/example_scenes/keyboardScene.ts +447 -0
- package/template/src/example_scenes/surfaceScene.ts +88 -0
- package/template/src/example_scenes/tutorials/easy1.ts +59 -0
- package/template/src/example_scenes/tutorials/easy2.ts +141 -0
- package/template/src/example_scenes/tutorials/easy3.ts +133 -0
- package/template/src/example_scenes/tutorials/medium1.ts +154 -0
- package/template/src/example_scenes/vectorField.ts +209 -0
- package/template/src/example_scenes/visulizingFunctions.ts +246 -0
- package/template/src/main/index.ts +101 -0
- package/template/src/main/rendering.ts +219 -0
- package/template/src/main/storage.ts +35 -0
- package/template/src/preload/index.d.ts +8 -0
- package/template/src/preload/index.ts +36 -0
- package/template/src/renderer/index.html +17 -0
- package/template/src/renderer/src/App.svelte +130 -0
- package/template/src/renderer/src/app.css +24 -0
- package/template/src/renderer/src/env.d.ts +2 -0
- package/template/src/renderer/src/lib/animation/animations.ts +214 -0
- package/template/src/renderer/src/lib/animation/captureCanvas.ts +85 -0
- package/template/src/renderer/src/lib/animation/helpers.ts +7 -0
- package/template/src/renderer/src/lib/animation/interpolations.ts +155 -0
- package/template/src/renderer/src/lib/animation/protocols.ts +79 -0
- package/template/src/renderer/src/lib/audio/loader.ts +104 -0
- package/template/src/renderer/src/lib/fonts/Roboto_Regular.json +1 -0
- package/template/src/renderer/src/lib/fonts/montserrat-medium.json +1 -0
- package/template/src/renderer/src/lib/fonts/montserrat.json +1 -0
- package/template/src/renderer/src/lib/general/helpers.ts +77 -0
- package/template/src/renderer/src/lib/general/onDestory.ts +10 -0
- package/template/src/renderer/src/lib/mathHelpers/vectors.ts +18 -0
- package/template/src/renderer/src/lib/rendering/bumpMaps/noise.ts +84 -0
- package/template/src/renderer/src/lib/rendering/helpers.ts +35 -0
- package/template/src/renderer/src/lib/rendering/lighting3d.ts +387 -0
- package/template/src/renderer/src/lib/rendering/materials.ts +6 -0
- package/template/src/renderer/src/lib/rendering/objects/import.ts +148 -0
- package/template/src/renderer/src/lib/rendering/objects2d.ts +489 -0
- package/template/src/renderer/src/lib/rendering/objects3d.ts +89 -0
- package/template/src/renderer/src/lib/rendering/protocols.ts +21 -0
- package/template/src/renderer/src/lib/rendering/setup.ts +71 -0
- package/template/src/renderer/src/lib/rendering/svg/drawing.ts +213 -0
- package/template/src/renderer/src/lib/rendering/svg/parsing.ts +717 -0
- package/template/src/renderer/src/lib/rendering/svg/rastered.ts +42 -0
- package/template/src/renderer/src/lib/rendering/svgObjects.ts +1137 -0
- package/template/src/renderer/src/lib/scene/helpers.ts +89 -0
- package/template/src/renderer/src/lib/scene/sceneClass.ts +648 -0
- package/template/src/renderer/src/lib/shaders/background_gradient/frag.glsl +12 -0
- package/template/src/renderer/src/lib/shaders/background_gradient/vert.glsl +6 -0
- package/template/src/renderer/src/lib/shaders/hdri_blur/frag.glsl +45 -0
- package/template/src/renderer/src/lib/shaders/hdri_blur/vert.glsl +5 -0
- package/template/src/renderer/src/main.ts +9 -0
- package/template/svelte.config.mjs +7 -0
- package/template/tsconfig.json +4 -0
- package/template/tsconfig.node.json +10 -0
- 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
|
+
}
|