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,89 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
export function createFunctionSurface(
|
|
4
|
+
func: (a: number, b: number) => number,
|
|
5
|
+
xMin: number,
|
|
6
|
+
xMax: number,
|
|
7
|
+
yMin: number,
|
|
8
|
+
yMax: number,
|
|
9
|
+
xSegments: number = 200,
|
|
10
|
+
ySegments: number = 200
|
|
11
|
+
) {
|
|
12
|
+
// Create a plane geometry
|
|
13
|
+
const width = xMax - xMin
|
|
14
|
+
const height = yMax - yMin
|
|
15
|
+
const geometry = new THREE.PlaneGeometry(width, height, xSegments, ySegments)
|
|
16
|
+
|
|
17
|
+
// Get the vertices and update z values based on the function
|
|
18
|
+
const vertices = geometry.attributes.position
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i <= xSegments; i++) {
|
|
21
|
+
for (let j = 0; j <= ySegments; j++) {
|
|
22
|
+
const index = i * (ySegments + 1) + j
|
|
23
|
+
|
|
24
|
+
// Map indices to actual x,y coordinates in our desired range
|
|
25
|
+
const x = xMin + (i / xSegments) * width
|
|
26
|
+
const z = yMin + (j / ySegments) * height
|
|
27
|
+
|
|
28
|
+
// Set vertex X and Y
|
|
29
|
+
vertices.setX(index, x)
|
|
30
|
+
const y = func(x, z)
|
|
31
|
+
vertices.setY(index, y)
|
|
32
|
+
|
|
33
|
+
// Calculate Z using the provided function
|
|
34
|
+
|
|
35
|
+
vertices.setZ(index, z)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Ensure normals are computed for proper lighting
|
|
40
|
+
geometry.computeVertexNormals()
|
|
41
|
+
|
|
42
|
+
// Create mesh with the geometry
|
|
43
|
+
const material = new THREE.MeshPhongMaterial({
|
|
44
|
+
color: 0x156289,
|
|
45
|
+
emissive: 0x072534,
|
|
46
|
+
side: THREE.DoubleSide,
|
|
47
|
+
flatShading: false,
|
|
48
|
+
wireframe: false
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
52
|
+
return mesh
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Function to update an existing function surface with a new time value
|
|
56
|
+
export function updateFunctionSurface(
|
|
57
|
+
mesh: THREE.Mesh,
|
|
58
|
+
func: (a: number, b: number) => number,
|
|
59
|
+
xMin: number,
|
|
60
|
+
xMax: number,
|
|
61
|
+
yMin: number,
|
|
62
|
+
yMax: number,
|
|
63
|
+
xSegments: number = 200,
|
|
64
|
+
ySegments: number = 200
|
|
65
|
+
) {
|
|
66
|
+
const width = xMax - xMin
|
|
67
|
+
const height = yMax - yMin
|
|
68
|
+
const vertices = mesh.geometry.attributes.position
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i <= xSegments; i++) {
|
|
71
|
+
for (let j = 0; j <= ySegments; j++) {
|
|
72
|
+
const index = i * (ySegments + 1) + j
|
|
73
|
+
|
|
74
|
+
// Map indices to actual x,y coordinates
|
|
75
|
+
const x = xMin + (i / xSegments) * width
|
|
76
|
+
const z = yMin + (j / ySegments) * height
|
|
77
|
+
|
|
78
|
+
// Update Y position based on the function with time
|
|
79
|
+
const y = func(x, z)
|
|
80
|
+
vertices.setY(index, y)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mark vertices for update
|
|
85
|
+
vertices.needsUpdate = true
|
|
86
|
+
|
|
87
|
+
// Recompute normals for proper lighting
|
|
88
|
+
mesh.geometry.computeVertexNormals()
|
|
89
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
|
3
|
+
|
|
4
|
+
export interface SceneComponents {
|
|
5
|
+
camera: THREE.PerspectiveCamera | THREE.OrthographicCamera
|
|
6
|
+
renderer: THREE.WebGLRenderer
|
|
7
|
+
scene: THREE.Scene
|
|
8
|
+
controls: OrbitControls
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const RERENDER = (state: SceneComponents) => {
|
|
12
|
+
state.renderer.render(state.scene, state.camera)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ADD = (state: SceneComponents, element: THREE.Mesh) => {
|
|
16
|
+
state.scene.add(element)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const REMOVE = (state: SceneComponents, element: THREE.Mesh) => {
|
|
20
|
+
state.scene.remove(element)
|
|
21
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import type { SceneComponents } from './protocols'
|
|
3
|
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
4
|
+
|
|
5
|
+
export const createScene = (
|
|
6
|
+
container: HTMLElement,
|
|
7
|
+
pixelsWidth: number,
|
|
8
|
+
pixelsHeight: number,
|
|
9
|
+
threeDim: boolean,
|
|
10
|
+
zoom: number,
|
|
11
|
+
farLimit: number
|
|
12
|
+
): SceneComponents => {
|
|
13
|
+
// Create the scene
|
|
14
|
+
const scene: THREE.Scene = new THREE.Scene()
|
|
15
|
+
|
|
16
|
+
// Get the container dimensions
|
|
17
|
+
const width: number = container.clientWidth
|
|
18
|
+
const height: number = container.clientHeight
|
|
19
|
+
// Use the provided dimensions for aspect ratio calculation
|
|
20
|
+
const aspect: number = pixelsWidth / pixelsHeight
|
|
21
|
+
|
|
22
|
+
// Camera setup
|
|
23
|
+
let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera
|
|
24
|
+
let controls: OrbitControls | null = null
|
|
25
|
+
|
|
26
|
+
if (threeDim) {
|
|
27
|
+
// 3D configuration
|
|
28
|
+
camera = new THREE.PerspectiveCamera(75, aspect, 0.1, farLimit)
|
|
29
|
+
camera.position.set(0, 0, zoom)
|
|
30
|
+
} else {
|
|
31
|
+
// 2D configuration
|
|
32
|
+
camera = new THREE.OrthographicCamera(-zoom * aspect, zoom * aspect, zoom, -zoom, 1, farLimit)
|
|
33
|
+
camera.position.set(0, 0, zoom)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Add orbit controls
|
|
37
|
+
controls = new OrbitControls(camera, container)
|
|
38
|
+
controls.enableDamping = true
|
|
39
|
+
controls.dampingFactor = 0.3
|
|
40
|
+
|
|
41
|
+
if (!threeDim) {
|
|
42
|
+
// Custom 2D controls behavior
|
|
43
|
+
controls.enableRotate = false // Disable rotation for 2D
|
|
44
|
+
controls.mouseButtons = {
|
|
45
|
+
LEFT: THREE.MOUSE.PAN, // Only allow panning with left mouse
|
|
46
|
+
MIDDLE: THREE.MOUSE.DOLLY,
|
|
47
|
+
RIGHT: THREE.MOUSE.PAN
|
|
48
|
+
}
|
|
49
|
+
controls.touches = {
|
|
50
|
+
ONE: THREE.TOUCH.PAN,
|
|
51
|
+
TWO: THREE.TOUCH.DOLLY_PAN
|
|
52
|
+
}
|
|
53
|
+
controls.maxZoom = 10 // Limit zoom for 2D view
|
|
54
|
+
controls.minZoom = 0.1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Create the renderer with antialiasing enabled and set its internal resolution
|
|
58
|
+
const renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer({ antialias: true })
|
|
59
|
+
renderer.setSize(width, height)
|
|
60
|
+
renderer.setPixelRatio(window.devicePixelRatio)
|
|
61
|
+
renderer.setViewport(0, 0, pixelsWidth, pixelsHeight)
|
|
62
|
+
renderer.setClearColor(0x000000)
|
|
63
|
+
|
|
64
|
+
// Append the renderer's canvas element to the provided container
|
|
65
|
+
container.appendChild(renderer.domElement)
|
|
66
|
+
|
|
67
|
+
renderer.render(scene, camera)
|
|
68
|
+
|
|
69
|
+
// Return the created objects for further manipulation if needed
|
|
70
|
+
return { scene, camera, renderer, controls }
|
|
71
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { COLORS } from '../helpers'
|
|
2
|
+
import { isApproximatelyEqual2D, isApproximatelyEqual2d3d, VectorizedElementNode } from './parsing'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
|
|
5
|
+
export const drawVectorizedNodes = (
|
|
6
|
+
vectorizedNodes: VectorizedElementNode[],
|
|
7
|
+
width: number,
|
|
8
|
+
options?: {
|
|
9
|
+
defaultFillColor?: number
|
|
10
|
+
defaultStrokeColor?: number
|
|
11
|
+
}
|
|
12
|
+
): THREE.Group => {
|
|
13
|
+
const { defaultFillColor = 0xff9900, defaultStrokeColor = 0x0099ff } = options ?? {}
|
|
14
|
+
|
|
15
|
+
const rootGroup = new THREE.Group()
|
|
16
|
+
|
|
17
|
+
const drawNode = (node: VectorizedElementNode): THREE.Object3D => {
|
|
18
|
+
const nodeGroup = new THREE.Group()
|
|
19
|
+
|
|
20
|
+
const props = node.properties ?? {}
|
|
21
|
+
const hasFill = 'fill' in props && props.fill !== 'none'
|
|
22
|
+
const hasStroke = 'stroke' in props && props.stroke !== 'none'
|
|
23
|
+
|
|
24
|
+
if (node.points.length >= 2 && node.tagName !== 'path' && (hasFill || hasStroke)) {
|
|
25
|
+
// ✅ Fill if applicable
|
|
26
|
+
if (node.isClosed && node.points.length >= 3 && hasFill) {
|
|
27
|
+
const shape = new THREE.Shape(node.points.map(([x, y]) => new THREE.Vector2(x, y)))
|
|
28
|
+
const fillColor = new THREE.Color()
|
|
29
|
+
|
|
30
|
+
const mesh = new THREE.Mesh(
|
|
31
|
+
new THREE.ShapeGeometry(shape),
|
|
32
|
+
new THREE.MeshBasicMaterial({ color: fillColor, side: THREE.DoubleSide })
|
|
33
|
+
)
|
|
34
|
+
nodeGroup.add(mesh)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ✅ Stroke if applicable
|
|
38
|
+
if (hasStroke) {
|
|
39
|
+
const vertices: [number, number, number][] = node.points.map(([x, y]) => [x, y, 0])
|
|
40
|
+
const geometry = pointsToStroke(vertices, props['stroke-width'] || 1)
|
|
41
|
+
|
|
42
|
+
const strokeColor = new THREE.Color(
|
|
43
|
+
typeof props.stroke === 'string' ? props.stroke : defaultStrokeColor
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const material = new THREE.MeshBasicMaterial({
|
|
47
|
+
color: strokeColor,
|
|
48
|
+
side: THREE.DoubleSide,
|
|
49
|
+
depthWrite: false,
|
|
50
|
+
depthTest: false
|
|
51
|
+
})
|
|
52
|
+
const strokeMesh = new THREE.Mesh(geometry, material)
|
|
53
|
+
nodeGroup.add(strokeMesh)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (node.tagName === 'path') {
|
|
58
|
+
const vertices: [number, number, number][] = node.points.map(([x, y]) => [x, y, 0])
|
|
59
|
+
let previousStart = 0
|
|
60
|
+
node.subpathIndices.forEach((index) => {
|
|
61
|
+
if (previousStart !== index) {
|
|
62
|
+
const subpathIsClosed = isApproximatelyEqual2d3d(
|
|
63
|
+
vertices[previousStart],
|
|
64
|
+
vertices[index - 1]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if (subpathIsClosed && hasFill) {
|
|
68
|
+
const shape = new THREE.Shape(
|
|
69
|
+
node.points.slice(previousStart, index).map(([x, y]) => new THREE.Vector2(x, y))
|
|
70
|
+
)
|
|
71
|
+
const fillColor = COLORS.black
|
|
72
|
+
|
|
73
|
+
const mesh = new THREE.Mesh(
|
|
74
|
+
new THREE.ShapeGeometry(shape),
|
|
75
|
+
new THREE.MeshBasicMaterial({ color: fillColor, side: THREE.DoubleSide })
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
nodeGroup.add(mesh)
|
|
79
|
+
} else {
|
|
80
|
+
const geometry = pointsToStroke(
|
|
81
|
+
vertices.slice(previousStart, index),
|
|
82
|
+
(props['stroke-width'] as number) || 1
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const strokeColor = COLORS.black
|
|
86
|
+
|
|
87
|
+
const material = new THREE.MeshBasicMaterial({
|
|
88
|
+
color: strokeColor,
|
|
89
|
+
side: THREE.DoubleSide,
|
|
90
|
+
depthWrite: false,
|
|
91
|
+
depthTest: false
|
|
92
|
+
})
|
|
93
|
+
const strokeMesh = new THREE.Mesh(geometry, material)
|
|
94
|
+
nodeGroup.add(strokeMesh)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
previousStart = index
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const child of node.children) {
|
|
103
|
+
if (typeof child === 'string') continue
|
|
104
|
+
const childGroup = drawNode(child)
|
|
105
|
+
nodeGroup.add(childGroup)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return nodeGroup
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const node of vectorizedNodes) {
|
|
112
|
+
const group = drawNode(node)
|
|
113
|
+
rootGroup.add(group)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const originalBox = new THREE.Box3().setFromObject(rootGroup)
|
|
117
|
+
const originalWidth = originalBox.max.x - originalBox.min.x
|
|
118
|
+
if (originalWidth <= 0) return rootGroup
|
|
119
|
+
|
|
120
|
+
const scaleFactor = width / originalWidth
|
|
121
|
+
rootGroup.scale.set(scaleFactor, -scaleFactor, 1) // Single Y flip here
|
|
122
|
+
|
|
123
|
+
// Center the scaled group
|
|
124
|
+
const scaledBox = new THREE.Box3().setFromObject(rootGroup)
|
|
125
|
+
const center = scaledBox.getCenter(new THREE.Vector3())
|
|
126
|
+
rootGroup.position.sub(center)
|
|
127
|
+
|
|
128
|
+
return rootGroup
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function pointsToStroke(points: [number, number, number][], width: number): THREE.BufferGeometry {
|
|
132
|
+
const halfWidth = width / 2
|
|
133
|
+
const leftOffsets: [number, number][] = []
|
|
134
|
+
const rightOffsets: [number, number][] = []
|
|
135
|
+
|
|
136
|
+
// Helper: compute a unit normal (perpendicular) from point A to B.
|
|
137
|
+
function computeNormal(
|
|
138
|
+
A: [number, number, number],
|
|
139
|
+
B: [number, number, number]
|
|
140
|
+
): [number, number, number] {
|
|
141
|
+
const dx = B[0] - A[0]
|
|
142
|
+
const dy = B[1] - A[1]
|
|
143
|
+
const len = Math.sqrt(dx * dx + dy * dy)
|
|
144
|
+
if (len === 0) return [0, 0, 0]
|
|
145
|
+
// Rotate by 90° counterclockwise: (-dy, dx)
|
|
146
|
+
return [-dy / len, dx / len, 0]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Use the normal of the first segment for the first point.
|
|
150
|
+
const nStart = computeNormal(points[0], points[1])
|
|
151
|
+
leftOffsets.push([points[0][0] + nStart[0] * halfWidth, points[0][1] + nStart[1] * halfWidth])
|
|
152
|
+
rightOffsets.push([points[0][0] - nStart[0] * halfWidth, points[0][1] - nStart[1] * halfWidth])
|
|
153
|
+
|
|
154
|
+
// For internal points, average the normals of adjacent segments.
|
|
155
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
156
|
+
const n1 = computeNormal(points[i - 1], points[i])
|
|
157
|
+
const n2 = computeNormal(points[i], points[i + 1])
|
|
158
|
+
// Average the two normals.
|
|
159
|
+
const avg: [number, number, number] = [n1[0] + n2[0], n1[1] + n2[1], 0]
|
|
160
|
+
const len = Math.sqrt(avg[0] * avg[0] + avg[1] * avg[1])
|
|
161
|
+
const normal: [number, number, number] = len === 0 ? n2 : [avg[0] / len, avg[1] / len, 0]
|
|
162
|
+
leftOffsets.push([points[i][0] + normal[0] * halfWidth, points[i][1] + normal[1] * halfWidth])
|
|
163
|
+
rightOffsets.push([points[i][0] - normal[0] * halfWidth, points[i][1] - normal[1] * halfWidth])
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Use the normal of the last segment for the last point.
|
|
167
|
+
const nEnd = computeNormal(points[points.length - 2], points[points.length - 1])
|
|
168
|
+
leftOffsets.push([
|
|
169
|
+
points[points.length - 1][0] + nEnd[0] * halfWidth,
|
|
170
|
+
points[points.length - 1][1] + nEnd[1] * halfWidth
|
|
171
|
+
])
|
|
172
|
+
rightOffsets.push([
|
|
173
|
+
points[points.length - 1][0] - nEnd[0] * halfWidth,
|
|
174
|
+
points[points.length - 1][1] - nEnd[1] * halfWidth
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
// Build triangles for each segment between consecutive points.
|
|
178
|
+
const positions: number[] = []
|
|
179
|
+
const normals: number[] = []
|
|
180
|
+
// All normals face +Z for a flat XY-plane.
|
|
181
|
+
const nz = [0, 0, 1]
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
184
|
+
// Create vertices for the quad:
|
|
185
|
+
// leftOffsets[i], rightOffsets[i], rightOffsets[i+1], leftOffsets[i+1]
|
|
186
|
+
const v0 = leftOffsets[i]
|
|
187
|
+
const v1 = rightOffsets[i]
|
|
188
|
+
const v2 = rightOffsets[i + 1]
|
|
189
|
+
const v3 = leftOffsets[i + 1]
|
|
190
|
+
|
|
191
|
+
// Triangle 1: v0, v1, v2
|
|
192
|
+
positions.push(v0[0], v0[1], 0)
|
|
193
|
+
positions.push(v1[0], v1[1], 0)
|
|
194
|
+
positions.push(v2[0], v2[1], 0)
|
|
195
|
+
|
|
196
|
+
// Triangle 2: v0, v2, v3
|
|
197
|
+
positions.push(v0[0], v0[1], 0)
|
|
198
|
+
positions.push(v2[0], v2[1], 0)
|
|
199
|
+
positions.push(v3[0], v3[1], 0)
|
|
200
|
+
|
|
201
|
+
// Each triangle has 3 vertices, add normals for each (pointing up)
|
|
202
|
+
for (let j = 0; j < 6; j++) {
|
|
203
|
+
normals.push(...nz)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const geometry = new THREE.BufferGeometry()
|
|
208
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
209
|
+
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
|
|
210
|
+
geometry.computeBoundingSphere()
|
|
211
|
+
|
|
212
|
+
return geometry
|
|
213
|
+
}
|