@js-draw/math
Advanced tools
Comparing version
@@ -110,5 +110,18 @@ import { Point2, Vec2 } from './Vec2'; | ||
getScaleFactor(): number; | ||
/** Returns the `idx`-th column (`idx` is 0-indexed). */ | ||
getColumn(idx: number): Vec3; | ||
/** Returns the magnitude of the entry with the largest entry */ | ||
maximumEntryMagnitude(): number; | ||
/** | ||
* Constructs a 3x3 translation matrix (for translating `Vec2`s) using | ||
* **transformVec2**. | ||
* | ||
* Creates a matrix in the form | ||
* $$ | ||
* \begin{pmatrix} | ||
* 1 & 0 & {\tt amount.x}\\ | ||
* 0 & 1 & {\tt amount.y}\\ | ||
* 0 & 0 & 1 | ||
* \end{pmatrix} | ||
* $$ | ||
*/ | ||
@@ -118,5 +131,22 @@ static translation(amount: Vec2): Mat33; | ||
static scaling2D(amount: number | Vec2, center?: Point2): Mat33; | ||
/** @see {@link fromCSSMatrix} */ | ||
/** | ||
* **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`. | ||
* | ||
* @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList} | ||
*/ | ||
toCSSMatrix(): string; | ||
/** | ||
* @beta May change or even be removed between minor releases. | ||
* | ||
* Converts this matrix into a list of CSS transforms that attempt to preserve | ||
* this matrix's translation. | ||
* | ||
* In Chrome/Firefox, translation attributes only support 6 digits (likely an artifact | ||
* of using lower-precision floating point numbers). This works around | ||
* that by expanding this matrix into the product of several CSS transforms. | ||
* | ||
* **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`. | ||
*/ | ||
toSafeCSSTransformList(): string; | ||
/** | ||
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. | ||
@@ -123,0 +153,0 @@ * |
@@ -9,2 +9,3 @@ "use strict"; | ||
const Vec3_1 = __importDefault(require("./Vec3")); | ||
const rounding_1 = require("./rounding"); | ||
/** | ||
@@ -263,5 +264,26 @@ * Represents a three dimensional linear transformation or | ||
} | ||
/** Returns the `idx`-th column (`idx` is 0-indexed). */ | ||
getColumn(idx) { | ||
return Vec3_1.default.of(this.rows[0].at(idx), this.rows[1].at(idx), this.rows[2].at(idx)); | ||
} | ||
/** Returns the magnitude of the entry with the largest entry */ | ||
maximumEntryMagnitude() { | ||
let greatestSoFar = Math.abs(this.a1); | ||
for (const entry of this.toArray()) { | ||
greatestSoFar = Math.max(greatestSoFar, Math.abs(entry)); | ||
} | ||
return greatestSoFar; | ||
} | ||
/** | ||
* Constructs a 3x3 translation matrix (for translating `Vec2`s) using | ||
* **transformVec2**. | ||
* | ||
* Creates a matrix in the form | ||
* $$ | ||
* \begin{pmatrix} | ||
* 1 & 0 & {\tt amount.x}\\ | ||
* 0 & 1 & {\tt amount.y}\\ | ||
* 0 & 0 & 1 | ||
* \end{pmatrix} | ||
* $$ | ||
*/ | ||
@@ -301,3 +323,7 @@ static translation(amount) { | ||
} | ||
/** @see {@link fromCSSMatrix} */ | ||
/** | ||
* **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`. | ||
* | ||
* @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList} | ||
*/ | ||
toCSSMatrix() { | ||
@@ -307,2 +333,119 @@ return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`; | ||
/** | ||
* @beta May change or even be removed between minor releases. | ||
* | ||
* Converts this matrix into a list of CSS transforms that attempt to preserve | ||
* this matrix's translation. | ||
* | ||
* In Chrome/Firefox, translation attributes only support 6 digits (likely an artifact | ||
* of using lower-precision floating point numbers). This works around | ||
* that by expanding this matrix into the product of several CSS transforms. | ||
* | ||
* **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`. | ||
*/ | ||
toSafeCSSTransformList() { | ||
// Check whether it's safe to return just the CSS matrix | ||
const translation = Vec2_1.Vec2.of(this.a3, this.b3); | ||
const translationRoundedX = (0, rounding_1.toRoundedString)(translation.x); | ||
const translationRoundedY = (0, rounding_1.toRoundedString)(translation.y); | ||
const nonDigitsRegex = /[^0-9]+/g; | ||
const translationXDigits = translationRoundedX.replace(nonDigitsRegex, '').length; | ||
const translationYDigits = translationRoundedY.replace(nonDigitsRegex, '').length; | ||
// Is it safe to just return the default CSS matrix? | ||
if (translationXDigits <= 5 && translationYDigits <= 5) { | ||
return this.toCSSMatrix(); | ||
} | ||
// Remove the last column (the translation column) | ||
let transform = new Mat33(this.a1, this.a2, 0, this.b1, this.b2, 0, 0, 0, 1); | ||
const transforms = []; | ||
let lastScale = null; | ||
// Appends a translate() command to the list of `transforms`. | ||
const addTranslate = (translation) => { | ||
lastScale = null; | ||
if (!translation.eq(Vec2_1.Vec2.zero)) { | ||
transforms.push(`translate(${(0, rounding_1.toRoundedString)(translation.x)}px, ${(0, rounding_1.toRoundedString)(translation.y)}px)`); | ||
} | ||
}; | ||
// Appends a scale() command to the list of transforms, possibly merging with | ||
// the last command, if a scale(). | ||
const addScale = (scale) => { | ||
// Merge with the last scale | ||
if (lastScale) { | ||
const newScale = lastScale.scale(scale); | ||
// Don't merge if the new scale has very large values | ||
if (newScale.maximumEntryMagnitude() < 1e7) { | ||
const previousCommand = transforms.pop(); | ||
console.assert(previousCommand.startsWith('scale'), 'Invalid state: Merging scale commands'); | ||
scale = newScale; | ||
} | ||
} | ||
if (scale.x === scale.y) { | ||
transforms.push(`scale(${(0, rounding_1.toRoundedString)(scale.x)})`); | ||
} | ||
else { | ||
transforms.push(`scale(${(0, rounding_1.toRoundedString)(scale.x)}, ${(0, rounding_1.toRoundedString)(scale.y)})`); | ||
} | ||
lastScale = scale; | ||
}; | ||
// Returns the number of digits before the `.` in the given number string. | ||
const digitsPreDecimalCount = (numberString) => { | ||
let decimalIndex = numberString.indexOf('.'); | ||
if (decimalIndex === -1) { | ||
decimalIndex = numberString.length; | ||
} | ||
return numberString.substring(0, decimalIndex).replace(nonDigitsRegex, '').length; | ||
}; | ||
// Returns the number of digits (positive for left shift, negative for right shift) | ||
// required to shift the decimal to the middle of the number. | ||
const getShift = (numberString) => { | ||
const preDecimal = digitsPreDecimalCount(numberString); | ||
const postDecimal = (numberString.match(/[.](\d*)/) ?? ['', ''])[1].length; | ||
// The shift required to center the decimal point. | ||
const toCenter = postDecimal - preDecimal; | ||
// toCenter is positive for a left shift (adding more pre-decimals), | ||
// so, after applying it, | ||
const postShiftPreDecimal = preDecimal + toCenter; | ||
// We want the digits before the decimal to have a length at most 4, however. | ||
// Thus, right shift until this is the case. | ||
const shiftForAtMost5DigitsPreDecimal = 4 - Math.max(postShiftPreDecimal, 4); | ||
return toCenter + shiftForAtMost5DigitsPreDecimal; | ||
}; | ||
const addShiftedTranslate = (translate, depth = 0) => { | ||
const xString = (0, rounding_1.toRoundedString)(translate.x); | ||
const yString = (0, rounding_1.toRoundedString)(translate.y); | ||
const xShiftDigits = getShift(xString); | ||
const yShiftDigits = getShift(yString); | ||
const shift = Vec2_1.Vec2.of(Math.pow(10, xShiftDigits), Math.pow(10, yShiftDigits)); | ||
const invShift = Vec2_1.Vec2.of(Math.pow(10, -xShiftDigits), Math.pow(10, -yShiftDigits)); | ||
addScale(invShift); | ||
const shiftedTranslate = translate.scale(shift); | ||
const roundedShiftedTranslate = Vec2_1.Vec2.of(Math.floor(shiftedTranslate.x), Math.floor(shiftedTranslate.y)); | ||
addTranslate(roundedShiftedTranslate); | ||
// Don't recurse more than 3 times -- the more times we recurse, the more | ||
// the scaling is influenced by error. | ||
if (!roundedShiftedTranslate.eq(shiftedTranslate) && depth < 3) { | ||
addShiftedTranslate(shiftedTranslate.minus(roundedShiftedTranslate), depth + 1); | ||
} | ||
addScale(shift); | ||
return translate; | ||
}; | ||
const adjustTransformFromScale = () => { | ||
if (lastScale) { | ||
const scaledTransform = transform.rightMul(Mat33.scaling2D(lastScale)); | ||
// If adding the scale to the transform leads to large values, avoid | ||
// doing this. | ||
if (scaledTransform.maximumEntryMagnitude() < 1e12) { | ||
transforms.pop(); | ||
transform = transform.rightMul(Mat33.scaling2D(lastScale)); | ||
lastScale = null; | ||
} | ||
} | ||
}; | ||
addShiftedTranslate(translation); | ||
adjustTransformFromScale(); | ||
if (!transform.eq(Mat33.identity)) { | ||
transforms.push(transform.toCSSMatrix()); | ||
} | ||
return transforms.join(' '); | ||
} | ||
/** | ||
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. | ||
@@ -321,26 +464,91 @@ * | ||
} | ||
const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)'; | ||
const numberSepExp = '[, \\t\\n]'; | ||
const regExpSource = `^\\s*matrix\\s*\\(${[ | ||
// According to MDN, matrix(a,b,c,d,e,f) has form: | ||
// ⎡ a c e ⎤ | ||
// ⎢ b d f ⎥ | ||
// ⎣ 0 0 1 ⎦ | ||
numberExp, numberExp, numberExp, | ||
numberExp, numberExp, numberExp, // b, d, f | ||
].join(`${numberSepExp}+`)}${numberSepExp}*\\)\\s*$`; | ||
const matrixExp = new RegExp(regExpSource, 'i'); | ||
const match = matrixExp.exec(cssString); | ||
if (!match) { | ||
throw new Error(`Unsupported transformation: ${cssString}`); | ||
const parseArguments = (argumentString) => { | ||
return argumentString.split(/[, \t\n]+/g).map(argString => { | ||
let isPercentage = false; | ||
if (argString.endsWith('%')) { | ||
isPercentage = true; | ||
argString = argString.substring(0, argString.length - 1); | ||
} | ||
// Remove trailing px units. | ||
argString = argString.replace(/px$/ig, ''); | ||
const numberExp = /^[-]?\d*(?:\.\d*)?(?:[eE][-+]?\d+)?$/i; | ||
if (!numberExp.exec(argString)) { | ||
throw new Error(`All arguments to transform functions must be numeric (state: ${JSON.stringify({ | ||
currentArgument: argString, | ||
allArguments: argumentString, | ||
})})`); | ||
} | ||
let argNumber = parseFloat(argString); | ||
if (isPercentage) { | ||
argNumber /= 100; | ||
} | ||
return argNumber; | ||
}); | ||
}; | ||
const keywordToAction = { | ||
matrix: (matrixData) => { | ||
if (matrixData.length !== 6) { | ||
throw new Error(`Invalid matrix argument: ${matrixData}. Must have length 6`); | ||
} | ||
const a = matrixData[0]; | ||
const b = matrixData[1]; | ||
const c = matrixData[2]; | ||
const d = matrixData[3]; | ||
const e = matrixData[4]; | ||
const f = matrixData[5]; | ||
const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1); | ||
return transform; | ||
}, | ||
scale: (scaleArgs) => { | ||
let scaleX, scaleY; | ||
if (scaleArgs.length === 1) { | ||
scaleX = scaleArgs[0]; | ||
scaleY = scaleArgs[0]; | ||
} | ||
else if (scaleArgs.length === 2) { | ||
scaleX = scaleArgs[0]; | ||
scaleY = scaleArgs[1]; | ||
} | ||
else { | ||
throw new Error(`The scale() function only supports two arguments. Given: ${scaleArgs}`); | ||
} | ||
return Mat33.scaling2D(Vec2_1.Vec2.of(scaleX, scaleY)); | ||
}, | ||
translate: (translateArgs) => { | ||
let translateX = 0; | ||
let translateY = 0; | ||
if (translateArgs.length === 1) { | ||
// If no y translation is given, assume 0. | ||
translateX = translateArgs[0]; | ||
} | ||
else if (translateArgs.length === 2) { | ||
translateX = translateArgs[0]; | ||
translateY = translateArgs[1]; | ||
} | ||
else { | ||
throw new Error(`The translate() function requires either 1 or 2 arguments. Given ${translateArgs}`); | ||
} | ||
return Mat33.translation(Vec2_1.Vec2.of(translateX, translateY)); | ||
}, | ||
}; | ||
// A command (\w+) | ||
// followed by a set of arguments ([ \t\n0-9eE.,\-%]+) | ||
const partRegex = /\s*(\w+)\s*\(([^)]*)\)/ig; | ||
let match; | ||
let matrix = null; | ||
while ((match = partRegex.exec(cssString)) !== null) { | ||
const action = match[1].toLowerCase(); | ||
if (!(action in keywordToAction)) { | ||
throw new Error(`Unsupported CSS transform action: ${action}`); | ||
} | ||
const args = parseArguments(match[2]); | ||
const currentMatrix = keywordToAction[action](args); | ||
if (!matrix) { | ||
matrix = currentMatrix; | ||
} | ||
else { | ||
matrix = matrix.rightMul(currentMatrix); | ||
} | ||
} | ||
const matrixData = match.slice(1).map(entry => parseFloat(entry)); | ||
const a = matrixData[0]; | ||
const b = matrixData[1]; | ||
const c = matrixData[2]; | ||
const d = matrixData[3]; | ||
const e = matrixData[4]; | ||
const f = matrixData[5]; | ||
const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1); | ||
return transform; | ||
return matrix ?? Mat33.identity; | ||
} | ||
@@ -347,0 +555,0 @@ } |
@@ -39,2 +39,9 @@ /** | ||
/** | ||
* Returns the entry of this with the greatest magnitude. | ||
* | ||
* In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of | ||
* all entries of this vector. | ||
*/ | ||
maximumEntryMagnitude(): number; | ||
/** | ||
* Return this' angle in the XY plane (treats this as a Vec2). | ||
@@ -41,0 +48,0 @@ * |
@@ -62,2 +62,11 @@ "use strict"; | ||
/** | ||
* Returns the entry of this with the greatest magnitude. | ||
* | ||
* In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of | ||
* all entries of this vector. | ||
*/ | ||
maximumEntryMagnitude() { | ||
return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z))); | ||
} | ||
/** | ||
* Return this' angle in the XY plane (treats this as a Vec2). | ||
@@ -64,0 +73,0 @@ * |
@@ -110,5 +110,18 @@ import { Point2, Vec2 } from './Vec2'; | ||
getScaleFactor(): number; | ||
/** Returns the `idx`-th column (`idx` is 0-indexed). */ | ||
getColumn(idx: number): Vec3; | ||
/** Returns the magnitude of the entry with the largest entry */ | ||
maximumEntryMagnitude(): number; | ||
/** | ||
* Constructs a 3x3 translation matrix (for translating `Vec2`s) using | ||
* **transformVec2**. | ||
* | ||
* Creates a matrix in the form | ||
* $$ | ||
* \begin{pmatrix} | ||
* 1 & 0 & {\tt amount.x}\\ | ||
* 0 & 1 & {\tt amount.y}\\ | ||
* 0 & 0 & 1 | ||
* \end{pmatrix} | ||
* $$ | ||
*/ | ||
@@ -118,5 +131,22 @@ static translation(amount: Vec2): Mat33; | ||
static scaling2D(amount: number | Vec2, center?: Point2): Mat33; | ||
/** @see {@link fromCSSMatrix} */ | ||
/** | ||
* **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`. | ||
* | ||
* @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList} | ||
*/ | ||
toCSSMatrix(): string; | ||
/** | ||
* @beta May change or even be removed between minor releases. | ||
* | ||
* Converts this matrix into a list of CSS transforms that attempt to preserve | ||
* this matrix's translation. | ||
* | ||
* In Chrome/Firefox, translation attributes only support 6 digits (likely an artifact | ||
* of using lower-precision floating point numbers). This works around | ||
* that by expanding this matrix into the product of several CSS transforms. | ||
* | ||
* **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`. | ||
*/ | ||
toSafeCSSTransformList(): string; | ||
/** | ||
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. | ||
@@ -123,0 +153,0 @@ * |
@@ -39,2 +39,9 @@ /** | ||
/** | ||
* Returns the entry of this with the greatest magnitude. | ||
* | ||
* In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of | ||
* all entries of this vector. | ||
*/ | ||
maximumEntryMagnitude(): number; | ||
/** | ||
* Return this' angle in the XY plane (treats this as a Vec2). | ||
@@ -41,0 +48,0 @@ * |
{ | ||
"name": "@js-draw/math", | ||
"version": "1.3.0", | ||
"version": "1.3.1", | ||
"description": "A math library for js-draw. ", | ||
@@ -25,3 +25,3 @@ "types": "./dist/mjs/lib.d.ts", | ||
"build": "rm -rf ./dist && mkdir dist && build-tool build", | ||
"watch": "rm -rf ./dist && mkdir dist && build-tool watch" | ||
"watch": "rm -rf ./dist/* && mkdir -p dist && build-tool watch" | ||
}, | ||
@@ -49,3 +49,3 @@ "dependencies": { | ||
], | ||
"gitHead": "46b3d8f819f8e083f6e3e1d01e027e4311355456" | ||
"gitHead": "65af7ec944f70b69b2a4b07d98e5bb92eeeca029" | ||
} |
@@ -201,45 +201,9 @@ import Mat33 from './Mat33'; | ||
it('should convert CSS matrix(...) strings to matricies', () => { | ||
// From MDN: | ||
// ⎡ a c e ⎤ | ||
// ⎢ b d f ⎥ = matrix(a,b,c,d,e,f) | ||
// ⎣ 0 0 1 ⎦ | ||
const identity = Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0)'); | ||
expect(identity).objEq(Mat33.identity); | ||
expect(Mat33.fromCSSMatrix('matrix(1, 2, 3, 4, 5, 6)')).objEq(new Mat33( | ||
1, 3, 5, | ||
2, 4, 6, | ||
0, 0, 1, | ||
)); | ||
expect(Mat33.fromCSSMatrix('matrix(1e2, 2, 3, 4, 5, 6)')).objEq(new Mat33( | ||
1e2, 3, 5, | ||
2, 4, 6, | ||
0, 0, 1, | ||
)); | ||
expect(Mat33.fromCSSMatrix('matrix(1.6, 2, .3, 4, 5, 6)')).objEq(new Mat33( | ||
1.6, .3, 5, | ||
2, 4, 6, | ||
0, 0, 1, | ||
)); | ||
expect(Mat33.fromCSSMatrix('matrix(-1, 2, 3.E-2, 4, -5.123, -6.5)')).objEq(new Mat33( | ||
-1, 0.03, -5.123, | ||
2, 4, -6.5, | ||
0, 0, 1, | ||
)); | ||
expect(Mat33.fromCSSMatrix('matrix(1.6,\n\t2, .3, 4, 5, 6)')).objEq(new Mat33( | ||
1.6, .3, 5, | ||
2, 4, 6, | ||
0, 0, 1, | ||
)); | ||
expect(Mat33.fromCSSMatrix('matrix(1.6,2, .3E-2, 4, 5, 6)')).objEq(new Mat33( | ||
1.6, 3e-3, 5, | ||
2, 4, 6, | ||
0, 0, 1, | ||
)); | ||
expect(Mat33.fromCSSMatrix('matrix(-1, 2e6, 3E-2,-5.123, -6.5e-1, 0.01)')).objEq(new Mat33( | ||
-1, 3E-2, -6.5e-1, | ||
2e6, -5.123, 0.01, | ||
0, 0, 1, | ||
)); | ||
it('getColumn should return the given column index', () => { | ||
expect(Mat33.identity.getColumn(0)).objEq(Vec3.unitX); | ||
expect(Mat33.identity.getColumn(1)).objEq(Vec3.of(0, 1, 0)); | ||
// scaling2D only scales the x/y components of vectors it transforms | ||
expect(Mat33.scaling2D(2).getColumn(2)).objEq(Vec3.of(0, 0, 1)); | ||
}); | ||
}); |
173
src/Mat33.ts
@@ -338,5 +338,33 @@ import { Point2, Vec2 } from './Vec2'; | ||
/** Returns the `idx`-th column (`idx` is 0-indexed). */ | ||
public getColumn(idx: number) { | ||
return Vec3.of( | ||
this.rows[0].at(idx), | ||
this.rows[1].at(idx), | ||
this.rows[2].at(idx), | ||
); | ||
} | ||
/** Returns the magnitude of the entry with the largest entry */ | ||
public maximumEntryMagnitude() { | ||
let greatestSoFar = Math.abs(this.a1); | ||
for (const entry of this.toArray()) { | ||
greatestSoFar = Math.max(greatestSoFar, Math.abs(entry)); | ||
} | ||
return greatestSoFar; | ||
} | ||
/** | ||
* Constructs a 3x3 translation matrix (for translating `Vec2`s) using | ||
* **transformVec2**. | ||
* | ||
* Creates a matrix in the form | ||
* $$ | ||
* \begin{pmatrix} | ||
* 1 & 0 & {\tt amount.x}\\ | ||
* 0 & 1 & {\tt amount.y}\\ | ||
* 0 & 0 & 1 | ||
* \end{pmatrix} | ||
* $$ | ||
*/ | ||
@@ -396,3 +424,7 @@ public static translation(amount: Vec2): Mat33 { | ||
/** @see {@link fromCSSMatrix} */ | ||
/** | ||
* **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`. | ||
* | ||
* @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList} | ||
*/ | ||
public toCSSMatrix(): string { | ||
@@ -417,37 +449,116 @@ return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`; | ||
const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)'; | ||
const numberSepExp = '[, \\t\\n]'; | ||
const regExpSource = `^\\s*matrix\\s*\\(${ | ||
[ | ||
// According to MDN, matrix(a,b,c,d,e,f) has form: | ||
// ⎡ a c e ⎤ | ||
// ⎢ b d f ⎥ | ||
// ⎣ 0 0 1 ⎦ | ||
numberExp, numberExp, numberExp, // a, c, e | ||
numberExp, numberExp, numberExp, // b, d, f | ||
].join(`${numberSepExp}+`) | ||
}${numberSepExp}*\\)\\s*$`; | ||
const matrixExp = new RegExp(regExpSource, 'i'); | ||
const match = matrixExp.exec(cssString); | ||
const parseArguments = (argumentString: string) => { | ||
return argumentString.split(/[, \t\n]+/g).map(argString => { | ||
let isPercentage = false; | ||
if (argString.endsWith('%')) { | ||
isPercentage = true; | ||
argString = argString.substring(0, argString.length - 1); | ||
} | ||
if (!match) { | ||
throw new Error(`Unsupported transformation: ${cssString}`); | ||
// Remove trailing px units. | ||
argString = argString.replace(/px$/ig, ''); | ||
const numberExp = /^[-]?\d*(?:\.\d*)?(?:[eE][-+]?\d+)?$/i; | ||
if (!numberExp.exec(argString)) { | ||
throw new Error( | ||
`All arguments to transform functions must be numeric (state: ${ | ||
JSON.stringify({ | ||
currentArgument: argString, | ||
allArguments: argumentString, | ||
}) | ||
})` | ||
); | ||
} | ||
let argNumber = parseFloat(argString); | ||
if (isPercentage) { | ||
argNumber /= 100; | ||
} | ||
return argNumber; | ||
}); | ||
}; | ||
const keywordToAction = { | ||
matrix: (matrixData: number[]) => { | ||
if (matrixData.length !== 6) { | ||
throw new Error(`Invalid matrix argument: ${matrixData}. Must have length 6`); | ||
} | ||
const a = matrixData[0]; | ||
const b = matrixData[1]; | ||
const c = matrixData[2]; | ||
const d = matrixData[3]; | ||
const e = matrixData[4]; | ||
const f = matrixData[5]; | ||
const transform = new Mat33( | ||
a, c, e, | ||
b, d, f, | ||
0, 0, 1 | ||
); | ||
return transform; | ||
}, | ||
scale: (scaleArgs: number[]) => { | ||
let scaleX, scaleY; | ||
if (scaleArgs.length === 1) { | ||
scaleX = scaleArgs[0]; | ||
scaleY = scaleArgs[0]; | ||
} else if (scaleArgs.length === 2) { | ||
scaleX = scaleArgs[0]; | ||
scaleY = scaleArgs[1]; | ||
} else { | ||
throw new Error(`The scale() function only supports two arguments. Given: ${scaleArgs}`); | ||
} | ||
return Mat33.scaling2D(Vec2.of(scaleX, scaleY)); | ||
}, | ||
translate: (translateArgs: number[]) => { | ||
let translateX = 0; | ||
let translateY = 0; | ||
if (translateArgs.length === 1) { | ||
// If no y translation is given, assume 0. | ||
translateX = translateArgs[0]; | ||
} else if (translateArgs.length === 2) { | ||
translateX = translateArgs[0]; | ||
translateY = translateArgs[1]; | ||
} else { | ||
throw new Error(`The translate() function requires either 1 or 2 arguments. Given ${translateArgs}`); | ||
} | ||
return Mat33.translation(Vec2.of(translateX, translateY)); | ||
}, | ||
}; | ||
// A command (\w+) | ||
// followed by a set of arguments ([ \t\n0-9eE.,\-%]+) | ||
const partRegex = /\s*(\w+)\s*\(([^)]*)\)/ig; | ||
let match; | ||
let matrix: Mat33|null = null; | ||
while ((match = partRegex.exec(cssString)) !== null) { | ||
const action = match[1].toLowerCase(); | ||
if (!(action in keywordToAction)) { | ||
throw new Error(`Unsupported CSS transform action: ${action}`); | ||
} | ||
const args = parseArguments(match[2]); | ||
const currentMatrix = keywordToAction[action as keyof typeof keywordToAction](args); | ||
if (!matrix) { | ||
matrix = currentMatrix; | ||
} else { | ||
matrix = matrix.rightMul(currentMatrix); | ||
} | ||
} | ||
const matrixData = match.slice(1).map(entry => parseFloat(entry)); | ||
const a = matrixData[0]; | ||
const b = matrixData[1]; | ||
const c = matrixData[2]; | ||
const d = matrixData[3]; | ||
const e = matrixData[4]; | ||
const f = matrixData[5]; | ||
const transform = new Mat33( | ||
a, c, e, | ||
b, d, f, | ||
0, 0, 1 | ||
); | ||
return transform; | ||
return matrix ?? Mat33.identity; | ||
} | ||
} | ||
export default Mat33; |
@@ -168,1 +168,2 @@ // @packageDocumentation @internal | ||
}; | ||
@@ -67,2 +67,12 @@ | ||
/** | ||
* Returns the entry of this with the greatest magnitude. | ||
* | ||
* In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of | ||
* all entries of this vector. | ||
*/ | ||
public maximumEntryMagnitude(): number { | ||
return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z))); | ||
} | ||
/** | ||
* Return this' angle in the XY plane (treats this as a Vec2). | ||
@@ -69,0 +79,0 @@ * |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
464714
6.44%130
0.78%12591
5.47%