diff --git a/README.md b/README.md
index 9d73281..5ea37e3 100644
--- a/README.md
+++ b/README.md
@@ -4,15 +4,16 @@ Skewed is a Typescript package for generating SVG of 3D graphics in real-time. I
Use it to make simple 3D infographics, 3D web-games, or generate 3D SVG files for importing into vector editors like Figma/Illustrator (Ie. make 3D icons).
-[Live demo](https://vgyf3c.csb.app/)
+Here's a [Live demo](https://vgyf3c.csb.app/). Docs coming soon.
+
-
+
# Usage
- 1. `npm install skewed`
- 1. Reference the [live demo](https://vgyf3c.csb.app/)'s [source code](https://codesandbox.io/s/skewed-demo-vgyf3c?file=/src/index.ts). More API examples coming soon. In the meantime
+1. `npm install skewed`
+1. Reference the [live demo](https://vgyf3c.csb.app/)'s [source code](https://codesandbox.io/s/skewed-demo-vgyf3c?file=/src/index.ts). More API examples coming soon. In the meantime
## Contributing
diff --git a/docs/images/skewed-screen-capture.gif b/docs/images/light-spinning-around-shapes.gif
similarity index 100%
rename from docs/images/skewed-screen-capture.gif
rename to docs/images/light-spinning-around-shapes.gif
diff --git a/docs/images/octopus.gif b/docs/images/octopus.gif
new file mode 100644
index 0000000..01d6450
Binary files /dev/null and b/docs/images/octopus.gif differ
diff --git a/docs/images/rotating-text.gif b/docs/images/rotating-text.gif
new file mode 100644
index 0000000..792fe9a
Binary files /dev/null and b/docs/images/rotating-text.gif differ
diff --git a/docs/images/worm.gif b/docs/images/worm.gif
new file mode 100644
index 0000000..09ea90e
Binary files /dev/null and b/docs/images/worm.gif differ
diff --git a/src/index.ts b/src/index.ts
index bf527a2..2adc3f3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,6 +12,7 @@ export * from "./shapes/Grid";
export * from "./shapes/Group";
export * from "./shapes/Cylinder";
export * from "./shapes/Sphere";
+export * from "./shapes/Text";
export * from "./meshes/Mesh";
export * from "./meshes/BoxMesh";
export * from "./math/Vector3";
diff --git a/src/math/Euler.ss b/src/math/Euler.ss
deleted file mode 100644
index 2b57c71..0000000
--- a/src/math/Euler.ss
+++ /dev/null
@@ -1,315 +0,0 @@
-// import { Quaternion } from './Quaternion.js';
-// import { Matrix4 } from './Matrix4.js';
-// import { clamp } from './MathUtils.js';
-
-// const _matrix = /*@__PURE__*/ new Matrix4();
-// const _quaternion = /*@__PURE__*/ new Quaternion();
-
-// class Euler {
-
-// constructor( x = 0, y = 0, z = 0, order = Euler.DEFAULT_ORDER ) {
-
-// this.isEuler = true;
-
-// this._x = x;
-// this._y = y;
-// this._z = z;
-// this._order = order;
-
-// }
-
-// get x() {
-
-// return this._x;
-
-// }
-
-// set x( value ) {
-
-// this._x = value;
-// this._onChangeCallback();
-
-// }
-
-// get y() {
-
-// return this._y;
-
-// }
-
-// set y( value ) {
-
-// this._y = value;
-// this._onChangeCallback();
-
-// }
-
-// get z() {
-
-// return this._z;
-
-// }
-
-// set z( value ) {
-
-// this._z = value;
-// this._onChangeCallback();
-
-// }
-
-// get order() {
-
-// return this._order;
-
-// }
-
-// set order( value ) {
-
-// this._order = value;
-// this._onChangeCallback();
-
-// }
-
-// set( x, y, z, order = this._order ) {
-
-// this._x = x;
-// this._y = y;
-// this._z = z;
-// this._order = order;
-
-// this._onChangeCallback();
-
-// return this;
-
-// }
-
-// clone() {
-
-// return new this.constructor( this._x, this._y, this._z, this._order );
-
-// }
-
-// copy( euler ) {
-
-// this._x = euler._x;
-// this._y = euler._y;
-// this._z = euler._z;
-// this._order = euler._order;
-
-// this._onChangeCallback();
-
-// return this;
-
-// }
-
-// setFromRotationMatrix( m, order = this._order, update = true ) {
-
-// // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
-
-// const te = m.elements;
-// const m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ];
-// const m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ];
-// const m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ];
-
-// switch ( order ) {
-
-// case 'XYZ':
-
-// this._y = Math.asin( clamp( m13, - 1, 1 ) );
-
-// if ( Math.abs( m13 ) < 0.9999999 ) {
-
-// this._x = Math.atan2( - m23, m33 );
-// this._z = Math.atan2( - m12, m11 );
-
-// } else {
-
-// this._x = Math.atan2( m32, m22 );
-// this._z = 0;
-
-// }
-
-// break;
-
-// case 'YXZ':
-
-// this._x = Math.asin( - clamp( m23, - 1, 1 ) );
-
-// if ( Math.abs( m23 ) < 0.9999999 ) {
-
-// this._y = Math.atan2( m13, m33 );
-// this._z = Math.atan2( m21, m22 );
-
-// } else {
-
-// this._y = Math.atan2( - m31, m11 );
-// this._z = 0;
-
-// }
-
-// break;
-
-// case 'ZXY':
-
-// this._x = Math.asin( clamp( m32, - 1, 1 ) );
-
-// if ( Math.abs( m32 ) < 0.9999999 ) {
-
-// this._y = Math.atan2( - m31, m33 );
-// this._z = Math.atan2( - m12, m22 );
-
-// } else {
-
-// this._y = 0;
-// this._z = Math.atan2( m21, m11 );
-
-// }
-
-// break;
-
-// case 'ZYX':
-
-// this._y = Math.asin( - clamp( m31, - 1, 1 ) );
-
-// if ( Math.abs( m31 ) < 0.9999999 ) {
-
-// this._x = Math.atan2( m32, m33 );
-// this._z = Math.atan2( m21, m11 );
-
-// } else {
-
-// this._x = 0;
-// this._z = Math.atan2( - m12, m22 );
-
-// }
-
-// break;
-
-// case 'YZX':
-
-// this._z = Math.asin( clamp( m21, - 1, 1 ) );
-
-// if ( Math.abs( m21 ) < 0.9999999 ) {
-
-// this._x = Math.atan2( - m23, m22 );
-// this._y = Math.atan2( - m31, m11 );
-
-// } else {
-
-// this._x = 0;
-// this._y = Math.atan2( m13, m33 );
-
-// }
-
-// break;
-
-// case 'XZY':
-
-// this._z = Math.asin( - clamp( m12, - 1, 1 ) );
-
-// if ( Math.abs( m12 ) < 0.9999999 ) {
-
-// this._x = Math.atan2( m32, m22 );
-// this._y = Math.atan2( m13, m11 );
-
-// } else {
-
-// this._x = Math.atan2( - m23, m33 );
-// this._y = 0;
-
-// }
-
-// break;
-
-// default:
-
-// console.warn( 'THREE.Euler: .setFromRotationMatrix() encountered an unknown order: ' + order );
-
-// }
-
-// this._order = order;
-
-// if ( update === true ) this._onChangeCallback();
-
-// return this;
-
-// }
-
-// setFromQuaternion( q, order, update ) {
-
-// _matrix.makeRotationFromQuaternion( q );
-
-// return this.setFromRotationMatrix( _matrix, order, update );
-
-// }
-
-// setFromVector3( v, order = this._order ) {
-
-// return this.set( v.x, v.y, v.z, order );
-
-// }
-
-// reorder( newOrder ) {
-
-// // WARNING: this discards revolution information -bhouston
-
-// _quaternion.setFromEuler( this );
-
-// return this.setFromQuaternion( _quaternion, newOrder );
-
-// }
-
-// equals( euler ) {
-
-// return ( euler._x === this._x ) && ( euler._y === this._y ) && ( euler._z === this._z ) && ( euler._order === this._order );
-
-// }
-
-// fromArray( array ) {
-
-// this._x = array[ 0 ];
-// this._y = array[ 1 ];
-// this._z = array[ 2 ];
-// if ( array[ 3 ] !== undefined ) this._order = array[ 3 ];
-
-// this._onChangeCallback();
-
-// return this;
-
-// }
-
-// toArray( array = [], offset = 0 ) {
-
-// array[ offset ] = this._x;
-// array[ offset + 1 ] = this._y;
-// array[ offset + 2 ] = this._z;
-// array[ offset + 3 ] = this._order;
-
-// return array;
-
-// }
-
-// _onChange( callback ) {
-
-// this._onChangeCallback = callback;
-
-// return this;
-
-// }
-
-// _onChangeCallback() {}
-
-// *[ Symbol.iterator ]() {
-
-// yield this._x;
-// yield this._y;
-// yield this._z;
-// yield this._order;
-
-// }
-
-// }
-
-// Euler.DEFAULT_ORDER = 'XYZ';
-
-// export { Euler };
diff --git a/src/math/Euler.ts b/src/math/Euler.ts
new file mode 100644
index 0000000..aecba8d
--- /dev/null
+++ b/src/math/Euler.ts
@@ -0,0 +1,251 @@
+import { Matrix4x4 } from "./Matrix4x4";
+
+// const _matrix = /*@__PURE__*/ Matrix4x4();
+
+export enum EulerOrder {
+ XYZ = "XYZ",
+ ZYX = "ZYX",
+ DEFAULT_ORDER = XYZ,
+}
+
+class Euler {
+ public isEuler = true;
+
+ constructor(
+ private _x = 0,
+ private _y = 0,
+ private _z = 0,
+ private _order: EulerOrder = EulerOrder.DEFAULT_ORDER
+ ) {}
+
+ get x() {
+ return this._x;
+ }
+
+ set x(value) {
+ this._x = value;
+ // this._onChangeCallback();
+ }
+
+ get y() {
+ return this._y;
+ }
+
+ set y(value) {
+ this._y = value;
+ // this._onChangeCallback();
+ }
+
+ get z() {
+ return this._z;
+ }
+
+ set z(value) {
+ this._z = value;
+ // this._onChangeCallback();
+ }
+
+ get order() {
+ return this._order;
+ }
+
+ set order(value) {
+ this._order = value;
+ // this._onChangeCallback();
+ }
+
+ // set(x, y, z, order = this._order) {
+ // this._x = x;
+ // this._y = y;
+ // this._z = z;
+ // this._order = order;
+
+ // // this._onChangeCallback();
+
+ // return this;
+ // }
+
+ // clone() {
+ // return new Euler(this._x, this._y, this._z, this._order);
+ // }
+
+ // copy(euler: Euler) {
+ // this._x = euler._x;
+ // this._y = euler._y;
+ // this._z = euler._z;
+ // this._order = euler._order;
+
+ // // this._onChangeCallback();
+
+ // return this;
+ // }
+
+ setFromRotationMatrix(m: Matrix4x4, order: EulerOrder = this._order) {
+ // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
+
+ const te = m.elements;
+ const m11 = te[0],
+ m12 = te[4],
+ m13 = te[8];
+ const m21 = te[1],
+ m22 = te[5],
+ m23 = te[9];
+ const m31 = te[2],
+ m32 = te[6],
+ m33 = te[10];
+
+ switch (order) {
+ case EulerOrder.XYZ:
+ this._y = Math.asin(clamp(m13, -1, 1));
+
+ if (Math.abs(m13) < 0.9999999) {
+ this._x = Math.atan2(-m23, m33);
+ this._z = Math.atan2(-m12, m11);
+ } else {
+ this._x = Math.atan2(m32, m22);
+ this._z = 0;
+ }
+
+ break;
+
+ // case "YXZ":
+ // this._x = Math.asin(-clamp(m23, -1, 1));
+
+ // if (Math.abs(m23) < 0.9999999) {
+ // this._y = Math.atan2(m13, m33);
+ // this._z = Math.atan2(m21, m22);
+ // } else {
+ // this._y = Math.atan2(-m31, m11);
+ // this._z = 0;
+ // }
+
+ // break;
+
+ // case "ZXY":
+ // this._x = Math.asin(clamp(m32, -1, 1));
+
+ // if (Math.abs(m32) < 0.9999999) {
+ // this._y = Math.atan2(-m31, m33);
+ // this._z = Math.atan2(-m12, m22);
+ // } else {
+ // this._y = 0;
+ // this._z = Math.atan2(m21, m11);
+ // }
+
+ // break;
+
+ case EulerOrder.ZYX:
+ this._y = Math.asin(-clamp(m31, -1, 1));
+
+ if (Math.abs(m31) < 0.9999999) {
+ this._x = Math.atan2(m32, m33);
+ this._z = Math.atan2(m21, m11);
+ } else {
+ this._x = 0;
+ this._z = Math.atan2(-m12, m22);
+ }
+
+ break;
+
+ // case "YZX":
+ // this._z = Math.asin(clamp(m21, -1, 1));
+
+ // if (Math.abs(m21) < 0.9999999) {
+ // this._x = Math.atan2(-m23, m22);
+ // this._y = Math.atan2(-m31, m11);
+ // } else {
+ // this._x = 0;
+ // this._y = Math.atan2(m13, m33);
+ // }
+
+ // break;
+
+ // case "XZY":
+ // this._z = Math.asin(-clamp(m12, -1, 1));
+
+ // if (Math.abs(m12) < 0.9999999) {
+ // this._x = Math.atan2(m32, m22);
+ // this._y = Math.atan2(m13, m11);
+ // } else {
+ // this._x = Math.atan2(-m23, m33);
+ // this._y = 0;
+ // }
+
+ // break;
+
+ // default:
+ // console.warn(
+ // "THREE.Euler: .setFromRotationMatrix() encountered an unknown order: " +
+ // order
+ // );
+ }
+
+ this._order = order;
+
+ // if (update === true) this._onChangeCallback();
+
+ return this;
+ }
+
+ // setFromVector3(v, order = this._order) {
+ // return this.set(v.x, v.y, v.z, order);
+ // }
+
+ // reorder(newOrder) {
+ // // WARNING: this discards revolution information -bhouston
+
+ // _quaternion.setFromEuler(this);
+
+ // return this.setFromQuaternion(_quaternion, newOrder);
+ // }
+
+ // equals(euler) {
+ // return (
+ // euler._x === this._x &&
+ // euler._y === this._y &&
+ // euler._z === this._z &&
+ // euler._order === this._order
+ // );
+ // }
+
+ // fromArray(array) {
+ // this._x = array[0];
+ // this._y = array[1];
+ // this._z = array[2];
+ // if (array[3] !== undefined) this._order = array[3];
+
+ // this._onChangeCallback();
+
+ // return this;
+ // }
+
+ // toArray(array = [], offset = 0) {
+ // array[offset] = this._x;
+ // array[offset + 1] = this._y;
+ // array[offset + 2] = this._z;
+ // array[offset + 3] = this._order;
+
+ // return array;
+ // }
+
+ // _onChange(callback) {
+ // this._onChangeCallback = callback;
+
+ // return this;
+ // }
+
+ // _onChangeCallback() {}
+
+ // *[Symbol.iterator]() {
+ // yield this._x;
+ // yield this._y;
+ // yield this._z;
+ // yield this._order;
+ // }
+}
+
+export { Euler };
+
+function clamp(value: number, min: number, max: number) {
+ return Math.max(min, Math.min(max, value));
+}
diff --git a/src/math/Matrix4x4.ts b/src/math/Matrix4x4.ts
index c69cc91..ac75aaa 100644
--- a/src/math/Matrix4x4.ts
+++ b/src/math/Matrix4x4.ts
@@ -180,6 +180,10 @@ function createMatrix4x4(
});
}
+// Actual implementation of Matrix4x4, we take this approach we because we want a
+// 'new' free API. Ie: `v = Matrix4x4()` instead of `v = new Matrix4x4()`.
+// This is based on a conversation with GPT-4 that helped meet my requirements
+// https://chat.openai.com/c/f22bc4d6-2cc3-44c1-8b91-28c2708f2c17
const Matrix4x4Proto = {
set(
this: Matrix4x4,
diff --git a/src/math/Vector3.test.ts b/src/math/Vector3.test.ts
index 88a8e3c..d39c978 100644
--- a/src/math/Vector3.test.ts
+++ b/src/math/Vector3.test.ts
@@ -3,6 +3,7 @@ import { Vector3 } from "./Vector3";
describe("Vector3", () => {
it("Factory variants should work", () => {
+ expect(Vector3()).toEqual(Vector3(0, 0, 0));
expect(Vector3(1, 2, 3)).toEqual(Vector3(1, 2, 3));
expect(Vector3([3, 4, 5])).toEqual(Vector3(3, 4, 5));
expect(Vector3(6, 7, 8)).toEqual(Vector3(6, 7, 8));
diff --git a/src/math/Vector3.ts b/src/math/Vector3.ts
index f7aa63c..7c1c35c 100644
--- a/src/math/Vector3.ts
+++ b/src/math/Vector3.ts
@@ -140,12 +140,12 @@ export function Vector3(x: number, y: number, z: number): Vector3;
*/
export function Vector3(coords: { x: number; y: number; z: number }): Vector3;
-// /**
-// * Creates a new Vector3 instance. The function supports various input formats for convenient instantiation.
-// *
-// * @returns A Vector3 instance initialized to (0, 0, 0) when called without arguments
-// */
-// export function Vector3(): Vector3;
+/**
+ * Creates a new Vector3 instance. The function supports various input formats for convenient instantiation.
+ *
+ * @returns A Vector3 instance initialized to (0, 0, 0) when called without arguments
+ */
+export function Vector3(): Vector3;
/**
* Creates a new Vector3 instance with the specified coordinates from an array.
@@ -164,10 +164,17 @@ export function Vector3(coords: [number, number, number]): Vector3;
* @returns A Vector3 instance with the specified x, y, and z coordinates, or initialized to (0, 0, 0) when called without arguments
*/
export function Vector3(
- x?: number | { x: number; y: number; z: number } | [number, number, number],
+ x?:
+ | number
+ | { x: number; y: number; z: number }
+ | [number, number, number]
+ | undefined,
y?: number,
z?: number
): Vector3 {
+ if (arguments.length === 0) {
+ return Vector3(0, 0, 0);
+ }
if (typeof x === "object") {
if (Array.isArray(x)) {
return createVector3(x[0], x[1], x[2]);
diff --git a/src/renderer/DebugRenderer.ts b/src/renderer/DebugRenderer.ts
index d11575c..3a3c8b9 100644
--- a/src/renderer/DebugRenderer.ts
+++ b/src/renderer/DebugRenderer.ts
@@ -10,14 +10,12 @@ export function DebugLine2D(
y2: number,
stroke: Color = Color(0, 0, 0)
) {
- const centerX = viewport.width / 2;
- const centerY = viewport.height / 2;
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.style.zIndex = "1000";
- line.setAttribute("x1", (x + centerX).toFixed(2));
- line.setAttribute("y1", (y + centerY).toFixed(2));
- line.setAttribute("x2", (x2 + centerX).toFixed(2));
- line.setAttribute("y2", (y2 + centerY).toFixed(2));
+ line.setAttribute("x1", x.toFixed(2));
+ line.setAttribute("y1", y.toFixed(2));
+ line.setAttribute("x2", x2.toFixed(2));
+ line.setAttribute("y2", y2.toFixed(2));
line.setAttribute("stroke", ColorToCSS(stroke));
// line.setAttribute("stroke-width", "0.1");
diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts
index 03df6be..fe1ea0b 100644
--- a/src/renderer/Renderer.ts
+++ b/src/renderer/Renderer.ts
@@ -8,6 +8,7 @@ import { applyLighting } from "../lighting/LightingModel";
import { renderSphere } from "./renderSphere";
import { ColorToCSS } from "../colors/Color";
import { renderCylinder } from "./renderCylinder";
+import { renderText } from "./renderText";
const CrackFillingStrokeWidth = 0.5;
@@ -126,6 +127,20 @@ export function render(
inverseAndProjectionMatrix
);
break;
+ case "text":
+ renderText(
+ scene,
+ svg,
+ defs,
+ shape,
+ viewport,
+ worldTransform,
+ cameraZoom,
+ cameraDirection,
+ inverseCameraMatrix,
+ inverseAndProjectionMatrix
+ );
+ break;
default:
throw new Error(`Unknown shape type: ${(shape as Shape).type}`);
}
diff --git a/src/renderer/renderText.ts b/src/renderer/renderText.ts
new file mode 100644
index 0000000..560f758
--- /dev/null
+++ b/src/renderer/renderText.ts
@@ -0,0 +1,111 @@
+import { projectToScreenCoordinate } from "../cameras/Camera";
+import { Matrix4x4 } from "../math/Matrix4x4";
+import { Vector3 } from "../math/Vector3";
+import { TextShape } from "../shapes/Shape";
+import { Scene } from "./Scene";
+import { Viewport } from "./Viewport";
+import { Color, ColorToCSS } from "../colors/Color";
+import { Euler, EulerOrder } from "../math/Euler";
+// import { DebugLine2D } from "./DebugRenderer";
+import { applyLighting } from "../lighting/LightingModel";
+
+export function renderText(
+ scene: Scene,
+ svg: SVGElement,
+ _defs: SVGDefsElement,
+ textShape: TextShape,
+ viewport: Viewport,
+ worldTransform: Matrix4x4,
+ cameraZoom: number,
+ _cameraDirection: Vector3,
+ inverseCameraMatrix: Matrix4x4,
+ inverseAndProjectionMatrix: Matrix4x4
+) {
+ const textScale = worldTransform.getScale().x;
+ const textScaleFactor = textScale * cameraZoom;
+
+ const transformMatrixCameraSpace = inverseCameraMatrix
+ .clone()
+ .multiply(worldTransform)
+ .extractRotation();
+ const faceNormalInWorldSpace = Vector3(0, 0, 0);
+ worldTransform.extractBasis(undefined, undefined, faceNormalInWorldSpace);
+
+ // Convert the light direction into camera space (not projected into screen space)
+ const directionalLightInCameraSpace = scene.directionalLight.direction
+ .clone()
+ .multiply(-1);
+ inverseCameraMatrix
+ .extractRotation()
+ .applyToVector3(directionalLightInCameraSpace);
+
+ const faceNormalInCameraSpace = Vector3(0, 0, 0);
+ transformMatrixCameraSpace.extractBasis(
+ undefined,
+ undefined,
+ faceNormalInCameraSpace
+ );
+ const facingWayFromCamera = faceNormalInCameraSpace.z < 0;
+ if (facingWayFromCamera) {
+ faceNormalInCameraSpace.multiply(-1);
+ }
+
+ const fill = applyLighting(
+ scene.directionalLight.color,
+ textShape.fill,
+ scene.ambientLightColor,
+ directionalLightInCameraSpace.dotProduct(faceNormalInCameraSpace)
+ );
+
+ function renderTextStackSlice(
+ offset: number,
+ fillString: string,
+ strokeString: string
+ ) {
+ const textElement = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ textElement.setAttribute("id", "text");
+
+ const { x, y } = projectToScreenCoordinate(
+ worldTransform
+ .getTranslation()
+ .add(faceNormalInWorldSpace.clone().multiply(offset)),
+ inverseAndProjectionMatrix,
+ viewport
+ );
+
+ textElement.setAttribute("font-size", textShape.fontSize.toFixed(2));
+ textElement.setAttribute("font-family", textShape.fontFamily);
+ textElement.setAttribute("fill", fillString);
+
+ textElement.setAttribute("stroke", strokeString);
+ textElement.setAttribute("stroke-width", textShape.strokeWidth.toFixed(2));
+
+ // Align the text to the center
+ textElement.setAttribute("text-anchor", "middle");
+ textElement.setAttribute("dominant-baseline", "middle");
+
+ textElement.textContent = textShape.text;
+
+ const e = transformMatrixCameraSpace.elements;
+
+ const xAxis = { x: e[0] * textScaleFactor, y: -e[1] * textScaleFactor };
+ const yAxis = { x: -e[4] * textScaleFactor, y: e[5] * textScaleFactor };
+
+ const precision = 3;
+ const transformMatrixText = `matrix(${xAxis.x.toFixed(
+ precision
+ )} ${xAxis.y.toFixed(precision)} ${yAxis.x.toFixed(
+ precision
+ )} ${yAxis.y.toFixed(precision)} ${x.toFixed(precision)} ${y.toFixed(
+ precision
+ )})`;
+ textElement.setAttribute("transform", transformMatrixText);
+
+ svg.appendChild(textElement);
+ }
+
+ renderTextStackSlice(0, fill, ColorToCSS(textShape.stroke));
+}
diff --git a/src/shapes/Shape.ts b/src/shapes/Shape.ts
index 2d06df0..28fa829 100644
--- a/src/shapes/Shape.ts
+++ b/src/shapes/Shape.ts
@@ -30,6 +30,7 @@ export const DefaultBasicShapeProperties = (): BasicShapeProperties => ({
});
export const DefaultShapeDimension = 100;
+export const DefaultFontSize = 16;
export type MeshShape = {
type: "mesh";
@@ -53,6 +54,14 @@ export type GroupShape = TransformProperties & {
children: Shape[];
};
+export type TextShape = BasicShapeProperties & {
+ type: "text";
+ id: string;
+ text: string;
+ fontSize: number;
+ fontFamily: string;
+};
+
export type GridShape = BasicShapeProperties & {
type: "grid";
id: string;
@@ -65,5 +74,6 @@ export type Shape =
| MeshShape
| SphereShape
| CylinderShape
+ | TextShape
| GroupShape
| GridShape;
diff --git a/src/shapes/Text.ts b/src/shapes/Text.ts
new file mode 100644
index 0000000..8d09fe0
--- /dev/null
+++ b/src/shapes/Text.ts
@@ -0,0 +1,28 @@
+import {
+ BasicShapeProperties,
+ DefaultBasicShapeProperties,
+ DefaultFontSize,
+ TextShape,
+} from "./Shape";
+
+export type TextProperties = {
+ text: string;
+ fontSize: number;
+ fontFamily: string;
+} & BasicShapeProperties;
+
+const DefaultSphereProperties: TextProperties = {
+ text: "",
+ fontSize: DefaultFontSize,
+ fontFamily: "Arial",
+ ...DefaultBasicShapeProperties(),
+};
+
+export function Text(props: Partial): TextShape {
+ const text: TextShape = {
+ type: "text",
+ ...DefaultSphereProperties,
+ ...props,
+ };
+ return text;
+}
diff --git a/vite.config.ts b/vite.config.ts
index 9aff703..45f0048 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -7,6 +7,7 @@ import dts from "vite-plugin-dts";
// https://vitejs.dev/guide/build.html#library-mode
export default defineConfig({
build: {
+ sourcemap: true,
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "skewed",
diff --git a/vite.config.ts.timestamp-1693595836515.mjs b/vite.config.ts.timestamp-1693595836515.mjs
deleted file mode 100644
index f357559..0000000
--- a/vite.config.ts.timestamp-1693595836515.mjs
+++ /dev/null
@@ -1,22 +0,0 @@
-// vite.config.ts
-import { resolve } from "path";
-import { defineConfig } from "file:///Users/francoislaberge/dev/skewed/node_modules/.pnpm/vite@4.0.0_@types+node@18.11.3/node_modules/vite/dist/node/index.js";
-import dts from "file:///Users/francoislaberge/dev/skewed/node_modules/.pnpm/vite-plugin-dts@1.7.1_@types+node@18.11.3_vite@4.0.0/node_modules/vite-plugin-dts/dist/index.mjs";
-var __vite_injected_original_dirname = "/Users/francoislaberge/dev/skewed";
-var vite_config_default = defineConfig({
- build: {
- lib: {
- entry: resolve(__vite_injected_original_dirname, "src/index.ts"),
- name: "skewed",
- fileName: "skewed"
- }
- },
- plugins: [
- // react(),
- dts()
- ]
-});
-export {
- vite_config_default as default
-};
-//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvZnJhbmNvaXNsYWJlcmdlL2Rldi9za2V3ZWRcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9mcmFuY29pc2xhYmVyZ2UvZGV2L3NrZXdlZC92aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvZnJhbmNvaXNsYWJlcmdlL2Rldi9za2V3ZWQvdml0ZS5jb25maWcudHNcIjsvLyB2aXRlLmNvbmZpZy50c1xuaW1wb3J0IHsgcmVzb2x2ZSB9IGZyb20gXCJwYXRoXCI7XG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiO1xuaW1wb3J0IGR0cyBmcm9tIFwidml0ZS1wbHVnaW4tZHRzXCI7XG4vLyBpbXBvcnQgcmVhY3QgZnJvbSBcIkB2aXRlanMvcGx1Z2luLXJlYWN0XCI7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9ndWlkZS9idWlsZC5odG1sI2xpYnJhcnktbW9kZVxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcbiAgYnVpbGQ6IHtcbiAgICBsaWI6IHtcbiAgICAgIGVudHJ5OiByZXNvbHZlKF9fZGlybmFtZSwgXCJzcmMvaW5kZXgudHNcIiksXG4gICAgICBuYW1lOiBcInNrZXdlZFwiLFxuICAgICAgZmlsZU5hbWU6IFwic2tld2VkXCIsXG4gICAgfSxcbiAgfSxcblxuICBwbHVnaW5zOiBbXG4gICAgLy8gcmVhY3QoKSxcbiAgICBkdHMoKSxcbiAgXSxcbn0pO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUNBLFNBQVMsZUFBZTtBQUN4QixTQUFTLG9CQUFvQjtBQUM3QixPQUFPLFNBQVM7QUFIaEIsSUFBTSxtQ0FBbUM7QUFPekMsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsT0FBTztBQUFBLElBQ0wsS0FBSztBQUFBLE1BQ0gsT0FBTyxRQUFRLGtDQUFXLGNBQWM7QUFBQSxNQUN4QyxNQUFNO0FBQUEsTUFDTixVQUFVO0FBQUEsSUFDWjtBQUFBLEVBQ0Y7QUFBQSxFQUVBLFNBQVM7QUFBQTtBQUFBLElBRVAsSUFBSTtBQUFBLEVBQ047QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
diff --git a/workbench/Settings.ts b/workbench/Settings.ts
index 28574c8..8a07572 100644
--- a/workbench/Settings.ts
+++ b/workbench/Settings.ts
@@ -228,7 +228,7 @@ export function getCamera(choice: CameraChoice, zoom: number = 1) {
};
}
-export type Environment = "none" | "underwater" | "grid" | "white-floor";
+export type Environment = "none" | "underwater" | "grid" | "white floor";
export function getEnvironment(environment: Environment = "grid"): Shape {
switch (environment) {
@@ -248,7 +248,7 @@ export function getEnvironment(environment: Environment = "grid"): Shape {
stroke: Color(0, 0, 0),
strokeWidth: 4,
});
- case "white-floor":
+ case "white floor":
document.body.style.backgroundColor = "rgb(32,32,32)";
return Box({
id: "background",
diff --git a/workbench/main.ts b/workbench/main.ts
index d6fdb50..d05b4ff 100644
--- a/workbench/main.ts
+++ b/workbench/main.ts
@@ -8,14 +8,16 @@ import Worm from "./scenes/Worm";
import { getPaused, setPaused } from "./Settings";
import Cylinders from "./scenes/Cylinders";
import SingleCylinder from "./scenes/SingleCylinder";
+import SingleText from "./scenes/SingleText";
-KitchenSink();
+// KitchenSink();
// Transforms();
// Octopus();
// Spheres();
// Cylinders();
// SingleSphere();
// SingleCylinder();
+SingleText();
// Worm();
document
diff --git a/workbench/scenes/SingleText.ts b/workbench/scenes/SingleText.ts
new file mode 100644
index 0000000..d61c4cd
--- /dev/null
+++ b/workbench/scenes/SingleText.ts
@@ -0,0 +1,188 @@
+import {
+ Scene,
+ Vector3,
+ Box,
+ Sphere,
+ Cylinder,
+ render,
+ Group,
+ Grid,
+ Color,
+ Text,
+} from "../../src/index";
+import {
+ getCamera,
+ getEnvironment,
+ getLighting,
+ getPaused,
+ onUpdate,
+} from "../Settings";
+import { Axii } from "../Axii";
+import { type } from "os";
+
+export default function () {
+ const referenceRadius = 75;
+
+ const lightSpeed = 0.3;
+ const lightDistance = 200;
+ const lightSphere = Sphere({
+ // id: "light",
+ radius: 5,
+ fill: Color(255, 255, 0, 0),
+ stroke: Color(255, 255, 0),
+ strokeWidth: 10,
+ });
+
+ const position = Vector3(0, 200, 0);
+ const text = Text({
+ id: "text",
+ text: "Hello",
+ position,
+ fontSize: 270,
+ scale: 1,
+ // radius: referenceRadius,
+ fill: Color(255, 0, 0),
+ stroke: Color(0, 0, 0),
+ strokeWidth: 10,
+ });
+
+ const referenceBox = Box({
+ id: "reference",
+ position: position.clone().add(Vector3(400, 0, 0)),
+ width: 430,
+ height: 130,
+ depth: 2,
+ // radius: referenceRadius,
+ fill: text.fill,
+ stroke: text.stroke,
+ strokeWidth: 0,
+ });
+
+ const fakeShadow = Box({
+ id: "reference",
+ width: 400,
+ height: 1,
+ depth: 3,
+ // radius: referenceRadius,
+ fill: Color(64, 64, 64, 0.5),
+ stroke: Color(0, 0, 0),
+ strokeWidth: 0,
+ });
+
+ const lighting = getLighting("moonlit");
+ lighting.ambientLightColor = Color(128, 128, 128);
+ const scene: Scene = {
+ ...lighting,
+ shapes: [
+ getEnvironment("white floor"),
+ // Axii(Vector3(-referenceRadius * 3, 0, 0)),
+ // Group({
+ // position: Vector3(0, 0, 0),
+ // rotation: Vector3(45, 0, 0),
+ // scale: 3,
+ // children: [
+ text,
+ // fakeShadow,
+ // referenceBox,
+ // lightSphere,
+ ],
+ };
+
+ lightSphere.position = Vector3(1, 1, -1);
+
+ lightSphere.fill = scene.directionalLight.color;
+
+ const { viewport, camera, updateCamera } = getCamera("isometric");
+
+ const onPointerEvent = (event: PointerEvent) => {
+ // return;
+ event.preventDefault();
+ event.stopPropagation();
+
+ const centerX = window.innerWidth / 2;
+ const centerY = window.innerHeight / 2;
+ const diffX = event.clientX - centerX;
+ const diffY = event.clientY - centerY;
+ const distance = Math.sqrt(diffX * diffX + diffY * diffY);
+ const lightSpeed = 2;
+
+ const distanceNormalized = distance / referenceRadius;
+ let degrees = distanceNormalized * 90;
+
+ const spinMode: string = "z";
+
+ if (spinMode === "y") {
+ if (diffX < 0) {
+ degrees *= -1;
+ }
+ lightSphere.position.x = Math.sin((degrees / 180) * Math.PI * lightSpeed);
+ lightSphere.position.y = 0.0;
+ lightSphere.position.z = Math.cos((degrees / 180) * Math.PI * lightSpeed);
+
+ if (event.buttons === 1) {
+ console.log();
+ lightSphere.position.z *= -1;
+ }
+ } else if (spinMode === "z") {
+ lightSphere.position.x = Math.sin((degrees / 180) * Math.PI * lightSpeed);
+ lightSphere.position.y = Math.cos((degrees / 180) * Math.PI * lightSpeed);
+ lightSphere.position.z = 0;
+ }
+
+ lightSphere.position.normalize().multiply(lightDistance).add(position);
+ };
+ document.addEventListener("pointerdown", onPointerEvent);
+ document.addEventListener("pointermove", onPointerEvent);
+ document.addEventListener("pointerup", onPointerEvent);
+
+ const overallSpeed = 1;
+ const rotationSpeed = 1 * overallSpeed;
+
+ onUpdate(({ now, deltaTime }) => {
+ // const cameraSpeed = 0.1 * overallSpeed;
+ const cameraSpeed = 0.0;
+ updateCamera(now * cameraSpeed * 360 + 45, 20);
+
+ // updateCamera(45, 20);
+
+ // text.rotation.y = 90;
+ text.rotation.x = (now * 180 * rotationSpeed) % 360;
+ text.rotation.z = (now * 90 * rotationSpeed) % 360;
+ // text.rotation.z = 45;
+
+ // Amount to make it fully invisible when in isometric view and the camera
+ // isn't rotated
+ text.rotation.y = 135;
+
+ // text.rotation.y = (now * 90 * rotationSpeed) % 360;
+ // text.rotation.x = 20;
+ referenceBox.rotation = text.rotation.clone();
+
+ // fakeShadow.position = text.position.clone().setY(0);
+ // fakeShadow.rotation.y = text.rotation.y;
+ // fakeShadow.scale = text.scale;
+ // text.rotation.y = (now * 120 * rotationSpeed) % 360;
+ // cylinder.rotation.x = 45;
+ // cylinder.rotation.x = 90;
+ // cylinder.rotation.y = now * 90;
+
+ // cylinder.rotation.x = now * 90;
+
+ // lightSphere.position.x =
+ // Math.sin(now * Math.PI * 2 * lightSpeed) * lightDistance;
+ // lightSphere.position.y = 0;
+ // lightSphere.position.z =
+ // Math.cos(now * Math.PI * 2 * lightSpeed) * lightDistance;
+
+ // lightSphere.position = Vector3(1, 0, 1).multiply(lightDistance);
+
+ scene.directionalLight.direction = lightSphere.position
+ .clone()
+ .normalize()
+ .multiply(-1);
+
+ // lightSphere.position.y = height / 2;
+
+ render(document.getElementById("root")!, scene, viewport, camera);
+ });
+}
diff --git a/workbench/scenes/Transforms.ts b/workbench/scenes/Transforms.ts
index 10f505a..291f72f 100644
--- a/workbench/scenes/Transforms.ts
+++ b/workbench/scenes/Transforms.ts
@@ -21,7 +21,7 @@ export default function () {
const scene: Scene = {
...getLighting("moonlit"),
shapes: [
- getEnvironment("white-floor"),
+ getEnvironment("white floor"),
Group({
position: Vector3(-450, 50, -450),
diff --git a/workbench/scenes/Worm.ts b/workbench/scenes/Worm.ts
index d8857ee..640a430 100644
--- a/workbench/scenes/Worm.ts
+++ b/workbench/scenes/Worm.ts
@@ -21,14 +21,15 @@ import {
export default function () {
const scene: Scene = {
...getLighting("moonlit"),
- shapes: [getEnvironment("white-floor")],
+ shapes: [getEnvironment("white floor")],
};
+ const scale = 1.5;
const spheres: SphereShape[] = [];
const sphereCount = 100;
for (let i = 0; i < sphereCount; i++) {
const sphere = Sphere({
- radius: 30,
+ radius: 30 * scale,
fill: Color(255, 128, 255),
position: Vector3(i * 100, 0, 0),
strokeWidth: 0,
@@ -46,9 +47,12 @@ export default function () {
const degreeOffet = (index / sphereCount) * 360;
const finalDegree = degreeOffet + percent * 360;
- sphere.position.x = Math.sin((finalDegree / 180) * Math.PI) * 40;
- sphere.position.y = sphere.radius;
- sphere.position.z = (index * 400) / (sphereCount - 1) - 200;
+ sphere.position.x = Math.sin((finalDegree / 180) * Math.PI) * 40 - 50;
+ sphere.position.x *= scale;
+ sphere.position.y = sphere.radius / 2;
+ sphere.position.y *= scale;
+ sphere.position.z = (index * 300) / (sphereCount - 1) - 200;
+ sphere.position.z *= scale;
});
const cameraSpeed = 0.0;