UNPKG

23.9 kBJavaScriptView Raw
1class Vec {
2 constructor(...values) {
3 this.values = values;
4 }
5 get dim() {
6 return this.values.length;
7 }
8 get x() {
9 return this.values[0];
10 }
11 get y() {
12 return this.values[1];
13 }
14 get z() {
15 return this.values[2];
16 }
17 get w() {
18 return this.values[3];
19 }
20 get xy() {
21 return new Vec(this.values[0], this.values[1]);
22 }
23 get xz() {
24 return new Vec(this.values[0], this.values[2]);
25 }
26 get yz() {
27 return new Vec(this.values[1], this.values[2]);
28 }
29 get xyz() {
30 return new Vec(this.values[0], this.values[1], this.values[2]);
31 }
32 /**
33 * Create vector from Array
34 * @param arr array of numbers
35 */
36 static fromArray(arr) {
37 return new Vec(...arr);
38 }
39 /**
40 * Create vector with x = y = n
41 * @param n the number
42 * @param dim the dimension
43 */
44 static fromNumber(n, dim) {
45 return new Vec(...Array(dim).fill(n));
46 }
47 /**
48 * clone vector
49 */
50 clone() {
51 return new Vec(...this.values);
52 }
53 /**
54 * add vector
55 * @param otherVec addend
56 * @returns addition result
57 */
58 add(otherVec) {
59 return new Vec(...this.values.map((v, idx) => v + otherVec.values[idx]));
60 }
61 /**
62 * subtract vector
63 * @param otherVec addend
64 * @returns subtraction result
65 */
66 sub(otherVec) {
67 return new Vec(...this.values.map((v, idx) => v - otherVec.values[idx]));
68 }
69 /**
70 * multiply vector with scalar
71 * @param value scalar
72 * @returns multiplication result
73 */
74 mul(value) {
75 return new Vec(...this.values.map((v) => v * value));
76 }
77 /**
78 * divide vector with scalar
79 * @param value scalar
80 * @returns multiplication result
81 */
82 div(value) {
83 return new Vec(...this.values.map((x) => x / value));
84 }
85 /**
86 * dot product
87 * @param otherVec
88 */
89 dot(otherVec) {
90 return this.values
91 .map((x, idx) => x * otherVec.values[idx])
92 .reduce((a, b) => a + b);
93 }
94 /**
95 * check for equality
96 * @param otherVec
97 */
98 equals(otherVec) {
99 return this.values
100 .map((v, idx) => v === otherVec.values[idx])
101 .reduce((a, b) => a === b);
102 }
103 /**
104 * Calculate length
105 */
106 get length() {
107 return Math.sqrt(this.values.map((v) => v ** 2).reduce((a, b) => a + b));
108 }
109 /**
110 * Convert to array
111 */
112 toArray() {
113 return this.values.slice(0);
114 }
115 /**
116 * Convert to string, in the form of `(x, y)`
117 */
118 toString() {
119 return `(${this.values.join(', ')})`;
120 }
121 /**
122 * cross product
123 * @param otherVec
124 * @returns new Vec3 instance containing cross product
125 */
126 cross(otherVec) {
127 if (this.dim !== 3 || otherVec.dim !== 3) {
128 throw Error('dimension not supported');
129 }
130 return new Vec(this.y * otherVec.z - this.z * otherVec.y, this.z * otherVec.x - this.x * otherVec.z, this.x * otherVec.y - this.y * otherVec.x);
131 }
132 /**
133 * normalized vector,
134 * @returns vector normalized to length = 1
135 */
136 get normalized() {
137 return this.div(this.length);
138 }
139}
140
141/** @class Mat */
142class Mat {
143 constructor(values, options) {
144 this.values = values;
145 if (options) {
146 this.numRows = options.numRows;
147 this.numCols = options.numCols;
148 }
149 else {
150 const dimension = Math.sqrt(values.length);
151 if (Number.isInteger(dimension)) {
152 this.numCols = this.numRows = dimension;
153 return;
154 }
155 throw Error('ArgumentError');
156 }
157 }
158 static identity(dimension) {
159 if (dimension <= 0 || !Number.isInteger(dimension)) {
160 throw Error('ArgumentError');
161 }
162 return new Mat(Array(dimension ** 2)
163 .fill(0)
164 .map((_, i) => (i % dimension === ((i / dimension) | 0) ? 1 : 0)));
165 }
166 /**
167 * Converts a vector with dimension n into a matrix with 1 col and n rows
168 * useful for matrix multiplication
169 * @param value the input vector
170 */
171 static fromVector(value) {
172 if (value instanceof Vec) {
173 return new Mat(value.toArray(), { numRows: value.dim, numCols: 1 });
174 }
175 throw Error('unsupported type');
176 }
177 /**
178 * Converts a bunch of vectors into a matrix
179 */
180 static fromVectors(vectors) {
181 if (!vectors || vectors.length === 0) {
182 throw Error('Argument error.');
183 }
184 const dimensions = vectors.map((v) => v.dim);
185 const dimensionsMatch = dimensions.every((x) => x === dimensions[0]);
186 const dimension = dimensions[0];
187 if (!dimensionsMatch) {
188 throw Error('Dimensions mismatch.');
189 }
190 const matrix = Array(dimension * vectors.length);
191 for (let i = 0; i < vectors.length; i++) {
192 for (let j = 0; j < dimension; j++) {
193 matrix[i + j * vectors.length] = vectors[i].values[j];
194 }
195 }
196 return new Mat(matrix, { numRows: dimension, numCols: vectors.length });
197 }
198 /**
199 * convert to array
200 */
201 toArray() {
202 return this.values;
203 }
204 valueAt(row, column) {
205 return this.values[column * this.numRows + row];
206 }
207 colAt(column) {
208 const { numRows } = this;
209 return this.values.slice(column * numRows, column * numRows + numRows);
210 }
211 rowAt(row) {
212 const { numRows, numCols } = this;
213 return Array(numCols)
214 .fill(0)
215 .map((_, column) => this.values[column * numRows + row]);
216 }
217 /**
218 * returns transposed matrix
219 */
220 transpose() {
221 const transposedValues = [];
222 Array(this.numRows)
223 .fill(0)
224 .map((_, i) => {
225 transposedValues.push(...this.rowAt(i));
226 });
227 return new Mat(transposedValues, {
228 numRows: this.numCols,
229 numCols: this.numRows,
230 });
231 }
232 equals(otherMatrix) {
233 if (this.values.length !== otherMatrix.values.length ||
234 this.numCols !== otherMatrix.numCols ||
235 this.numRows !== otherMatrix.numRows) {
236 return false;
237 }
238 for (let i = 0; i < this.values.length; i++) {
239 if (this.values[i] !== otherMatrix.values[i]) {
240 return false;
241 }
242 }
243 return true;
244 }
245 add(otherMatrix) {
246 if (this.numCols === otherMatrix.numCols &&
247 this.numRows === otherMatrix.numRows &&
248 this.values.length === otherMatrix.values.length) {
249 const newValues = this.values.map((value, i) => value + otherMatrix.values[i]);
250 return new Mat(newValues, {
251 numRows: this.numRows,
252 numCols: this.numCols,
253 });
254 }
255 throw Error('ArgumentError');
256 }
257 sub(otherMatrix) {
258 if (this.numCols === otherMatrix.numCols &&
259 this.numRows === otherMatrix.numRows &&
260 this.values.length === otherMatrix.values.length) {
261 const newValues = this.values.map((value, i) => value - otherMatrix.values[i]);
262 return new Mat(newValues, {
263 numRows: this.numRows,
264 numCols: this.numCols,
265 });
266 }
267 throw Error('ArgumentError');
268 }
269 mul(param) {
270 if (typeof param === 'number') {
271 const multipliedValues = this.values.map((value) => value * param);
272 return new Mat(multipliedValues, {
273 numRows: this.numRows,
274 numCols: this.numCols,
275 });
276 }
277 if (param instanceof Vec) {
278 if (param.dim !== this.numCols) {
279 throw Error('dimension mismatch');
280 }
281 const m = this.mul(Mat.fromVector(param));
282 return new Vec(...m.values);
283 }
284 if (param instanceof Mat) {
285 const mat = param;
286 const { numRows } = this;
287 const { numCols } = mat;
288 const multipliedValues = Array(numRows * numCols)
289 .fill(0)
290 .map((_, idx) => {
291 const y = idx % numRows;
292 const x = (idx / numRows) | 0;
293 const row = this.rowAt(y);
294 const col = mat.colAt(x);
295 return row.map((value, i) => value * col[i]).reduce((a, b) => a + b);
296 });
297 return new Mat(multipliedValues, { numRows, numCols });
298 }
299 throw Error('ArgumentError');
300 }
301 determinant() {
302 const { numRows, numCols } = this;
303 const v = this.values;
304 if (numRows !== numCols) {
305 throw Error('ArgumentError');
306 }
307 if (numRows === 2) {
308 return v[0] * v[3] - v[1] * v[2];
309 }
310 if (numRows === 3) {
311 // a0 d1 g2
312 // b3 e4 h5
313 // c6 f7 i8
314 // aei + bfg + cdh
315 //-gec - hfa - idb
316 return (v[0] * v[4] * v[8] +
317 v[3] * v[7] * v[2] +
318 v[6] * v[1] * v[5] -
319 v[2] * v[4] * v[6] -
320 v[5] * v[7] * v[0] -
321 v[8] * v[1] * v[3]);
322 }
323 throw Error('NotImplementedYet');
324 }
325 toString() {
326 const { numRows, numCols, values } = this;
327 return `mat${numRows}x${numCols}(${values.join(', ')})`;
328 }
329}
330const Mat2 = {
331 /**
332 * create rotation matrix
333 * @param angle angle in radians
334 */
335 rotation(angle) {
336 const S = Math.sin(angle);
337 const C = Math.cos(angle);
338 // prettier-ignore
339 return new Mat([
340 C, S,
341 -S, C
342 ]);
343 },
344 scaling(sx, sy) {
345 // prettier-ignore
346 return new Mat([
347 sx, 0,
348 0, sy
349 ]);
350 },
351};
352const Mat3 = {
353 /**
354 * create translation matrix
355 * @param x translation in x-direction
356 * @param y translation in y-direction
357 * @returns 3x3 translation matrix
358 */
359 translation(x, y) {
360 // prettier-ignore
361 return new Mat([
362 1, 0, 0,
363 0, 1, 0,
364 x, y, 1
365 ]);
366 },
367 /**
368 * create scaling matrix
369 * @param sx
370 * @param sy
371 * @param sz
372 * @returns 3x3 scale matrix
373 */
374 scaling(sx, sy, sz) {
375 // prettier-ignore
376 return new Mat([
377 sx, 0, 0,
378 0, sy, 0,
379 0, 0, sz
380 ]);
381 },
382 rotX(angle) {
383 const { sin, cos } = Math;
384 const S = sin(angle);
385 const C = cos(angle);
386 // prettier-ignore
387 return new Mat([
388 1, 0, 0,
389 0, C, S,
390 0, -S, C
391 ]);
392 },
393 rotY(angle) {
394 const { sin, cos } = Math;
395 const S = sin(angle);
396 const C = cos(angle);
397 // prettier-ignore
398 return new Mat([
399 C, 0, -S,
400 0, 1, 0,
401 S, 0, C
402 ]);
403 },
404 rotZ(angle) {
405 const { sin, cos } = Math;
406 const S = sin(angle);
407 const C = cos(angle);
408 // prettier-ignore
409 return new Mat([
410 C, S, 0,
411 -S, C, 0,
412 0, 0, 1
413 ]);
414 },
415};
416const Mat4 = {
417 identity() {
418 // prettier-ignore
419 return new Mat([
420 1, 0, 0, 0,
421 0, 1, 0, 0,
422 0, 0, 1, 0,
423 0, 0, 0, 1
424 ]);
425 },
426 translation(x, y, z) {
427 // prettier-ignore
428 return new Mat([
429 1, 0, 0, 0,
430 0, 1, 0, 0,
431 0, 0, 1, 0,
432 x, y, z, 1
433 ]);
434 },
435 scaling(sx, sy, sz) {
436 // prettier-ignore
437 return new Mat([
438 sx, 0, 0, 0,
439 0, sy, 0, 0,
440 0, 0, sz, 0,
441 0, 0, 0, 1
442 ]);
443 },
444 rotX(angle) {
445 const { sin, cos } = Math;
446 const S = sin(angle);
447 const C = cos(angle);
448 // prettier-ignore
449 return new Mat([
450 1, 0, 0, 0,
451 0, C, S, 0,
452 0, -S, C, 0,
453 0, 0, 0, 1
454 ]);
455 },
456 rotY(angle) {
457 const { sin, cos } = Math;
458 const S = sin(angle);
459 const C = cos(angle);
460 // prettier-ignore
461 return new Mat([
462 C, 0, -S, 0,
463 0, 1, 0, 0,
464 S, 0, C, 0,
465 0, 0, 0, 1
466 ]);
467 },
468 rotZ(angle) {
469 const { sin, cos } = Math;
470 const S = sin(angle);
471 const C = cos(angle);
472 // prettier-ignore
473 return new Mat([
474 C, S, 0, 0,
475 -S, C, 0, 0,
476 0, 0, 1, 0,
477 0, 0, 0, 1
478 ]);
479 },
480};
481
482/**
483 * Create a view matrix
484 * @param eye position of the eye is (where you are)
485 * @param center position where you want to look at
486 * @param up it's a normalized vector, quite often (0,1,0)
487 * @returns view matrix
488 * @see https://www.khronos.org/opengl/wiki/GluLookAt_code
489 */
490function lookAt(eye, center, up) {
491 const forward = eye.sub(center).normalized;
492 const side = forward.cross(up).normalized;
493 const up2 = side.cross(forward);
494 // prettier-ignore
495 return new Mat([
496 side.x, side.y, side.z, 0,
497 up2.x, up2.y, up2.z, 0,
498 -forward.x, -forward.y, -forward.z, 0,
499 0, 0, 0, 1
500 ]);
501}
502
503/**
504 * creates a transformation that produces a parallel projection
505 * @param left coordinate for the left vertical clipping planes.
506 * @param right coordinate for the right vertical clipping planes.
507 * @param bottom coordinate for the bottom horizontal clippling pane.
508 * @param top coordinate for the top horizontal clipping pane
509 * @param zNear Specify the distances to the nearer and farther depth clipping planes. These values are negative if the plane is to be behind the viewer.
510 * @param zFar Specify the distances to the nearer and farther depth clipping planes. These values are negative if the plane is to be behind the viewer.
511 * @returns 4x4 orthographic transformation matrix
512 * @see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glOrtho.xml
513 */
514function ortho(left, right, bottom, top, zNear, zFar) {
515 const tx = -(right + left) / (right - left);
516 const ty = -(top + bottom) / (top - bottom);
517 const tz = -(zFar + zNear) / (zFar - zNear);
518 // prettier-ignore
519 return new Mat([
520 2 / (right - left), 0, 0,
521 0, 0, 2 / (top - bottom), 0,
522 0, 0, 0, -2 / (zFar - zNear),
523 0, tx, ty, tz, 1
524 ]);
525}
526// glFrustum(left, right, bottom, top, zNear, zFar)
527/**
528 * creates a perspective matrix that produces a perspective projection
529 * @param left coordinates for the vertical left clipping pane
530 * @param right coordinates for the vertical right clipping pane
531 * @param bottom coordinates for the horizontal bottom clipping pane
532 * @param top coodinates for the top horizontal clipping pane
533 * @param zNear Specify the distances to the near depth clipping plane. Must be positive.
534 * @param zFar Specify the distances to the far depth clipping planes. Must be positive.
535 * @returns 4x4 perspective projection matrix
536 * @see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml
537 */
538function frustum(left, right, bottom, top, zNear, zFar) {
539 const t1 = 2 * zNear;
540 const t2 = right - left;
541 const t3 = top - bottom;
542 const t4 = zFar - zNear;
543 // prettier-ignore
544 return new Mat([t1 / t2, 0, 0, 0,
545 0, t1 / t3, 0, 0,
546 (right + left) / t2, (top + bottom) / t3, (-zFar - zNear) / t4, -1,
547 0, 0, (-t1 * zFar) / t4, 0]);
548}
549/**
550 * creates a perspective projection matrix
551 * @param fieldOfView Specifies the field of view angle, in degrees, in the y direction.
552 * @param aspectRatio Specifies the aspect ratio that determines the field of view in the x direction. The aspect ratio is the ratio of x (width) to y (height).
553 * @param zNear Specifies the distance from the viewer to the near clipping plane (always positive).
554 * @param zFar Specifies the distance from the viewer to the far clipping plane (always positive).
555 * @returns 4x4 perspective projection matrix
556 */
557function perspective(fieldOfView, aspectRatio, zNear, zFar) {
558 const y = zNear * Math.tan((fieldOfView * Math.PI) / 360);
559 const x = y * aspectRatio;
560 return frustum(-x, x, -y, y, zNear, zFar);
561}
562
563/** @class Geometry */
564class Geometry {
565 constructor(vertices, faces, normals, texCoords) {
566 this.vertices = vertices;
567 this.faces = faces;
568 this.normals = normals;
569 this.texCoords = texCoords;
570 }
571 /**
572 * converts to triangle array
573 */
574 toTriangles() {
575 const { faces, vertices } = this;
576 return faces
577 .map((face) => {
578 if (face.length === 3) {
579 return face.map((vertexIndex) => vertices[vertexIndex]);
580 }
581 if (face.length === 4) {
582 const q = face.map((vertexIndex) => vertices[vertexIndex]);
583 return [q[0], q[1], q[3], q[3], q[1], q[2]];
584 }
585 throw Error('not supported');
586 })
587 .flat()
588 .map((v) => v.toArray())
589 .flat();
590 }
591 /**
592 * Calculate the surface normal of a triangle
593 * @param p1 3d vector of point 1
594 * @param p2 3d vector of point 2
595 * @param p3 3d vector of point 3
596 */
597 static calculateSurfaceNormal(p1, p2, p3) {
598 const u = p2.sub(p1);
599 const v = p3.sub(p1);
600 return new Vec(u.x * v.z - u.z * v.y, u.z * v.x - u.x * v.z, u.x * v.y - u.y * v.x);
601 }
602 /**
603 * Create a box geometry with the sizes a * b * c,
604 * centered at (0, 0, 0), 2 triangles per side.
605 *
606 * @name box
607 * @param {number} sizeA
608 * @param {number} sizeB
609 * @param {number} sizeC
610 */
611 static box(sizeA = 1.0, sizeB = 1.0, sizeC = 1.0) {
612 const a = sizeA * 0.5;
613 const b = sizeB * 0.5;
614 const c = sizeC * 0.5;
615 const vertices = [
616 [-a, -b, -c],
617 [a, -b, -c],
618 [-a, b, -c],
619 [a, b, -c],
620 [-a, -b, c],
621 [a, -b, c],
622 [-a, b, c],
623 [a, b, c],
624 ].map((v) => new Vec(...v));
625 // 0______1
626 // 4/|____5/|
627 // |2|____|_|3
628 // |/ ____|/
629 // 6 7
630 const faces = [
631 // back
632 [0, 1, 2],
633 [2, 1, 3],
634 // front
635 [5, 4, 7],
636 [7, 4, 6],
637 // left
638 [4, 0, 6],
639 [6, 0, 2],
640 // right
641 [7, 5, 1],
642 [1, 7, 3],
643 // top
644 [1, 0, 5],
645 [5, 0, 4],
646 // bottom
647 [2, 3, 6],
648 [6, 3, 7],
649 ];
650 const normals = faces.map((f) => Geometry.calculateSurfaceNormal(vertices[f[0]], vertices[f[1]], vertices[f[2]]));
651 return new Geometry(vertices, faces, normals, []);
652 }
653 /**
654 * create a cube
655 * @param size
656 */
657 static cube(size = 1.0) {
658 return Geometry.box(size, size, size);
659 }
660 /**
661 * create a plane grid mesh
662 * @param x x-coord of the top left corner
663 * @param y y-coord of the top left corner
664 * @param width width of the plane
665 * @param height height of the plane
666 * @param rows number of rows
667 * @param cols number of columns
668 */
669 static grid(x, y, width, height, rows, cols) {
670 const deltaX = width / cols;
671 const deltaY = height / rows;
672 const vertices = Array((cols + 1) * (rows + 1))
673 .fill(0)
674 .map((_, i) => {
675 const ix = i % cols;
676 const iy = (i / cols) | 0;
677 return new Vec(x + ix * deltaX, y + iy * deltaY, 0);
678 });
679 const faces = Array(rows * cols)
680 .fill(0)
681 .map((_, i) => {
682 const ix = i % cols;
683 const iy = (i / cols) | 0;
684 const idx = iy * rows + ix;
685 return [
686 [idx, idx + 1, idx + rows],
687 [idx + 1, idx + rows + 1, idx + rows],
688 ];
689 })
690 .flat(1);
691 const normals = faces.map((f) => Geometry.calculateSurfaceNormal(vertices[f[0]], vertices[f[1]], vertices[f[2]]));
692 return new Geometry(vertices, faces, normals, []);
693 }
694 /**
695 * Create sphere geometry
696 * @param r radius
697 * @param sides number of sides (around the sphere)
698 * @param segments number of segments (from top to bottom)
699 * @see adapted from https://vorg.github.io/pex/docs/pex-gen/Sphere.html
700 */
701 static sphere(r = 0.5, sides = 36, segments = 18) {
702 const vertices = [];
703 const texCoords = [];
704 const faces = [];
705 const dphi = 360 / sides;
706 const dtheta = 180 / segments;
707 const evalPos = (theta, phi) => {
708 const deg = Math.PI / 180.0;
709 var pos = new Vec(r * Math.sin(theta * deg) * Math.sin(phi * deg), r * Math.cos(theta * deg), r * Math.sin(theta * deg) * Math.cos(phi * deg));
710 return pos;
711 };
712 for (let segment = 0; segment <= segments; segment++) {
713 const theta = segment * dtheta;
714 for (let side = 0; side <= sides; side++) {
715 const phi = side * dphi;
716 const pos = evalPos(theta, phi);
717 const texCoord = new Vec(phi / 360.0, theta / 180.0);
718 vertices.push(pos);
719 texCoords.push(texCoord);
720 if (segment === segments)
721 continue;
722 if (side === sides)
723 continue;
724 if (segment == 0) {
725 // first segment uses triangles
726 faces.push([
727 segment * (sides + 1) + side,
728 (segment + 1) * (sides + 1) + side,
729 (segment + 1) * (sides + 1) + side + 1,
730 ]);
731 }
732 else if (segment == segments - 1) {
733 // last segment also uses triangles
734 faces.push([
735 segment * (sides + 1) + side,
736 (segment + 1) * (sides + 1) + side + 1,
737 segment * (sides + 1) + side + 1,
738 ]);
739 }
740 else {
741 // A --- B
742 // D --- C
743 const A = segment * (sides + 1) + side;
744 const B = (segment + 1) * (sides + 1) + side;
745 const C = (segment + 1) * (sides + 1) + side + 1;
746 const D = segment * (sides + 1) + side + 1;
747 faces.push([A, B, D]);
748 faces.push([B, C, D]);
749 }
750 }
751 }
752 const normals = faces.map((f) => Geometry.calculateSurfaceNormal(vertices[f[0]], vertices[f[1]], vertices[f[2]]));
753 return new Geometry(vertices, faces, normals, texCoords);
754 }
755}
756
757export { Geometry, Mat, Mat2, Mat3, Mat4, Vec, frustum, lookAt, ortho, perspective };