@js-draw/math
Advanced tools
Comparing version 1.3.0 to 1.3.1
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
464714
130
12591