From bf47f69b04f76f78a1ca749099655f2f0d0277be Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 5 Jun 2019 17:40:46 -0400 Subject: [PATCH 1/5] use a destination parameter for bounds; add initFromMatrixRadius - pass bounds as a destination parameter - add initFromMatrixRadius - use initFromMatrixRadius in getAABB --- src/Drawable.js | 25 ++++++++++--------------- src/Rectangle.js | 25 +++++++++++++++++++++++++ src/Skin.js | 4 ++-- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/Drawable.js b/src/Drawable.js index 1d90a2977..8c8703300 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -453,7 +453,7 @@ class Drawable { * Before calling this, ensure the renderer has updated convex hull points. * @return {!Rectangle} Bounds for a tight box around the Drawable. */ - getBounds () { + getBounds (bounds) { if (this.needsConvexHullPoints()) { throw new Error('Needs updated convex hull points before bounds calculation.'); } @@ -462,7 +462,7 @@ class Drawable { } const transformedHullPoints = this._getTransformedHullPoints(); // Search through transformed points to generate box on axes. - const bounds = new Rectangle(); + bounds = bounds || new Rectangle(); bounds.initFromPointsAABB(transformedHullPoints); return bounds; } @@ -473,7 +473,7 @@ class Drawable { * Before calling this, ensure the renderer has updated convex hull points. * @return {!Rectangle} Bounds for a tight box around a slice of the Drawable. */ - getBoundsForBubble () { + getBoundsForBubble (bounds) { if (this.needsConvexHullPoints()) { throw new Error('Needs updated convex hull points before bubble bounds calculation.'); } @@ -485,7 +485,7 @@ class Drawable { const maxY = Math.max.apply(null, transformedHullPoints.map(p => p[1])); const filteredHullPoints = transformedHullPoints.filter(p => p[1] > maxY - slice); // Search through filtered points to generate box on axes. - const bounds = new Rectangle(); + bounds = bounds || new Rectangle(); bounds.initFromPointsAABB(filteredHullPoints); return bounds; } @@ -499,18 +499,13 @@ class Drawable { * faster to calculate so may be desired for quick checks/optimizations. * @return {!Rectangle} Rough axis-aligned bounding box for Drawable. */ - getAABB () { + getAABB (bounds) { if (this._transformDirty) { this._calculateTransform(); } const tm = this._uniforms.u_modelMatrix; - const bounds = new Rectangle(); - bounds.initFromPointsAABB([ - twgl.m4.transformPoint(tm, [-0.5, -0.5, 0]), - twgl.m4.transformPoint(tm, [0.5, -0.5, 0]), - twgl.m4.transformPoint(tm, [-0.5, 0.5, 0]), - twgl.m4.transformPoint(tm, [0.5, 0.5, 0]) - ]); + bounds = bounds || new Rectangle(); + bounds.initFromMatrixRadius(tm, 0.5); return bounds; } @@ -520,12 +515,12 @@ class Drawable { * known, but otherwise return the rough AABB of the Drawable. * @return {!Rectangle} Bounds for the Drawable. */ - getFastBounds () { + getFastBounds (bounds) { this.updateMatrix(); if (!this.needsConvexHullPoints()) { - return this.getBounds(); + return this.getBounds(bounds); } - return this.getAABB(); + return this.getAABB(bounds); } /** diff --git a/src/Rectangle.js b/src/Rectangle.js index 7659e22f1..726a151f5 100644 --- a/src/Rectangle.js +++ b/src/Rectangle.js @@ -54,6 +54,31 @@ class Rectangle { } } + initFromMatrixRadius (m, r) { + // const v0 = r; + // const v1 = r; + // const v2 = r; + const m00 = m[(0 * 4) + 0]; + const m01 = m[(0 * 4) + 1]; + const m10 = m[(1 * 4) + 0]; + const m11 = m[(1 * 4) + 1]; + const m30 = m[(3 * 4) + 0]; + const m31 = m[(3 * 4) + 1]; + // var d = v0 * m03 + v1 * m13 + v2 * m23 + m33; + // dst[0] = ( + const x = Math.abs(r * m00) + Math.abs(r * m10); + // + v2 * m20 + m30) / d; + // dst[1] = ( + const y = Math.abs(r * m01) + Math.abs(r * m11); + // + v2 * m21 + m31) / d; + // dst[2] = (v0 * m02 + v1 * m12 + v2 * m22 + m32) / d; + + this.left = -x + m30; + this.right = x + m30; + this.top = y + m31; + this.bottom = -y + m31; + } + /** * Determine if this Rectangle intersects some other. * Note that this is a comparison assuming the Rectangle was diff --git a/src/Skin.js b/src/Skin.js index e0e741357..8d124bed6 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -148,8 +148,8 @@ class Skin extends EventEmitter { * @param {Array} drawable - The Drawable instance this skin is using. * @return {!Rectangle} The drawable's bounds. */ - getFenceBounds (drawable) { - return drawable.getFastBounds(); + getFenceBounds (drawable, bounds) { + return drawable.getFastBounds(bounds); } /** From a840089bc94b410dc6a64298be5493ee03e46dd9 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 5 Jun 2019 18:13:01 -0400 Subject: [PATCH 2/5] fence bounds --- src/BitmapSkin.js | 5 +++-- src/Drawable.js | 34 +++++++++++++++++++--------------- src/RenderWebGL.js | 7 ++++--- src/Skin.js | 5 +++-- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/BitmapSkin.js b/src/BitmapSkin.js index b4b03d3b7..94f2984c4 100644 --- a/src/BitmapSkin.js +++ b/src/BitmapSkin.js @@ -62,10 +62,11 @@ class BitmapSkin extends Skin { /** * Get the bounds of the drawable for determining its fenced position. * @param {Array} drawable - The Drawable instance this skin is using. + * @param {?Rectangle} result - Optional destination for bounds calculation. * @return {!Rectangle} The drawable's bounds. For compatibility with Scratch 2, we always use getAABB for bitmaps. */ - getFenceBounds (drawable) { - return drawable.getAABB(); + getFenceBounds (drawable, result) { + return drawable.getAABB(result); } /** diff --git a/src/Drawable.js b/src/Drawable.js index 8c8703300..6ab8a1c36 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -451,9 +451,10 @@ class Drawable { * This function applies the transform matrix to the known convex hull, * and then finds the minimum box along the axes. * Before calling this, ensure the renderer has updated convex hull points. + * @param {?Rectangle} result optional destination for bounds calculation * @return {!Rectangle} Bounds for a tight box around the Drawable. */ - getBounds (bounds) { + getBounds (result) { if (this.needsConvexHullPoints()) { throw new Error('Needs updated convex hull points before bounds calculation.'); } @@ -462,18 +463,19 @@ class Drawable { } const transformedHullPoints = this._getTransformedHullPoints(); // Search through transformed points to generate box on axes. - bounds = bounds || new Rectangle(); - bounds.initFromPointsAABB(transformedHullPoints); - return bounds; + result = result || new Rectangle(); + result.initFromPointsAABB(transformedHullPoints); + return result; } /** * Get the precise bounds for the upper 8px slice of the Drawable. * Used for calculating where to position a text bubble. * Before calling this, ensure the renderer has updated convex hull points. + * @param {?Rectangle} result optional destination for bounds calculation * @return {!Rectangle} Bounds for a tight box around a slice of the Drawable. */ - getBoundsForBubble (bounds) { + getBoundsForBubble (result) { if (this.needsConvexHullPoints()) { throw new Error('Needs updated convex hull points before bubble bounds calculation.'); } @@ -485,9 +487,9 @@ class Drawable { const maxY = Math.max.apply(null, transformedHullPoints.map(p => p[1])); const filteredHullPoints = transformedHullPoints.filter(p => p[1] > maxY - slice); // Search through filtered points to generate box on axes. - bounds = bounds || new Rectangle(); - bounds.initFromPointsAABB(filteredHullPoints); - return bounds; + result = result || new Rectangle(); + result.initFromPointsAABB(filteredHullPoints); + return result; } /** @@ -497,30 +499,32 @@ class Drawable { * which is tightly snapped to account for a Drawable's transparent regions. * `getAABB` returns a much less accurate bounding box, but will be much * faster to calculate so may be desired for quick checks/optimizations. + * @param {?Rectangle} result optional destination for bounds calculation * @return {!Rectangle} Rough axis-aligned bounding box for Drawable. */ - getAABB (bounds) { + getAABB (result) { if (this._transformDirty) { this._calculateTransform(); } const tm = this._uniforms.u_modelMatrix; - bounds = bounds || new Rectangle(); - bounds.initFromMatrixRadius(tm, 0.5); - return bounds; + result = result || new Rectangle(); + result.initFromMatrixRadius(tm, 0.5); + return result; } /** * Return the best Drawable bounds possible without performing graphics queries. * I.e., returns the tight bounding box when the convex hull points are already * known, but otherwise return the rough AABB of the Drawable. + * @param {?Rectangle} result optional destination for bounds calculation * @return {!Rectangle} Bounds for the Drawable. */ - getFastBounds (bounds) { + getFastBounds (result) { this.updateMatrix(); if (!this.needsConvexHullPoints()) { - return this.getBounds(bounds); + return this.getBounds(result); } - return this.getAABB(bounds); + return this.getAABB(result); } /** diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 478245512..ba629eacb 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -16,6 +16,7 @@ const log = require('./util/log'); const __isTouchingDrawablesPoint = twgl.v3.create(); const __candidatesBounds = new Rectangle(); +const __fenceBounds = new Rectangle(); const __touchingColor = new Uint8ClampedArray(4); const __blendColor = new Uint8ClampedArray(4); @@ -1357,7 +1358,7 @@ class RenderWebGL extends EventEmitter { const dx = x - drawable._position[0]; const dy = y - drawable._position[1]; - const aabb = drawable._skin.getFenceBounds(drawable); + const aabb = drawable._skin.getFenceBounds(drawable, __fenceBounds); const inset = Math.floor(Math.min(aabb.width, aabb.height) / 2); const sx = this._xRight - Math.min(FENCE_WIDTH, inset); @@ -1627,14 +1628,14 @@ class RenderWebGL extends EventEmitter { } twgl.setUniforms(currentShader, uniforms); - + /* adjust blend function for this skin */ if (drawable.skin.hasPremultipliedAlpha){ gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } else { gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } - + twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES); } diff --git a/src/Skin.js b/src/Skin.js index 8d124bed6..473c70bdf 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -146,10 +146,11 @@ class Skin extends EventEmitter { /** * Get the bounds of the drawable for determining its fenced position. * @param {Array} drawable - The Drawable instance this skin is using. + * @param {?Rectangle} result - Optional destination for bounds calculation. * @return {!Rectangle} The drawable's bounds. */ - getFenceBounds (drawable, bounds) { - return drawable.getFastBounds(bounds); + getFenceBounds (drawable, result) { + return drawable.getFastBounds(result); } /** From 5d7957ff9b13a23d043f236928fdc8e69c55e60c Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Tue, 11 Jun 2019 14:19:13 -0400 Subject: [PATCH 3/5] document out how initFromModelMatrix works --- src/Drawable.js | 2 +- src/Rectangle.js | 98 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/src/Drawable.js b/src/Drawable.js index 6ab8a1c36..ea94acdc2 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -508,7 +508,7 @@ class Drawable { } const tm = this._uniforms.u_modelMatrix; result = result || new Rectangle(); - result.initFromMatrixRadius(tm, 0.5); + result.initFromModelMatrix(tm); return result; } diff --git a/src/Rectangle.js b/src/Rectangle.js index 726a151f5..56204a060 100644 --- a/src/Rectangle.js +++ b/src/Rectangle.js @@ -54,25 +54,99 @@ class Rectangle { } } - initFromMatrixRadius (m, r) { - // const v0 = r; - // const v1 = r; - // const v2 = r; + /** + * Initialize a Rectangle to a 1 unit square transformed by a model matrix. + * @param {Array.} m A 4x4 matrix to transform the rectangle by. + */ + initFromModelMatrix (m) { + // Treat this function like we are transforming a vector with each + // component set to 0.5 by a matrix m. + // const v0 = 0.5; + // const v1 = 0.5; + // const v2 = 0.5; + + // Of the matrix to do this in 2D space, instead of the 3D provided by + // the matrix, we need the 2x2 "top left" that represents the scale and + // rotation ... const m00 = m[(0 * 4) + 0]; const m01 = m[(0 * 4) + 1]; const m10 = m[(1 * 4) + 0]; const m11 = m[(1 * 4) + 1]; + // ... and the 1x2 "top right" that represents position. const m30 = m[(3 * 4) + 0]; const m31 = m[(3 * 4) + 1]; - // var d = v0 * m03 + v1 * m13 + v2 * m23 + m33; - // dst[0] = ( - const x = Math.abs(r * m00) + Math.abs(r * m10); - // + v2 * m20 + m30) / d; - // dst[1] = ( - const y = Math.abs(r * m01) + Math.abs(r * m11); - // + v2 * m21 + m31) / d; - // dst[2] = (v0 * m02 + v1 * m12 + v2 * m22 + m32) / d; + // This is how we would normally transform the vector by the matrix. + // var determinant = v0 * m03 + v1 * m13 + v2 * m23 + m33; + // dst[0] = (v0 * m00 + v1 * m10 + v2 * m20 + m30) / determinant; + // dst[1] = (v0 * m01 + v1 * m11 + v2 * m21 + m31) / determinant; + // dst[2] = (v0 * m02 + v1 * m12 + v2 * m22 + m32) / determinant; + + // We can skip the v2 multiplications and the determinant. + + // Alternatively done with 4 vectors, those vectors would be reflected + // on the x and y axis. We can build those 4 vectors by transforming the + // parts of one vector and reflecting them on the axises after + // multiplication. + + // const x0 = 0.5 * m00; + // const x1 = 0.5 * m10; + // const y0 = 0.5 * m01; + // const y1 = 0.5 * m11; + + // const p0x = x0 + x1; + // const p0y = y0 + y1; + // const p1x = -x0 + x1; + // const p1y = -y0 + y1; + // const p2x = -x0 + -x1; + // const p2y = -y0 + -y1; + // const p3x = x0 + -x1; + // const p3y = y0 + -y1; + + // Since we want to reduce those 4 points to a min and max for each + // axis, we can use those multiplied components to build the min and max + // values without comparing the points. + + // We can start by getting the min and max for each of all the points. + // const left = Math.min(x0 + x1, -x0 + x1, -x0 + -x1, x0 + -x1); + // const right = Math.max(x0 + x1, -x0 + x1, -x0 + -x1, x0 + -x1); + // const top = Math.max(y0 + y1, -y0 + y1, -y0 + -y1, y0 + -y1); + // const bottom = Math.min(y0 + y1, -y0 + y1, -y0 + -y1, y0 + -y1); + + // Each of those can be replaced with min and max operations on the 0 + // and 1 matrix output components. + // const left = Math.min(x0, -x0) + Math.min(x1, -x1); + // const right = Math.max(x0, -x0) + Math.max(x1, -x1); + // const top = Math.max(y0, -y0) + Math.max(y1, -y1); + // const bottom = Math.min(y0, -y0) + Math.min(y1, -y1); + + // And they can be replaced with absolute values. + // const left = -Math.abs(x0) + -Math.abs(x1); + // const right = Math.abs(x0) + Math.abs(x1); + // const top = Math.abs(y0) + Math.abs(y1); + // const bottom = -Math.abs(y0) + -Math.abs(y1); + + // And those with positive and negative sums of the absolute values. + // const left = -(Math.abs(x0) + Math.abs(x1)); + // const right = +(Math.abs(x0) + Math.abs(x1)); + // const top = +(Math.abs(y0) + Math.abs(y1)); + // const bottom = -(Math.abs(y0) + -Math.abs(y1)); + + // We can perform those sums once and reuse them for the bounds. + // const x = Math.abs(x0) + Math.abs(x1); + // const y = Math.abs(y0) + Math.abs(y1); + // const left = -x; + // const right = x; + // const top = y; + // const bottom = -y; + + // Building those absolute sums for the 0.5 vector components by the + // matrix components ... + const x = Math.abs(0.5 * m00) + Math.abs(0.5 * m10); + const y = Math.abs(0.5 * m01) + Math.abs(0.5 * m11); + + // And adding them to the position components in the matrices + // initializes our Rectangle. this.left = -x + m30; this.right = x + m30; this.top = y + m31; From b7004878ff403cb2f50794568706f3712f45ed05 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 19 Jun 2019 18:36:17 -0400 Subject: [PATCH 4/5] set jsdoc tutorials to come from docs --- .jsdoc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.jsdoc.json b/.jsdoc.json index f4d8b9fcb..b047b6911 100644 --- a/.jsdoc.json +++ b/.jsdoc.json @@ -15,6 +15,7 @@ "private": true, "readme": "README.md", "recurse": true, - "template": "node_modules/docdash" + "template": "node_modules/docdash", + "tutorials": "docs" } } From 994e9be00bb518cf243cdd52b8b26a5275b20424 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 19 Jun 2019 18:36:41 -0400 Subject: [PATCH 5/5] move the initFromModelMatrix process docs into a jsdoc tutorial --- docs/Rectangle-AABB-Matrix.md | 192 ++++++++++++++++++++++++++++++++++ src/Rectangle.js | 96 ++--------------- 2 files changed, 203 insertions(+), 85 deletions(-) create mode 100644 docs/Rectangle-AABB-Matrix.md diff --git a/docs/Rectangle-AABB-Matrix.md b/docs/Rectangle-AABB-Matrix.md new file mode 100644 index 000000000..e0c33b4a8 --- /dev/null +++ b/docs/Rectangle-AABB-Matrix.md @@ -0,0 +1,192 @@ +# Rectangle AABB Matrix + +Initialize a Rectangle to a 1 unit square centered at 0 x 0 transformed by a model matrix. + +----- + +Every drawable is a 1 x 1 unit square that is rotated by its direction, scaled by its skin size and scale, and offset by its rotation center and position. The square representation is made up of 4 points that are transformed by the drawable properties. Often we want a shape that simplifies those 4 points into a non-rotated shape, a axis aligned bounding box. + +One approach is to compare the x and y components of each transformed vector and find the minimum and maximum x component and the minimum and maximum y component. + +We can start from this approach and determine an alternative one that prodcues the same output with less work. + +Starting with transforming one point, here is a 3D point, `v`, transformation by a matrix, `m`. + +```js +const v0 = v[0]; +const v1 = v[1]; +const v2 = v[2]; + +const d = v0 * m[(0 * 4) + 3] + v1 * m[(1 * 4) + 3] + v2 * m[(2 * 4) + 3] + m[(3 * 4) + 3]; +dst[0] = (v0 * m[(0 * 4) + 0] + v1 * m[(1 * 4) + 0] + v2 * m[(2 * 4) + 0] + m[(3 * 4) + 0]) / d; +dst[1] = (v0 * m[(0 * 4) + 1] + v1 * m[(1 * 4) + 1] + v2 * m[(2 * 4) + 1] + m[(3 * 4) + 1]) / d; +dst[2] = (v0 * m[(0 * 4) + 2] + v1 * m[(1 * 4) + 2] + v2 * m[(2 * 4) + 2] + m[(3 * 4) + 2]) / d; +``` + +As this is a 2D rectangle we can cancel out the third dimension, and the determinant, 'd'. + +```js +const v0 = v[0]; +const v1 = v[1]; + +dst = [ + v0 * m[(0 * 4) + 0] + v1 * m[(1 * 4) + 0] + m[(3 * 4) + 0, + v0 * m[(0 * 4) + 1] + v1 * m[(1 * 4) + 1] + m[(3 * 4) + 1 +]; +``` + +Let's set the matrix points to shorter names for convenience. + +```js +const m00 = m[(0 * 4) + 0]; +const m01 = m[(0 * 4) + 1]; +const m10 = m[(1 * 4) + 0]; +const m11 = m[(1 * 4) + 1]; +const m30 = m[(3 * 4) + 0]; +const m31 = m[(3 * 4) + 1]; +``` + +We need 4 points with positive and negative 0.5 values so the square has sides of length 1. + +```js +let p = [0.5, 0.5]; +let q = [-0.5, 0.5]; +let r = [-0.5, -0.5]; +let s = [0.5, -0.5]; +``` + +Transform the points by the matrix. + +```js +p = [ + 0.5 * m00 + 0.5 * m10 + m30, + 0.5 * m01 + 0.5 * m11 + m31 +]; +q = [ + -0.5 * m00 + -0.5 * m10 + m30, + 0.5 * m01 + 0.5 * m11 + m31 +]; +r = [ + -0.5 * m00 + -0.5 * m10 + m30, + -0.5 * m01 + -0.5 * m11 + m31 +]; +s = [ + 0.5 * m00 + 0.5 * m10 + m30, + -0.5 * m01 + -0.5 * m11 + m31 +]; +``` + +With 4 transformed points we can build the left, right, top, and bottom values for the Rectangle. Each will use the minimum or the maximum of one of the components of all points. + +```js +const left = Math.min(p[0], q[0], r[0], s[0]); +const right = Math.max(p[0], q[0], r[0], s[0]); +const top = Math.max(p[1], q[1], r[1], s[1]); +const bottom = Math.min(p[1], q[1], r[1], s[1]); +``` + +Fill those calls with the vector expressions. + +```js +const left = Math.min( + 0.5 * m00 + 0.5 * m10 + m30, + -0.5 * m00 + 0.5 * m10 + m30, + -0.5 * m00 + -0.5 * m10 + m30, + 0.5 * m00 + -0.5 * m10 + m30 +); +const right = Math.max( + 0.5 * m00 + 0.5 * m10 + m30, + -0.5 * m00 + 0.5 * m10 + m30, + -0.5 * m00 + -0.5 * m10 + m30, + 0.5 * m00 + -0.5 * m10 + m30 +); +const top = Math.max( + 0.5 * m01 + 0.5 * m11 + m31, + -0.5 * m01 + 0.5 * m11 + m31, + -0.5 * m01 + -0.5 * m11 + m31, + 0.5 * m01 + -0.5 * m11 + m31 +); +const bottom = Math.min( + 0.5 * m01 + 0.5 * m11 + m31, + -0.5 * m01 + 0.5 * m11 + m31, + -0.5 * m01 + -0.5 * m11 + m31, + 0.5 * m01 + -0.5 * m11 + m31 +); +``` + +Pull out the `0.5 * m??` patterns. + +```js +const x0 = 0.5 * m00; +const x1 = 0.5 * m10; +const y0 = 0.5 * m01; +const y1 = 0.5 * m11; + +const left = Math.min(x0 + x1 + m30, -x0 + x1 + m30, -x0 + -x1 + m30, x0 + -x1 + m30); +const right = Math.max(x0 + x1 + m30, -x0 + x1 + m30, -x0 + -x1 + m30, x0 + -x1 + m30); +const top = Math.max(y0 + y1 + m31, -y0 + y1 + m31, -y0 + -y1 + m31, y0 + -y1 + m31); +const bottom = Math.min(y0 + y1 + m31, -y0 + y1 + m31, -y0 + -y1 + m31, y0 + -y1 + m31); +``` + +Now each argument for the min and max calls take an expression like `(a * x0 + b * x1 + m3?)`. As each expression has the x0, x1, and m3? variables we can split the min and max calls on the addition operators. Each new call has all the coefficients of that variable. + +```js +const left = Math.min(x0, -x0) + Math.min(x1, -x1) + Math.min(m30, m30); +const right = Math.max(x0, -x0) + Math.max(x1, -x1) + Math.max(m30, m30); +const top = Math.max(y0, -y0) + Math.max(y1, -y1) + Math.max(m31, m31); +const bottom = Math.min(y0, -y0) + Math.min(y1, -y1) + Math.min(m31, m31); +``` + +The min or max of two copies of the same value will just be that value. + +```js +const left = Math.min(x0, -x0) + Math.min(x1, -x1) + m30; +const right = Math.max(x0, -x0) + Math.max(x1, -x1) + m30; +const top = Math.max(y0, -y0) + Math.max(y1, -y1) + m31; +const bottom = Math.min(y0, -y0) + Math.min(y1, -y1) + m31; +``` + +The max of a negative and positive variable will be the absolute value of that variable. The min of a negative and positive variable will the negated absolute value of that variable. + +```js +const left = -Math.abs(x0) + -Math.abs(x1) + m30; +const right = Math.abs(x0) + Math.abs(x1) + m30; +const top = Math.abs(y0) + Math.abs(y1) + m31; +const bottom = -Math.abs(y0) + -Math.abs(y1) + m31; +``` + +Pulling out the negations of the absolute values, left and right as well as top and bottom are the positive or negative sum of the absolute value of the saled and rotated unit value. + +```js +const left = -(Math.abs(x0) + Math.abs(x1)) + m30; +const right = Math.abs(x0) + Math.abs(x1) + m30; +const top = Math.abs(y0) + Math.abs(y1) + m31; +const bottom = -(Math.abs(y0) + Math.abs(y1)) + m31; +``` + +We call pull out those sums and use them twice. + +```js +const x = Math.abs(x0) + Math.abs(x1); +const y = Math.abs(y0) + Math.abs(y1); + +const left = -x + m30; +const right = x + m30; +const top = y + m31; +const bottom = -y + m31; +``` + +This lets us arrive at our goal. Inlining some of our variables we get this block that will initialize a Rectangle to a unit square transformed by a matrix. + +```js +const m30 = m[(3 * 4) + 0]; +const m31 = m[(3 * 4) + 1]; + +const x = Math.abs(0.5 * m[(0 * 4) + 0]) + Math.abs(0.5 * m[(1 * 4) + 0]); +const y = Math.abs(0.5 * m[(0 * 4) + 1]) + Math.abs(0.5 * m[(1 * 4) + 1]); + +const left = -x + m30; +const right = x + m30; +const top = y + m31; +const bottom = -y + m31; +``` diff --git a/src/Rectangle.js b/src/Rectangle.js index 56204a060..6556cb1ba 100644 --- a/src/Rectangle.js +++ b/src/Rectangle.js @@ -55,98 +55,24 @@ class Rectangle { } /** - * Initialize a Rectangle to a 1 unit square transformed by a model matrix. + * Initialize a Rectangle to a 1 unit square centered at 0 x 0 transformed + * by a model matrix. * @param {Array.} m A 4x4 matrix to transform the rectangle by. + * @tutorial Rectangle-AABB-Matrix */ initFromModelMatrix (m) { - // Treat this function like we are transforming a vector with each - // component set to 0.5 by a matrix m. - // const v0 = 0.5; - // const v1 = 0.5; - // const v2 = 0.5; - - // Of the matrix to do this in 2D space, instead of the 3D provided by - // the matrix, we need the 2x2 "top left" that represents the scale and - // rotation ... - const m00 = m[(0 * 4) + 0]; - const m01 = m[(0 * 4) + 1]; - const m10 = m[(1 * 4) + 0]; - const m11 = m[(1 * 4) + 1]; - // ... and the 1x2 "top right" that represents position. + // In 2D space, we will soon use the 2x2 "top left" scale and rotation + // submatrix, while we store and the 1x2 "top right" that position + // vector. const m30 = m[(3 * 4) + 0]; const m31 = m[(3 * 4) + 1]; - // This is how we would normally transform the vector by the matrix. - // var determinant = v0 * m03 + v1 * m13 + v2 * m23 + m33; - // dst[0] = (v0 * m00 + v1 * m10 + v2 * m20 + m30) / determinant; - // dst[1] = (v0 * m01 + v1 * m11 + v2 * m21 + m31) / determinant; - // dst[2] = (v0 * m02 + v1 * m12 + v2 * m22 + m32) / determinant; - - // We can skip the v2 multiplications and the determinant. - - // Alternatively done with 4 vectors, those vectors would be reflected - // on the x and y axis. We can build those 4 vectors by transforming the - // parts of one vector and reflecting them on the axises after - // multiplication. - - // const x0 = 0.5 * m00; - // const x1 = 0.5 * m10; - // const y0 = 0.5 * m01; - // const y1 = 0.5 * m11; - - // const p0x = x0 + x1; - // const p0y = y0 + y1; - // const p1x = -x0 + x1; - // const p1y = -y0 + y1; - // const p2x = -x0 + -x1; - // const p2y = -y0 + -y1; - // const p3x = x0 + -x1; - // const p3y = y0 + -y1; - - // Since we want to reduce those 4 points to a min and max for each - // axis, we can use those multiplied components to build the min and max - // values without comparing the points. - - // We can start by getting the min and max for each of all the points. - // const left = Math.min(x0 + x1, -x0 + x1, -x0 + -x1, x0 + -x1); - // const right = Math.max(x0 + x1, -x0 + x1, -x0 + -x1, x0 + -x1); - // const top = Math.max(y0 + y1, -y0 + y1, -y0 + -y1, y0 + -y1); - // const bottom = Math.min(y0 + y1, -y0 + y1, -y0 + -y1, y0 + -y1); - - // Each of those can be replaced with min and max operations on the 0 - // and 1 matrix output components. - // const left = Math.min(x0, -x0) + Math.min(x1, -x1); - // const right = Math.max(x0, -x0) + Math.max(x1, -x1); - // const top = Math.max(y0, -y0) + Math.max(y1, -y1); - // const bottom = Math.min(y0, -y0) + Math.min(y1, -y1); - - // And they can be replaced with absolute values. - // const left = -Math.abs(x0) + -Math.abs(x1); - // const right = Math.abs(x0) + Math.abs(x1); - // const top = Math.abs(y0) + Math.abs(y1); - // const bottom = -Math.abs(y0) + -Math.abs(y1); - - // And those with positive and negative sums of the absolute values. - // const left = -(Math.abs(x0) + Math.abs(x1)); - // const right = +(Math.abs(x0) + Math.abs(x1)); - // const top = +(Math.abs(y0) + Math.abs(y1)); - // const bottom = -(Math.abs(y0) + -Math.abs(y1)); - - // We can perform those sums once and reuse them for the bounds. - // const x = Math.abs(x0) + Math.abs(x1); - // const y = Math.abs(y0) + Math.abs(y1); - // const left = -x; - // const right = x; - // const top = y; - // const bottom = -y; - - // Building those absolute sums for the 0.5 vector components by the - // matrix components ... - const x = Math.abs(0.5 * m00) + Math.abs(0.5 * m10); - const y = Math.abs(0.5 * m01) + Math.abs(0.5 * m11); + // "Transform" a (0.5, 0.5) vector by the scale and rotation matrix but + // sum the absolute of each component instead of use the signed values. + const x = Math.abs(0.5 * m[(0 * 4) + 0]) + Math.abs(0.5 * m[(1 * 4) + 0]); + const y = Math.abs(0.5 * m[(0 * 4) + 1]) + Math.abs(0.5 * m[(1 * 4) + 1]); - // And adding them to the position components in the matrices - // initializes our Rectangle. + // And adding them to the position components initializes our Rectangle. this.left = -x + m30; this.right = x + m30; this.top = y + m31;