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;