From 6c96e997c6be04d60f29774372f7578b9b59ae80 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 22 Oct 2020 06:37:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(=F0=9F=93=89):=20addLine()=20and=20addArc(?= =?UTF-8?q?)=20(#383)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Matrix4.ts | 270 ++++++++++++++++++++++++++++++++++ src/Paths.ts | 35 +++++ src/__tests__/Matrix4.test.ts | 40 +++++ src/__tests__/index.ts | 1 + src/index.ts | 1 + 5 files changed, 347 insertions(+) create mode 100644 src/Matrix4.ts create mode 100644 src/__tests__/Matrix4.test.ts diff --git a/src/Matrix4.ts b/src/Matrix4.ts new file mode 100644 index 00000000..466f8fea --- /dev/null +++ b/src/Matrix4.ts @@ -0,0 +1,270 @@ +export type Vec4 = readonly [number, number, number, number]; + +export type Matrix4 = readonly [Vec4, Vec4, Vec4, Vec4]; + +type Transform3dName = + | "translateX" + | "translateY" + | "translateZ" + | "scale" + | "scaleX" + | "scaleY" + | "skewX" + | "skewY" + | "rotateZ" + | "rotate" + | "perspective" + | "rotateX" + | "rotateY" + | "rotateZ" + | "matrix"; + +type Transformations = { + [Name in Transform3dName]: Name extends "matrix" ? Matrix4 : number; +}; +export type Transforms3d = ( + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick +)[]; + +const exhaustiveCheck = (a: never): never => { + throw new Error(`Unexhaustive handling for ${a}`); +}; + +export const identityMatrix4: Matrix4 = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], +]; + +const translateXMatrix = (x: number): Matrix4 => { + "worklet"; + return [ + [1, 0, 0, x], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; +}; + +const translateYMatrix = (y: number): Matrix4 => { + "worklet"; + return [ + [1, 0, 0, 0], + [0, 1, 0, y], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; +}; + +const translateZMatrix = (z: number): Matrix4 => { + "worklet"; + return [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, z], + [0, 0, 0, 1], + ]; +}; + +const scaleMatrix = (s: number): Matrix4 => { + "worklet"; + return [ + [s, 0, 0, 0], + [0, s, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; +}; + +const scaleXMatrix = (s: number): Matrix4 => { + "worklet"; + return [ + [s, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; +}; + +const skewXMatrix = (s: number): Matrix4 => { + "worklet"; + return [ + [1, Math.tan(s), 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; +}; + +const skewYMatrix = (s: number): Matrix4 => { + "worklet"; + return [ + [1, 0, 0, 0], + [Math.tan(s), 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; +}; + +const scaleYMatrix = (s: number): Matrix4 => { + "worklet"; + return [ + [1, 0, 0, 0], + [0, s, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; +}; + +const perspectiveMatrix = (p: number): Matrix4 => { + "worklet"; + return [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, -1 / p, 1], + ]; +}; + +const rotateXMatrix = (r: number): Matrix4 => { + "worklet"; + return [ + [1, 0, 0, 0], + [0, Math.cos(r), -Math.sin(r), 0], + [0, Math.sin(r), Math.cos(r), 0], + [0, 0, 0, 1], + ]; +}; + +const rotateYMatrix = (r: number): Matrix4 => { + "worklet"; + return [ + [Math.cos(r), 0, Math.sin(r), 0], + [0, 1, 0, 0], + [-Math.sin(r), 0, Math.cos(r), 0], + [0, 0, 0, 1], + ]; +}; + +const rotateZMatrix = (r: number): Matrix4 => { + "worklet"; + return [ + [Math.cos(r), -Math.sin(r), 0, 0], + [Math.sin(r), Math.cos(r), 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; +}; + +export const dot4 = (row: Vec4, col: Vec4) => { + "worklet"; + return row[0] * col[0] + row[1] * col[1] + row[2] * col[2] + row[3] * col[3]; +}; + +export const matrixVecMul4 = (m: Matrix4, v: Vec4) => + [dot4(m[0], v), dot4(m[1], v), dot4(m[2], v), dot4(m[3], v)] as const; + +export const multiply4 = (m1: Matrix4, m2: Matrix4) => { + const col0 = [m2[0][0], m2[1][0], m2[2][0], m2[3][0]] as const; + const col1 = [m2[0][1], m2[1][1], m2[2][1], m2[3][1]] as const; + const col2 = [m2[0][2], m2[1][2], m2[2][2], m2[3][2]] as const; + const col3 = [m2[0][3], m2[1][3], m2[2][3], m2[3][3]] as const; + return [ + [ + dot4(m1[0], col0), + dot4(m1[0], col1), + dot4(m1[0], col2), + dot4(m1[0], col3), + ], + [ + dot4(m1[1], col0), + dot4(m1[1], col1), + dot4(m1[1], col2), + dot4(m1[1], col3), + ], + [ + dot4(m1[2], col0), + dot4(m1[2], col1), + dot4(m1[2], col2), + dot4(m1[2], col3), + ], + [ + dot4(m1[3], col0), + dot4(m1[3], col1), + dot4(m1[3], col2), + dot4(m1[3], col3), + ], + ] as const; +}; + +export const processTransform3d = (transforms: Transforms3d) => + transforms.reduce((acc, transform) => { + const key = Object.keys(transform)[0] as Transform3dName; + if (key === "translateX") { + const value = (transform as Pick)[key]; + return multiply4(acc, translateXMatrix(value)); + } + if (key === "translateY") { + const value = (transform as Pick)[key]; + return multiply4(acc, translateYMatrix(value)); + } + if (key === "translateZ") { + const value = (transform as Pick)[key]; + return multiply4(acc, translateZMatrix(value)); + } + if (key === "scale") { + const value = (transform as Pick)[key]; + return multiply4(acc, scaleMatrix(value)); + } + if (key === "scaleX") { + const value = (transform as Pick)[key]; + return multiply4(acc, scaleXMatrix(value)); + } + if (key === "scaleY") { + const value = (transform as Pick)[key]; + return multiply4(acc, scaleYMatrix(value)); + } + if (key === "skewX") { + const value = (transform as Pick)[key]; + return multiply4(acc, skewXMatrix(value)); + } + if (key === "skewY") { + const value = (transform as Pick)[key]; + return multiply4(acc, skewYMatrix(value)); + } + if (key === "rotateX") { + const value = (transform as Pick)[key]; + return multiply4(acc, rotateXMatrix(value)); + } + if (key === "rotateY") { + const value = (transform as Pick)[key]; + return multiply4(acc, rotateYMatrix(value)); + } + if (key === "perspective") { + const value = (transform as Pick)[key]; + return multiply4(acc, perspectiveMatrix(value)); + } + if (key === "rotate" || key === "rotateZ") { + const value = (transform as Pick)[key]; + return multiply4(acc, rotateZMatrix(value)); + } + if (key === "matrix") { + const matrix = (transform as Pick)[key]; + return multiply4(acc, matrix); + } + return exhaustiveCheck(key); + }, identityMatrix4); diff --git a/src/Paths.ts b/src/Paths.ts index 719036e1..2c43aa53 100644 --- a/src/Paths.ts +++ b/src/Paths.ts @@ -167,6 +167,27 @@ export const createPath = (move: Vector): Path => { }; }; +/** + * @summary Add an arc command to a path + */ +export const addArc = (path: Path, corner: Vector, to: Vector) => { + "worklet"; + const last = path.curves[path.curves.length - 1]; + const from = last ? last.to : path.move; + const arc = 9 / 16; + path.curves.push({ + c1: { + x: (corner.x - from.x) * arc + from.x, + y: (corner.y - from.y) * arc + from.y, + }, + c2: { + x: (corner.x - to.x) * arc + to.x, + y: (corner.y - to.y) * arc + to.y, + }, + to, + }); +}; + /** * @summary Add a cubic Bèzier curve command to a path. */ @@ -179,6 +200,20 @@ export const addCurve = (path: Path, c: Curve) => { }); }; +/** + * @summary Add a line command to a path. + */ +export const addLine = (path: Path, to: Vector) => { + "worklet"; + const last = path.curves[path.curves.length - 1]; + const from = last ? last.to : path.move; + path.curves.push({ + c1: from, + c2: to, + to, + }); +}; + /** * @summary Add a quadratic Bèzier curve command to a path. */ diff --git a/src/__tests__/Matrix4.test.ts b/src/__tests__/Matrix4.test.ts new file mode 100644 index 00000000..ae2ae914 --- /dev/null +++ b/src/__tests__/Matrix4.test.ts @@ -0,0 +1,40 @@ +import { + identityMatrix4, + matrixVecMul4, + multiply4, + processTransform3d, +} from "../Matrix4"; + +test("processTransform3d()", () => { + expect( + processTransform3d([{ rotateX: Math.PI }, { rotateY: Math.PI }]) + ).toStrictEqual([ + [-1, 0, 1.2246467991473532e-16, 0], + [1.4997597826618576e-32, -1, 1.2246467991473532e-16, 0], + [1.2246467991473532e-16, 1.2246467991473532e-16, 1, 0], + [0, 0, 0, 1], + ]); +}); + +test("multiply4()", () => { + expect( + multiply4( + identityMatrix4, + processTransform3d([{ rotateX: Math.PI }, { rotateY: Math.PI }]) + ) + ).toStrictEqual([ + [-1, 0, 1.2246467991473532e-16, 0], + [1.4997597826618576e-32, -1, 1.2246467991473532e-16, 0], + [1.2246467991473532e-16, 1.2246467991473532e-16, 1, 0], + [0, 0, 0, 1], + ]); +}); + +test("matrixVecMul4()", () => { + expect( + matrixVecMul4( + processTransform3d([{ rotateX: Math.PI }, { rotateY: Math.PI }]), + [0.5, 0.5, 0, 1] + ) + ).toStrictEqual([-0.5, -0.5, 1.2246467991473532e-16, 1]); +}); diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index 47a42267..9a374024 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -3,3 +3,4 @@ import "./Math.test"; import "./Physics.test"; import "./Paths.test"; import "./Array.test"; +import "./Matrix4.test"; diff --git a/src/index.ts b/src/index.ts index 86100ae3..3e510dfa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,4 +7,5 @@ export * from "./Colors"; export * from "./Paths"; export * from "./Physics"; export * from "./Array"; +export * from "./Matrix4"; export { default as ReText } from "./ReText";