skotch 0.0.1
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/README.md +7 -0
- package/bun.lock +26 -0
- package/index.html +315 -0
- package/main.ts +950 -0
- package/package.json +16 -0
- package/tsconfig.json +29 -0
package/README.md
ADDED
package/bun.lock
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "skotch",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/bun": "latest",
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"typescript": "^5",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
"packages": {
|
|
16
|
+
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
|
17
|
+
|
|
18
|
+
"@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
|
19
|
+
|
|
20
|
+
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
|
21
|
+
|
|
22
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
23
|
+
|
|
24
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
25
|
+
}
|
|
26
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Skotch Demo</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 20px;
|
|
12
|
+
font-family: Arial, sans-serif;
|
|
13
|
+
background: #1a1a2e;
|
|
14
|
+
color: #eee;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
h1 {
|
|
18
|
+
text-align: center;
|
|
19
|
+
color: #4a9eff;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.container {
|
|
23
|
+
max-width: 1200px;
|
|
24
|
+
margin: 0 auto;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.demo-section {
|
|
28
|
+
margin: 30px 0;
|
|
29
|
+
background: #16213e;
|
|
30
|
+
border-radius: 10px;
|
|
31
|
+
padding: 20px;
|
|
32
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
h2 {
|
|
36
|
+
color: #4a9eff;
|
|
37
|
+
margin-top: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
canvas {
|
|
41
|
+
display: block;
|
|
42
|
+
margin: 20px auto;
|
|
43
|
+
background: #0f1419;
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
box-shadow: 0 0 20px rgba(74, 158, 255, 0.2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.controls {
|
|
49
|
+
text-align: center;
|
|
50
|
+
margin-top: 15px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
button {
|
|
54
|
+
background: #4a9eff;
|
|
55
|
+
color: white;
|
|
56
|
+
border: none;
|
|
57
|
+
padding: 10px 20px;
|
|
58
|
+
margin: 5px;
|
|
59
|
+
border-radius: 5px;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
font-size: 14px;
|
|
62
|
+
transition: background 0.3s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
button:hover {
|
|
66
|
+
background: #357abd;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
code {
|
|
70
|
+
background: #0f1419;
|
|
71
|
+
padding: 2px 6px;
|
|
72
|
+
border-radius: 3px;
|
|
73
|
+
color: #4a9eff;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pre {
|
|
77
|
+
background: #0f1419;
|
|
78
|
+
padding: 15px;
|
|
79
|
+
border-radius: 5px;
|
|
80
|
+
overflow-x: auto;
|
|
81
|
+
border-left: 4px solid #4a9eff;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
84
|
+
</head>
|
|
85
|
+
|
|
86
|
+
<body>
|
|
87
|
+
<div class="container">
|
|
88
|
+
<h1>Skotch Library Demo</h1>
|
|
89
|
+
|
|
90
|
+
<div class="demo-section">
|
|
91
|
+
<h2>1. Rotating Cube</h2>
|
|
92
|
+
<p>A simple spinning cube with automatic rotation animation.</p>
|
|
93
|
+
<canvas id="canvas1" width="800" height="600"></canvas>
|
|
94
|
+
<div class="controls">
|
|
95
|
+
<button id="btn1">Toggle Animation</button>
|
|
96
|
+
</div>
|
|
97
|
+
<pre><code>const scene = Easy3D.createScene('canvas1');
|
|
98
|
+
const cube = Easy3D.createCube(1.5, [1, 0.5, 0.2, 1]);
|
|
99
|
+
scene.add(cube);
|
|
100
|
+
|
|
101
|
+
scene.onAnimate(() => {
|
|
102
|
+
cube.transform.rotation.y += 0.01;
|
|
103
|
+
cube.transform.rotation.x += 0.005;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
scene.startAnimation();</code></pre>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="demo-section">
|
|
110
|
+
<h2>2. Multiple Objects</h2>
|
|
111
|
+
<p>Multiple geometric primitives with different animations.</p>
|
|
112
|
+
<canvas id="canvas2" width="800" height="600"></canvas>
|
|
113
|
+
<div class="controls">
|
|
114
|
+
<button id="btn2">Toggle Animation</button>
|
|
115
|
+
</div>
|
|
116
|
+
<pre><code>const scene = Easy3D.createScene('canvas2');
|
|
117
|
+
|
|
118
|
+
const sphere = Easy3D.createSphere(0.8, 32, [0.2, 0.5, 1, 1]);
|
|
119
|
+
sphere.transform.position.x = -2;
|
|
120
|
+
|
|
121
|
+
const pyramid = Easy3D.createPyramid(1.2, [1, 0.8, 0.2, 1]);
|
|
122
|
+
pyramid.transform.position.x = 2;
|
|
123
|
+
|
|
124
|
+
const torus = Easy3D.createTorus(0.8, 0.3, 32, [0.8, 0.2, 0.8, 1]);
|
|
125
|
+
|
|
126
|
+
scene.add(sphere);
|
|
127
|
+
scene.add(pyramid);
|
|
128
|
+
scene.add(torus);
|
|
129
|
+
|
|
130
|
+
let time = 0;
|
|
131
|
+
scene.onAnimate((dt) => {
|
|
132
|
+
time += dt;
|
|
133
|
+
sphere.transform.rotation.y += dt;
|
|
134
|
+
pyramid.transform.rotation.y -= dt * 0.5;
|
|
135
|
+
torus.transform.rotation.x += dt * 0.8;
|
|
136
|
+
});</code></pre>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="demo-section">
|
|
140
|
+
<h2>3. Orbital Camera</h2>
|
|
141
|
+
<p>Camera orbiting around a central object.</p>
|
|
142
|
+
<canvas id="canvas3" width="800" height="600"></canvas>
|
|
143
|
+
<div class="controls">
|
|
144
|
+
<button id="btn3">Toggle Animation</button>
|
|
145
|
+
</div>
|
|
146
|
+
<pre><code>const scene = Easy3D.createScene('canvas3');
|
|
147
|
+
const cube = Easy3D.createCube(1.5, [0.3, 0.9, 0.6, 1]);
|
|
148
|
+
scene.add(cube);
|
|
149
|
+
|
|
150
|
+
let angle = 0;
|
|
151
|
+
scene.onAnimate((dt) => {
|
|
152
|
+
angle += dt * 0.5;
|
|
153
|
+
const camera = scene.getCamera();
|
|
154
|
+
camera.position.x = Math.cos(angle) * 5;
|
|
155
|
+
camera.position.z = Math.sin(angle) * 5;
|
|
156
|
+
camera.position.y = 2 + Math.sin(angle * 2);
|
|
157
|
+
});</code></pre>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="demo-section">
|
|
161
|
+
<h2>4. Complex Animation</h2>
|
|
162
|
+
<p>Solar system-like animation with multiple objects.</p>
|
|
163
|
+
<canvas id="canvas4" width="800" height="600"></canvas>
|
|
164
|
+
<div class="controls">
|
|
165
|
+
<button id="btn4">Toggle Animation</button>
|
|
166
|
+
</div>
|
|
167
|
+
<pre><code>const scene = Easy3D.createScene('canvas4');
|
|
168
|
+
|
|
169
|
+
const sun = Skotch.createSphere(1, 32, [1, 0.8, 0.2, 1]);
|
|
170
|
+
const planet = Skotch.createSphere(0.4, 24, [0.2, 0.6, 1, 1]);
|
|
171
|
+
const moon = Skotch.createSphere(0.2, 16, [0.7, 0.7, 0.7, 1]);
|
|
172
|
+
|
|
173
|
+
scene.add(sun);
|
|
174
|
+
scene.add(planet);
|
|
175
|
+
scene.add(moon);
|
|
176
|
+
|
|
177
|
+
let time = 0;
|
|
178
|
+
scene.onAnimate((dt) => {
|
|
179
|
+
time += dt;
|
|
180
|
+
sun.transform.rotation.y += dt * 0.2;
|
|
181
|
+
|
|
182
|
+
planet.transform.position.x = Math.cos(time) * 3;
|
|
183
|
+
planet.transform.position.z = Math.sin(time) * 3;
|
|
184
|
+
|
|
185
|
+
moon.transform.position.x = planet.transform.position.x + Math.cos(time * 3) * 0.8;
|
|
186
|
+
moon.transform.position.z = planet.transform.position.z + Math.sin(time * 3) * 0.8;
|
|
187
|
+
});</code></pre>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<script type="module">
|
|
192
|
+
import Skotch from './skotch.js';
|
|
193
|
+
|
|
194
|
+
const scene1 = Skotch.createScene('canvas1');
|
|
195
|
+
const cube1 = Skotch.createCube(1.5, [1, 0.5, 0.2, 1]);
|
|
196
|
+
scene1.add(cube1);
|
|
197
|
+
|
|
198
|
+
scene1.onAnimate(() => {
|
|
199
|
+
cube1.transform.rotation.y += 0.01;
|
|
200
|
+
cube1.transform.rotation.x += 0.005;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
scene1.startAnimation();
|
|
204
|
+
|
|
205
|
+
document.getElementById('btn1').addEventListener('click', () => {
|
|
206
|
+
if (scene1.isAnimating) {
|
|
207
|
+
scene1.stopAnimation();
|
|
208
|
+
} else {
|
|
209
|
+
scene1.startAnimation();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const scene2 = Skotch.createScene('canvas2');
|
|
214
|
+
const sphere = Skotch.createSphere(0.8, 32, [0.2, 0.5, 1, 1]);
|
|
215
|
+
sphere.transform.position.x = -2;
|
|
216
|
+
|
|
217
|
+
const pyramid = Skotch.createPyramid(1.2, [1, 0.8, 0.2, 1]);
|
|
218
|
+
pyramid.transform.position.x = 2;
|
|
219
|
+
|
|
220
|
+
const torus = Skotch.createTorus(0.8, 0.3, 32, [0.8, 0.2, 0.8, 1]);
|
|
221
|
+
|
|
222
|
+
scene2.add(sphere);
|
|
223
|
+
scene2.add(pyramid);
|
|
224
|
+
scene2.add(torus);
|
|
225
|
+
|
|
226
|
+
let time2 = 0;
|
|
227
|
+
scene2.onAnimate((dt) => {
|
|
228
|
+
time2 += dt;
|
|
229
|
+
sphere.transform.rotation.y += dt;
|
|
230
|
+
sphere.transform.position.y = Math.sin(time2 * 2) * 0.3;
|
|
231
|
+
|
|
232
|
+
pyramid.transform.rotation.y -= dt * 0.5;
|
|
233
|
+
pyramid.transform.position.y = Math.cos(time2 * 1.5) * 0.3;
|
|
234
|
+
|
|
235
|
+
torus.transform.rotation.x += dt * 0.8;
|
|
236
|
+
torus.transform.rotation.y += dt * 0.4;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
scene2.startAnimation();
|
|
240
|
+
|
|
241
|
+
document.getElementById('btn2').addEventListener('click', () => {
|
|
242
|
+
if (scene2.isAnimating) {
|
|
243
|
+
scene2.stopAnimation();
|
|
244
|
+
} else {
|
|
245
|
+
scene2.startAnimation();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const scene3 = Skotch.createScene('canvas3');
|
|
250
|
+
const cube3 = Skotch.createCube(1.5, [0.3, 0.9, 0.6, 1]);
|
|
251
|
+
scene3.add(cube3);
|
|
252
|
+
|
|
253
|
+
let angle3 = 0;
|
|
254
|
+
scene3.onAnimate((dt) => {
|
|
255
|
+
angle3 += dt * 0.5;
|
|
256
|
+
const camera = scene3.getCamera();
|
|
257
|
+
camera.position.x = Math.cos(angle3) * 5;
|
|
258
|
+
camera.position.z = Math.sin(angle3) * 5;
|
|
259
|
+
camera.position.y = 2 + Math.sin(angle3 * 2);
|
|
260
|
+
|
|
261
|
+
cube3.transform.rotation.y += dt * 0.3;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
scene3.startAnimation();
|
|
265
|
+
|
|
266
|
+
document.getElementById('btn3').addEventListener('click', () => {
|
|
267
|
+
if (scene3.isAnimating) {
|
|
268
|
+
scene3.stopAnimation();
|
|
269
|
+
} else {
|
|
270
|
+
scene3.startAnimation();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const scene4 = Skotch.createScene('canvas4');
|
|
275
|
+
const sun = Skotch.createSphere(1, 32, [1, 0.8, 0.2, 1]);
|
|
276
|
+
const planet = Skotch.createSphere(0.4, 24, [0.2, 0.6, 1, 1]);
|
|
277
|
+
const moon = Skotch.createSphere(0.2, 16, [0.7, 0.7, 0.7, 1]);
|
|
278
|
+
|
|
279
|
+
scene4.add(sun);
|
|
280
|
+
scene4.add(planet);
|
|
281
|
+
scene4.add(moon);
|
|
282
|
+
|
|
283
|
+
const camera4 = scene4.getCamera();
|
|
284
|
+
camera4.position.y = 4;
|
|
285
|
+
camera4.position.z = 8;
|
|
286
|
+
|
|
287
|
+
let time4 = 0;
|
|
288
|
+
|
|
289
|
+
scene4.onAnimate((dt) => {
|
|
290
|
+
time4 += dt;
|
|
291
|
+
|
|
292
|
+
sun.transform.rotation.y += dt * 0.2;
|
|
293
|
+
|
|
294
|
+
planet.transform.position.x = Math.cos(time4) * 3;
|
|
295
|
+
planet.transform.position.z = Math.sin(time4) * 3;
|
|
296
|
+
planet.transform.rotation.y += dt;
|
|
297
|
+
|
|
298
|
+
moon.transform.position.x = planet.transform.position.x + Math.cos(time4 * 3) * 0.8;
|
|
299
|
+
moon.transform.position.z = planet.transform.position.z + Math.sin(time4 * 3) * 0.8;
|
|
300
|
+
moon.transform.position.y = Math.sin(time4 * 3) * 0.3;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
scene4.startAnimation();
|
|
304
|
+
|
|
305
|
+
document.getElementById('btn4').addEventListener('click', () => {
|
|
306
|
+
if (scene4.isAnimating) {
|
|
307
|
+
scene4.stopAnimation();
|
|
308
|
+
} else {
|
|
309
|
+
scene4.startAnimation();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
</script>
|
|
313
|
+
</body>
|
|
314
|
+
|
|
315
|
+
</html>
|
package/main.ts
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skotch - 3D Graphics Library
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) 2025 - Navid M.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Some 3D vector.
|
|
9
|
+
*/
|
|
10
|
+
export class Vec3 {
|
|
11
|
+
constructor(
|
|
12
|
+
public x: number = 0,
|
|
13
|
+
public y: number = 0,
|
|
14
|
+
public z: number = 0,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
add(v: Vec3): Vec3 {
|
|
18
|
+
return new Vec3(this.x + v.x, this.y + v.y, this.z + v.z);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
subtract(v: Vec3): Vec3 {
|
|
22
|
+
return new Vec3(this.x - v.x, this.y - v.y, this.z - v.z);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
multiply(scalar: number): Vec3 {
|
|
26
|
+
return new Vec3(this.x * scalar, this.y * scalar, this.z * scalar);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
normalize(): Vec3 {
|
|
30
|
+
const len = Math.sqrt(
|
|
31
|
+
this.x * this.x + this.y * this.y + this.z * this.z,
|
|
32
|
+
);
|
|
33
|
+
return len > 0
|
|
34
|
+
? new Vec3(this.x / len, this.y / len, this.z / len)
|
|
35
|
+
: new Vec3();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cross(v: Vec3): Vec3 {
|
|
39
|
+
return new Vec3(
|
|
40
|
+
this.y * v.z - this.z * v.y,
|
|
41
|
+
this.z * v.x - this.x * v.z,
|
|
42
|
+
this.x * v.y - this.y * v.x,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static zero(): Vec3 {
|
|
47
|
+
return new Vec3(0, 0, 0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static up(): Vec3 {
|
|
51
|
+
return new Vec3(0, 1, 0);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class Mat4 {
|
|
56
|
+
data: Float32Array;
|
|
57
|
+
|
|
58
|
+
constructor(data?: number[]) {
|
|
59
|
+
this.data = new Float32Array(data || Mat4.identityData());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static identityData(): number[] {
|
|
63
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static identity(): Mat4 {
|
|
67
|
+
return new Mat4();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static perspective(
|
|
71
|
+
fov: number,
|
|
72
|
+
aspect: number,
|
|
73
|
+
near: number,
|
|
74
|
+
far: number,
|
|
75
|
+
): Mat4 {
|
|
76
|
+
const f = 1.0 / Math.tan(fov / 2);
|
|
77
|
+
const rangeInv = 1 / (near - far);
|
|
78
|
+
|
|
79
|
+
return new Mat4([
|
|
80
|
+
f / aspect,
|
|
81
|
+
0,
|
|
82
|
+
0,
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
f,
|
|
86
|
+
0,
|
|
87
|
+
0,
|
|
88
|
+
0,
|
|
89
|
+
0,
|
|
90
|
+
(near + far) * rangeInv,
|
|
91
|
+
-1,
|
|
92
|
+
0,
|
|
93
|
+
0,
|
|
94
|
+
near * far * rangeInv * 2,
|
|
95
|
+
0,
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static lookAt(eye: Vec3, target: Vec3, up: Vec3): Mat4 {
|
|
100
|
+
const z = eye.subtract(target).normalize();
|
|
101
|
+
const x = up.cross(z).normalize();
|
|
102
|
+
const y = z.cross(x).normalize();
|
|
103
|
+
|
|
104
|
+
return new Mat4([
|
|
105
|
+
x.x,
|
|
106
|
+
y.x,
|
|
107
|
+
z.x,
|
|
108
|
+
0,
|
|
109
|
+
x.y,
|
|
110
|
+
y.y,
|
|
111
|
+
z.y,
|
|
112
|
+
0,
|
|
113
|
+
x.z,
|
|
114
|
+
y.z,
|
|
115
|
+
z.z,
|
|
116
|
+
0,
|
|
117
|
+
-x.x * eye.x - x.y * eye.y - x.z * eye.z,
|
|
118
|
+
-y.x * eye.x - y.y * eye.y - y.z * eye.z,
|
|
119
|
+
-z.x * eye.x - z.y * eye.y - z.z * eye.z,
|
|
120
|
+
1,
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static translation(x: number, y: number, z: number): Mat4 {
|
|
125
|
+
return new Mat4([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static rotationX(angle: number): Mat4 {
|
|
129
|
+
const c = Math.cos(angle);
|
|
130
|
+
const s = Math.sin(angle);
|
|
131
|
+
return new Mat4([1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static rotationY(angle: number): Mat4 {
|
|
135
|
+
const c = Math.cos(angle);
|
|
136
|
+
const s = Math.sin(angle);
|
|
137
|
+
return new Mat4([c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static rotationZ(angle: number): Mat4 {
|
|
141
|
+
const c = Math.cos(angle);
|
|
142
|
+
const s = Math.sin(angle);
|
|
143
|
+
return new Mat4([c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static scaling(x: number, y: number, z: number): Mat4 {
|
|
147
|
+
return new Mat4([x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
multiply(other: Mat4): Mat4 {
|
|
151
|
+
const result = new Float32Array(16);
|
|
152
|
+
const a = this.data;
|
|
153
|
+
const b = other.data;
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < 4; i++) {
|
|
156
|
+
for (let j = 0; j < 4; j++) {
|
|
157
|
+
result[i * 4 + j] =
|
|
158
|
+
a[i * 4 + 0] * b[0 * 4 + j] +
|
|
159
|
+
a[i * 4 + 1] * b[1 * 4 + j] +
|
|
160
|
+
a[i * 4 + 2] * b[2 * 4 + j] +
|
|
161
|
+
a[i * 4 + 3] * b[3 * 4 + j];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return new Mat4(Array.from(result));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface Geometry {
|
|
170
|
+
vertices: number[];
|
|
171
|
+
indices: number[];
|
|
172
|
+
normals: number[];
|
|
173
|
+
colors: number[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class GeometryBuilder {
|
|
177
|
+
static cube(
|
|
178
|
+
size: number = 1,
|
|
179
|
+
color: number[] = [1, 0.5, 0.2, 1],
|
|
180
|
+
): Geometry {
|
|
181
|
+
const s = size / 2;
|
|
182
|
+
|
|
183
|
+
const vertices = [
|
|
184
|
+
-s,
|
|
185
|
+
-s,
|
|
186
|
+
s,
|
|
187
|
+
s,
|
|
188
|
+
-s,
|
|
189
|
+
s,
|
|
190
|
+
s,
|
|
191
|
+
s,
|
|
192
|
+
s,
|
|
193
|
+
-s,
|
|
194
|
+
s,
|
|
195
|
+
s,
|
|
196
|
+
// Back face
|
|
197
|
+
-s,
|
|
198
|
+
-s,
|
|
199
|
+
-s,
|
|
200
|
+
-s,
|
|
201
|
+
s,
|
|
202
|
+
-s,
|
|
203
|
+
s,
|
|
204
|
+
s,
|
|
205
|
+
-s,
|
|
206
|
+
s,
|
|
207
|
+
-s,
|
|
208
|
+
-s,
|
|
209
|
+
// Top face
|
|
210
|
+
-s,
|
|
211
|
+
s,
|
|
212
|
+
-s,
|
|
213
|
+
-s,
|
|
214
|
+
s,
|
|
215
|
+
s,
|
|
216
|
+
s,
|
|
217
|
+
s,
|
|
218
|
+
s,
|
|
219
|
+
s,
|
|
220
|
+
s,
|
|
221
|
+
-s,
|
|
222
|
+
// Bottom face
|
|
223
|
+
-s,
|
|
224
|
+
-s,
|
|
225
|
+
-s,
|
|
226
|
+
s,
|
|
227
|
+
-s,
|
|
228
|
+
-s,
|
|
229
|
+
s,
|
|
230
|
+
-s,
|
|
231
|
+
s,
|
|
232
|
+
-s,
|
|
233
|
+
-s,
|
|
234
|
+
s,
|
|
235
|
+
// Right face
|
|
236
|
+
s,
|
|
237
|
+
-s,
|
|
238
|
+
-s,
|
|
239
|
+
s,
|
|
240
|
+
s,
|
|
241
|
+
-s,
|
|
242
|
+
s,
|
|
243
|
+
s,
|
|
244
|
+
s,
|
|
245
|
+
s,
|
|
246
|
+
-s,
|
|
247
|
+
s,
|
|
248
|
+
// Left face
|
|
249
|
+
-s,
|
|
250
|
+
-s,
|
|
251
|
+
-s,
|
|
252
|
+
-s,
|
|
253
|
+
-s,
|
|
254
|
+
s,
|
|
255
|
+
-s,
|
|
256
|
+
s,
|
|
257
|
+
s,
|
|
258
|
+
-s,
|
|
259
|
+
s,
|
|
260
|
+
-s,
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const indices = [
|
|
264
|
+
0,
|
|
265
|
+
1,
|
|
266
|
+
2,
|
|
267
|
+
0,
|
|
268
|
+
2,
|
|
269
|
+
3, // front
|
|
270
|
+
4,
|
|
271
|
+
5,
|
|
272
|
+
6,
|
|
273
|
+
4,
|
|
274
|
+
6,
|
|
275
|
+
7, // back
|
|
276
|
+
8,
|
|
277
|
+
9,
|
|
278
|
+
10,
|
|
279
|
+
8,
|
|
280
|
+
10,
|
|
281
|
+
11, // top
|
|
282
|
+
12,
|
|
283
|
+
13,
|
|
284
|
+
14,
|
|
285
|
+
12,
|
|
286
|
+
14,
|
|
287
|
+
15, // bottom
|
|
288
|
+
16,
|
|
289
|
+
17,
|
|
290
|
+
18,
|
|
291
|
+
16,
|
|
292
|
+
18,
|
|
293
|
+
19, // right
|
|
294
|
+
20,
|
|
295
|
+
21,
|
|
296
|
+
22,
|
|
297
|
+
20,
|
|
298
|
+
22,
|
|
299
|
+
23, // left
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const normals = [
|
|
303
|
+
0,
|
|
304
|
+
0,
|
|
305
|
+
1,
|
|
306
|
+
0,
|
|
307
|
+
0,
|
|
308
|
+
1,
|
|
309
|
+
0,
|
|
310
|
+
0,
|
|
311
|
+
1,
|
|
312
|
+
0,
|
|
313
|
+
0,
|
|
314
|
+
1, // front
|
|
315
|
+
0,
|
|
316
|
+
0,
|
|
317
|
+
-1,
|
|
318
|
+
0,
|
|
319
|
+
0,
|
|
320
|
+
-1,
|
|
321
|
+
0,
|
|
322
|
+
0,
|
|
323
|
+
-1,
|
|
324
|
+
0,
|
|
325
|
+
0,
|
|
326
|
+
-1, // back
|
|
327
|
+
0,
|
|
328
|
+
1,
|
|
329
|
+
0,
|
|
330
|
+
0,
|
|
331
|
+
1,
|
|
332
|
+
0,
|
|
333
|
+
0,
|
|
334
|
+
1,
|
|
335
|
+
0,
|
|
336
|
+
0,
|
|
337
|
+
1,
|
|
338
|
+
0, // top
|
|
339
|
+
0,
|
|
340
|
+
-1,
|
|
341
|
+
0,
|
|
342
|
+
0,
|
|
343
|
+
-1,
|
|
344
|
+
0,
|
|
345
|
+
0,
|
|
346
|
+
-1,
|
|
347
|
+
0,
|
|
348
|
+
0,
|
|
349
|
+
-1,
|
|
350
|
+
0, // bottom
|
|
351
|
+
1,
|
|
352
|
+
0,
|
|
353
|
+
0,
|
|
354
|
+
1,
|
|
355
|
+
0,
|
|
356
|
+
0,
|
|
357
|
+
1,
|
|
358
|
+
0,
|
|
359
|
+
0,
|
|
360
|
+
1,
|
|
361
|
+
0,
|
|
362
|
+
0, // right
|
|
363
|
+
-1,
|
|
364
|
+
0,
|
|
365
|
+
0,
|
|
366
|
+
-1,
|
|
367
|
+
0,
|
|
368
|
+
0,
|
|
369
|
+
-1,
|
|
370
|
+
0,
|
|
371
|
+
0,
|
|
372
|
+
-1,
|
|
373
|
+
0,
|
|
374
|
+
0, // left
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
const colors = [];
|
|
378
|
+
for (let i = 0; i < 24; i++) {
|
|
379
|
+
colors.push(...color);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { vertices, indices, normals, colors };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
static sphere(
|
|
386
|
+
radius: number = 1,
|
|
387
|
+
segments: number = 32,
|
|
388
|
+
color: number[] = [0.2, 0.5, 1, 1],
|
|
389
|
+
): Geometry {
|
|
390
|
+
const vertices: number[] = [];
|
|
391
|
+
const indices: number[] = [];
|
|
392
|
+
const normals: number[] = [];
|
|
393
|
+
const colors: number[] = [];
|
|
394
|
+
|
|
395
|
+
for (let lat = 0; lat <= segments; lat++) {
|
|
396
|
+
const theta = (lat * Math.PI) / segments;
|
|
397
|
+
const sinTheta = Math.sin(theta);
|
|
398
|
+
const cosTheta = Math.cos(theta);
|
|
399
|
+
|
|
400
|
+
for (let lon = 0; lon <= segments; lon++) {
|
|
401
|
+
const phi = (lon * 2 * Math.PI) / segments;
|
|
402
|
+
const sinPhi = Math.sin(phi);
|
|
403
|
+
const cosPhi = Math.cos(phi);
|
|
404
|
+
|
|
405
|
+
const x = cosPhi * sinTheta;
|
|
406
|
+
const y = cosTheta;
|
|
407
|
+
const z = sinPhi * sinTheta;
|
|
408
|
+
|
|
409
|
+
vertices.push(radius * x, radius * y, radius * z);
|
|
410
|
+
normals.push(x, y, z);
|
|
411
|
+
colors.push(...color);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (let lat = 0; lat < segments; lat++) {
|
|
416
|
+
for (let lon = 0; lon < segments; lon++) {
|
|
417
|
+
const first = lat * (segments + 1) + lon;
|
|
418
|
+
const second = first + segments + 1;
|
|
419
|
+
|
|
420
|
+
indices.push(first, second, first + 1);
|
|
421
|
+
indices.push(second, second + 1, first + 1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return { vertices, indices, normals, colors };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
static pyramid(
|
|
429
|
+
size: number = 1,
|
|
430
|
+
color: number[] = [1, 0.8, 0.2, 1],
|
|
431
|
+
): Geometry {
|
|
432
|
+
const s = size / 2;
|
|
433
|
+
|
|
434
|
+
const vertices = [
|
|
435
|
+
-s,
|
|
436
|
+
0,
|
|
437
|
+
-s,
|
|
438
|
+
s,
|
|
439
|
+
0,
|
|
440
|
+
-s,
|
|
441
|
+
s,
|
|
442
|
+
0,
|
|
443
|
+
s,
|
|
444
|
+
-s,
|
|
445
|
+
0,
|
|
446
|
+
s,
|
|
447
|
+
0,
|
|
448
|
+
size,
|
|
449
|
+
0,
|
|
450
|
+
0,
|
|
451
|
+
size,
|
|
452
|
+
0,
|
|
453
|
+
0,
|
|
454
|
+
size,
|
|
455
|
+
0,
|
|
456
|
+
0,
|
|
457
|
+
size,
|
|
458
|
+
0,
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
const indices = [
|
|
462
|
+
0,
|
|
463
|
+
1,
|
|
464
|
+
2,
|
|
465
|
+
0,
|
|
466
|
+
2,
|
|
467
|
+
3, // base
|
|
468
|
+
0,
|
|
469
|
+
1,
|
|
470
|
+
4, // front
|
|
471
|
+
1,
|
|
472
|
+
2,
|
|
473
|
+
5, // right
|
|
474
|
+
2,
|
|
475
|
+
3,
|
|
476
|
+
6, // back
|
|
477
|
+
3,
|
|
478
|
+
0,
|
|
479
|
+
7, // left
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
const normals: number[] = [];
|
|
483
|
+
for (let i = 0; i < vertices.length / 3; i++) {
|
|
484
|
+
normals.push(0, 1, 0);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const colors: number[] = [];
|
|
488
|
+
for (let i = 0; i < vertices.length / 3; i++) {
|
|
489
|
+
colors.push(...color);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return { vertices, indices, normals, colors };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
static torus(
|
|
496
|
+
majorRadius: number = 1,
|
|
497
|
+
minorRadius: number = 0.3,
|
|
498
|
+
segments: number = 32,
|
|
499
|
+
color: number[] = [0.8, 0.2, 0.8, 1],
|
|
500
|
+
): Geometry {
|
|
501
|
+
const vertices: number[] = [];
|
|
502
|
+
const indices: number[] = [];
|
|
503
|
+
const normals: number[] = [];
|
|
504
|
+
const colors: number[] = [];
|
|
505
|
+
|
|
506
|
+
for (let i = 0; i <= segments; i++) {
|
|
507
|
+
const u = (i * 2 * Math.PI) / segments;
|
|
508
|
+
for (let j = 0; j <= segments; j++) {
|
|
509
|
+
const v = (j * 2 * Math.PI) / segments;
|
|
510
|
+
|
|
511
|
+
const x =
|
|
512
|
+
(majorRadius + minorRadius * Math.cos(v)) * Math.cos(u);
|
|
513
|
+
const y = minorRadius * Math.sin(v);
|
|
514
|
+
const z =
|
|
515
|
+
(majorRadius + minorRadius * Math.cos(v)) * Math.sin(u);
|
|
516
|
+
|
|
517
|
+
vertices.push(x, y, z);
|
|
518
|
+
|
|
519
|
+
const nx = Math.cos(v) * Math.cos(u);
|
|
520
|
+
const ny = Math.sin(v);
|
|
521
|
+
const nz = Math.cos(v) * Math.sin(u);
|
|
522
|
+
normals.push(nx, ny, nz);
|
|
523
|
+
|
|
524
|
+
colors.push(...color);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
for (let i = 0; i < segments; i++) {
|
|
529
|
+
for (let j = 0; j < segments; j++) {
|
|
530
|
+
const a = i * (segments + 1) + j;
|
|
531
|
+
const b = a + segments + 1;
|
|
532
|
+
|
|
533
|
+
indices.push(a, b, a + 1);
|
|
534
|
+
indices.push(b, b + 1, a + 1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return { vertices, indices, normals, colors };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export class Transform {
|
|
543
|
+
position: Vec3 = new Vec3(0, 0, 0);
|
|
544
|
+
rotation: Vec3 = new Vec3(0, 0, 0);
|
|
545
|
+
scale: Vec3 = new Vec3(1, 1, 1);
|
|
546
|
+
|
|
547
|
+
getMatrix(): Mat4 {
|
|
548
|
+
const translation = Mat4.translation(
|
|
549
|
+
this.position.x,
|
|
550
|
+
this.position.y,
|
|
551
|
+
this.position.z,
|
|
552
|
+
);
|
|
553
|
+
const rotationX = Mat4.rotationX(this.rotation.x);
|
|
554
|
+
const rotationY = Mat4.rotationY(this.rotation.y);
|
|
555
|
+
const rotationZ = Mat4.rotationZ(this.rotation.z);
|
|
556
|
+
const scaling = Mat4.scaling(this.scale.x, this.scale.y, this.scale.z);
|
|
557
|
+
|
|
558
|
+
return translation
|
|
559
|
+
.multiply(rotationY)
|
|
560
|
+
.multiply(rotationX)
|
|
561
|
+
.multiply(rotationZ)
|
|
562
|
+
.multiply(scaling);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export class Mesh {
|
|
567
|
+
geometry: Geometry;
|
|
568
|
+
transform: Transform = new Transform();
|
|
569
|
+
private buffers?: {
|
|
570
|
+
vertex: WebGLBuffer;
|
|
571
|
+
index: WebGLBuffer;
|
|
572
|
+
normal: WebGLBuffer;
|
|
573
|
+
color: WebGLBuffer;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
constructor(geometry: Geometry) {
|
|
577
|
+
this.geometry = geometry;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
initBuffers(gl: WebGLRenderingContext) {
|
|
581
|
+
const vertexBuffer = gl.createBuffer();
|
|
582
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
583
|
+
gl.bufferData(
|
|
584
|
+
gl.ARRAY_BUFFER,
|
|
585
|
+
new Float32Array(this.geometry.vertices),
|
|
586
|
+
gl.STATIC_DRAW,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const indexBuffer = gl.createBuffer();
|
|
590
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
591
|
+
gl.bufferData(
|
|
592
|
+
gl.ELEMENT_ARRAY_BUFFER,
|
|
593
|
+
new Uint16Array(this.geometry.indices),
|
|
594
|
+
gl.STATIC_DRAW,
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const normalBuffer = gl.createBuffer();
|
|
598
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
|
599
|
+
gl.bufferData(
|
|
600
|
+
gl.ARRAY_BUFFER,
|
|
601
|
+
new Float32Array(this.geometry.normals),
|
|
602
|
+
gl.STATIC_DRAW,
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const colorBuffer = gl.createBuffer();
|
|
606
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
|
|
607
|
+
gl.bufferData(
|
|
608
|
+
gl.ARRAY_BUFFER,
|
|
609
|
+
new Float32Array(this.geometry.colors),
|
|
610
|
+
gl.STATIC_DRAW,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
this.buffers = {
|
|
614
|
+
vertex: vertexBuffer!,
|
|
615
|
+
index: indexBuffer!,
|
|
616
|
+
normal: normalBuffer!,
|
|
617
|
+
color: colorBuffer!,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
getBuffers() {
|
|
622
|
+
return this.buffers;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export class Camera {
|
|
627
|
+
position: Vec3 = new Vec3(0, 0, 5);
|
|
628
|
+
target: Vec3 = new Vec3(0, 0, 0);
|
|
629
|
+
up: Vec3 = Vec3.up();
|
|
630
|
+
fov: number = Math.PI / 4;
|
|
631
|
+
aspect: number = 1;
|
|
632
|
+
near: number = 0.1;
|
|
633
|
+
far: number = 100;
|
|
634
|
+
|
|
635
|
+
getViewMatrix(): Mat4 {
|
|
636
|
+
return Mat4.lookAt(this.position, this.target, this.up);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
getProjectionMatrix(): Mat4 {
|
|
640
|
+
return Mat4.perspective(this.fov, this.aspect, this.near, this.far);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export class Renderer {
|
|
645
|
+
private gl: WebGLRenderingContext;
|
|
646
|
+
private program?: WebGLProgram;
|
|
647
|
+
private locations?: {
|
|
648
|
+
position: number;
|
|
649
|
+
normal: number;
|
|
650
|
+
color: number;
|
|
651
|
+
modelMatrix: WebGLUniformLocation;
|
|
652
|
+
viewMatrix: WebGLUniformLocation;
|
|
653
|
+
projectionMatrix: WebGLUniformLocation;
|
|
654
|
+
lightDirection: WebGLUniformLocation;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
constructor(canvas: HTMLCanvasElement) {
|
|
658
|
+
const gl = canvas.getContext("webgl");
|
|
659
|
+
if (!gl) {
|
|
660
|
+
throw new Error("WebGL not supported");
|
|
661
|
+
}
|
|
662
|
+
this.gl = gl;
|
|
663
|
+
this.initShaders();
|
|
664
|
+
this.setupGL();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private initShaders() {
|
|
668
|
+
const vertexShaderSource = `
|
|
669
|
+
attribute vec3 aPosition;
|
|
670
|
+
attribute vec3 aNormal;
|
|
671
|
+
attribute vec4 aColor;
|
|
672
|
+
|
|
673
|
+
uniform mat4 uModelMatrix;
|
|
674
|
+
uniform mat4 uViewMatrix;
|
|
675
|
+
uniform mat4 uProjectionMatrix;
|
|
676
|
+
uniform vec3 uLightDirection;
|
|
677
|
+
|
|
678
|
+
varying vec4 vColor;
|
|
679
|
+
varying float vLighting;
|
|
680
|
+
|
|
681
|
+
void main() {
|
|
682
|
+
gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aPosition, 1.0);
|
|
683
|
+
|
|
684
|
+
vec3 normal = normalize((uModelMatrix * vec4(aNormal, 0.0)).xyz);
|
|
685
|
+
float lighting = max(dot(normal, normalize(uLightDirection)), 0.3);
|
|
686
|
+
|
|
687
|
+
vColor = aColor;
|
|
688
|
+
vLighting = lighting;
|
|
689
|
+
}
|
|
690
|
+
`;
|
|
691
|
+
|
|
692
|
+
const fragmentShaderSource = `
|
|
693
|
+
precision mediump float;
|
|
694
|
+
|
|
695
|
+
varying vec4 vColor;
|
|
696
|
+
varying float vLighting;
|
|
697
|
+
|
|
698
|
+
void main() {
|
|
699
|
+
gl_FragColor = vec4(vColor.rgb * vLighting, vColor.a);
|
|
700
|
+
}
|
|
701
|
+
`;
|
|
702
|
+
|
|
703
|
+
const vertexShader = this.compileShader(
|
|
704
|
+
vertexShaderSource,
|
|
705
|
+
this.gl.VERTEX_SHADER,
|
|
706
|
+
);
|
|
707
|
+
const fragmentShader = this.compileShader(
|
|
708
|
+
fragmentShaderSource,
|
|
709
|
+
this.gl.FRAGMENT_SHADER,
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
const program = this.gl.createProgram()!;
|
|
713
|
+
this.gl.attachShader(program, vertexShader);
|
|
714
|
+
this.gl.attachShader(program, fragmentShader);
|
|
715
|
+
this.gl.linkProgram(program);
|
|
716
|
+
|
|
717
|
+
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
718
|
+
throw new Error(
|
|
719
|
+
"Program linking failed: " + this.gl.getProgramInfoLog(program),
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
this.program = program;
|
|
724
|
+
this.gl.useProgram(program);
|
|
725
|
+
|
|
726
|
+
this.locations = {
|
|
727
|
+
position: this.gl.getAttribLocation(program, "aPosition"),
|
|
728
|
+
normal: this.gl.getAttribLocation(program, "aNormal"),
|
|
729
|
+
color: this.gl.getAttribLocation(program, "aColor"),
|
|
730
|
+
modelMatrix: this.gl.getUniformLocation(program, "uModelMatrix")!,
|
|
731
|
+
viewMatrix: this.gl.getUniformLocation(program, "uViewMatrix")!,
|
|
732
|
+
projectionMatrix: this.gl.getUniformLocation(
|
|
733
|
+
program,
|
|
734
|
+
"uProjectionMatrix",
|
|
735
|
+
)!,
|
|
736
|
+
lightDirection: this.gl.getUniformLocation(
|
|
737
|
+
program,
|
|
738
|
+
"uLightDirection",
|
|
739
|
+
)!,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private compileShader(source: string, type: number): WebGLShader {
|
|
744
|
+
const shader = this.gl.createShader(type)!;
|
|
745
|
+
this.gl.shaderSource(shader, source);
|
|
746
|
+
this.gl.compileShader(shader);
|
|
747
|
+
|
|
748
|
+
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
"Shader compilation failed: " +
|
|
751
|
+
this.gl.getShaderInfoLog(shader),
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return shader;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private setupGL() {
|
|
759
|
+
this.gl.enable(this.gl.DEPTH_TEST);
|
|
760
|
+
this.gl.enable(this.gl.CULL_FACE);
|
|
761
|
+
this.gl.clearColor(0.1, 0.1, 0.15, 1.0);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
clear() {
|
|
765
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
render(mesh: Mesh, camera: Camera) {
|
|
769
|
+
if (!this.program || !this.locations) return;
|
|
770
|
+
|
|
771
|
+
const buffers = mesh.getBuffers();
|
|
772
|
+
if (!buffers) {
|
|
773
|
+
mesh.initBuffers(this.gl);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const b = mesh.getBuffers()!;
|
|
777
|
+
|
|
778
|
+
this.gl.uniformMatrix4fv(
|
|
779
|
+
this.locations.modelMatrix,
|
|
780
|
+
false,
|
|
781
|
+
mesh.transform.getMatrix().data,
|
|
782
|
+
);
|
|
783
|
+
this.gl.uniformMatrix4fv(
|
|
784
|
+
this.locations.viewMatrix,
|
|
785
|
+
false,
|
|
786
|
+
camera.getViewMatrix().data,
|
|
787
|
+
);
|
|
788
|
+
this.gl.uniformMatrix4fv(
|
|
789
|
+
this.locations.projectionMatrix,
|
|
790
|
+
false,
|
|
791
|
+
camera.getProjectionMatrix().data,
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
this.gl.uniform3f(this.locations.lightDirection, 0.5, 0.7, 1.0);
|
|
795
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, b.vertex);
|
|
796
|
+
this.gl.enableVertexAttribArray(this.locations.position);
|
|
797
|
+
this.gl.vertexAttribPointer(
|
|
798
|
+
this.locations.position,
|
|
799
|
+
3,
|
|
800
|
+
this.gl.FLOAT,
|
|
801
|
+
false,
|
|
802
|
+
0,
|
|
803
|
+
0,
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, b.normal);
|
|
807
|
+
this.gl.enableVertexAttribArray(this.locations.normal);
|
|
808
|
+
this.gl.vertexAttribPointer(
|
|
809
|
+
this.locations.normal,
|
|
810
|
+
3,
|
|
811
|
+
this.gl.FLOAT,
|
|
812
|
+
false,
|
|
813
|
+
0,
|
|
814
|
+
0,
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, b.color);
|
|
818
|
+
this.gl.enableVertexAttribArray(this.locations.color);
|
|
819
|
+
this.gl.vertexAttribPointer(
|
|
820
|
+
this.locations.color,
|
|
821
|
+
4,
|
|
822
|
+
this.gl.FLOAT,
|
|
823
|
+
false,
|
|
824
|
+
0,
|
|
825
|
+
0,
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, b.index);
|
|
829
|
+
this.gl.drawElements(
|
|
830
|
+
this.gl.TRIANGLES,
|
|
831
|
+
mesh.geometry.indices.length,
|
|
832
|
+
this.gl.UNSIGNED_SHORT,
|
|
833
|
+
0,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export class Scene {
|
|
839
|
+
private renderer: Renderer;
|
|
840
|
+
private camera: Camera;
|
|
841
|
+
private meshes: Mesh[] = [];
|
|
842
|
+
private animationCallbacks: ((time: number) => void)[] = [];
|
|
843
|
+
private isAnimating: boolean = false;
|
|
844
|
+
private lastTime: number = 0;
|
|
845
|
+
|
|
846
|
+
constructor(canvas: HTMLCanvasElement) {
|
|
847
|
+
this.renderer = new Renderer(canvas);
|
|
848
|
+
this.camera = new Camera();
|
|
849
|
+
this.camera.aspect = canvas.width / canvas.height;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
add(mesh: Mesh): Mesh {
|
|
853
|
+
this.meshes.push(mesh);
|
|
854
|
+
return mesh;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
remove(mesh: Mesh) {
|
|
858
|
+
const index = this.meshes.indexOf(mesh);
|
|
859
|
+
if (index > -1) {
|
|
860
|
+
this.meshes.splice(index, 1);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
setCamera(camera: Camera) {
|
|
865
|
+
this.camera = camera;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
getCamera(): Camera {
|
|
869
|
+
return this.camera;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
onAnimate(callback: (time: number) => void) {
|
|
873
|
+
this.animationCallbacks.push(callback);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
render() {
|
|
877
|
+
this.renderer.clear();
|
|
878
|
+
for (const mesh of this.meshes) {
|
|
879
|
+
this.renderer.render(mesh, this.camera);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
startAnimation() {
|
|
884
|
+
if (this.isAnimating) return;
|
|
885
|
+
this.isAnimating = true;
|
|
886
|
+
this.lastTime = performance.now();
|
|
887
|
+
this.animate();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
stopAnimation() {
|
|
891
|
+
this.isAnimating = false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private animate = () => {
|
|
895
|
+
if (!this.isAnimating) return;
|
|
896
|
+
|
|
897
|
+
const currentTime = performance.now();
|
|
898
|
+
const deltaTime = (currentTime - this.lastTime) / 1000;
|
|
899
|
+
this.lastTime = currentTime;
|
|
900
|
+
|
|
901
|
+
for (const callback of this.animationCallbacks) {
|
|
902
|
+
callback(deltaTime);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
this.render();
|
|
906
|
+
requestAnimationFrame(this.animate);
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export class Skotch {
|
|
911
|
+
static createScene(canvasId: string): Scene {
|
|
912
|
+
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
|
|
913
|
+
if (!canvas) {
|
|
914
|
+
throw new Error(`Canvas with id "${canvasId}" not found`);
|
|
915
|
+
}
|
|
916
|
+
return new Scene(canvas);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
static createCube(size?: number, color?: number[]): Mesh {
|
|
920
|
+
return new Mesh(GeometryBuilder.cube(size, color));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
static createSphere(
|
|
924
|
+
radius?: number,
|
|
925
|
+
segments?: number,
|
|
926
|
+
color?: number[],
|
|
927
|
+
): Mesh {
|
|
928
|
+
return new Mesh(GeometryBuilder.sphere(radius, segments, color));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
static createPyramid(size?: number, color?: number[]): Mesh {
|
|
932
|
+
return new Mesh(GeometryBuilder.pyramid(size, color));
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
static createTorus(
|
|
936
|
+
majorRadius?: number,
|
|
937
|
+
minorRadius?: number,
|
|
938
|
+
segments?: number,
|
|
939
|
+
color?: number[],
|
|
940
|
+
): Mesh {
|
|
941
|
+
return new Mesh(
|
|
942
|
+
GeometryBuilder.torus(majorRadius, minorRadius, segments, color),
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
static Vec3 = Vec3;
|
|
947
|
+
static Camera = Camera;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export default Skotch;
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skotch",
|
|
3
|
+
"module": "main.ts",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "v0.0.1",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"prod": "bun build main.ts --outfile dist/skotch.min.js --minify",
|
|
8
|
+
"test": "bun test"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "latest"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"typescript": "^5"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|