Last active
May 9, 2019 09:11
-
-
Save yongjun21/8b3a052fbe4be04864a8e6614b86ac30 to your computer and use it in GitHub Desktop.
Helper `transform` object for chaining SVG transformations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const IDENTITY = { | |
a: 1, | |
b: 0, | |
c: 0, | |
d: 1, | |
e: 0, | |
f: 0 | |
} | |
const epsilon = 0.00001 | |
export default class TransformHelper { | |
constructor () { | |
this.params = Object.assign({}, IDENTITY) | |
this.apply = this.apply.bind(this) | |
this.unapply = this.unapply.bind(this) | |
} | |
applyOrigin (fn, x0, y0) { | |
this.params.e -= x0 | |
this.params.f -= y0 | |
fn.call(this) | |
this.params.e += x0 | |
this.params.f += y0 | |
return this | |
} | |
translate (dx = 0, dy = 0) { | |
this.params.e += dx | |
this.params.f += dy | |
return this | |
} | |
translateX (dx = 0) { | |
this.params.e += dx | |
return this | |
} | |
translateY (dy = 0) { | |
this.params.f += dy | |
return this | |
} | |
scale (sx, sy = [0, 0], origin = [0, 0]) { | |
if (sx == null) return this | |
if (typeof sy !== 'number') { | |
[sy, origin] = [sx, sy] | |
} | |
this.scaleX(sx, origin[0]) | |
this.scaleY(sy, origin[1]) | |
return this | |
} | |
scaleX (sx, x0 = 0) { | |
if (sx == null) return this | |
return this.applyOrigin(function () { | |
this.params.a *= sx | |
this.params.c *= sx | |
this.params.e *= sx | |
}, x0, 0) | |
} | |
scaleY (sy, y0 = 0) { | |
if (sy == null) return this | |
return this.applyOrigin(function () { | |
this.params.b *= sy | |
this.params.d *= sy | |
this.params.f *= sy | |
}, 0, y0) | |
} | |
flipX (x0 = 0) { | |
return this.scaleX(-1, x0) | |
} | |
flipY (y0 = 0) { | |
return this.scaleY(-1, y0) | |
} | |
rotate180 (origin = [0, 0]) { | |
this.flipX(origin[0]) | |
this.flipY(origin[1]) | |
return this | |
} | |
rotateRight (origin = [0, 0]) { | |
return this.applyOrigin(function () { | |
const {a, b, c, d, e, f} = this.params | |
this.params.a = -b | |
this.params.b = a | |
this.params.c = -d | |
this.params.d = c | |
this.params.e = -f | |
this.params.f = e | |
}, origin[0], origin[1]) | |
} | |
rotateLeft (origin = [0, 0]) { | |
return this.applyOrigin(function () { | |
const {a, b, c, d, e, f} = this.params | |
this.params.a = b | |
this.params.b = -a | |
this.params.c = d | |
this.params.d = -c | |
this.params.e = f | |
this.params.f = -e | |
}, origin[0], origin[1]) | |
} | |
invert (origin = [0, 0]) { | |
return this.applyOrigin(function () { | |
const {a, b, c, d, e, f} = this.params | |
this.params.a = b | |
this.params.b = a | |
this.params.c = d | |
this.params.d = c | |
this.params.e = f | |
this.params.f = e | |
}, origin[0], origin[1]) | |
} | |
rotate (A, x0 = 0, y0 = 0) { | |
if (A == null) return this | |
if (Array.isArray(x0)) { | |
[x0, y0] = x0 // allows origin to be passed in in the form of an array | |
} | |
const rad = A * Math.PI / 180 | |
const sinA = Math.sin(rad) | |
const cosA = Math.cos(rad) | |
return this.applyOrigin(function () { | |
const {a, b, c, d, e, f} = this.params | |
this.params.a = cosA * a - sinA * b | |
this.params.b = sinA * a + cosA * b | |
this.params.c = cosA * c - sinA * d | |
this.params.d = sinA * c + cosA * d | |
this.params.e = cosA * e - sinA * f | |
this.params.f = sinA * e + cosA * f | |
}, x0, y0) | |
} | |
skewX (A, x0 = 0) { | |
if (A == null) return this | |
const tanA = Math.tan(A * Math.PI / 180) | |
return this.applyOrigin(function () { | |
const {a, b, c, d, e, f} = this.params | |
this.params.a = a + tanA * b | |
this.params.c = c + tanA * d | |
this.params.e = e + tanA * f | |
}, x0, 0) | |
} | |
skewY (A, y0 = 0) { | |
if (A == null) return this | |
const tanA = Math.tan(A * Math.PI / 180) | |
return this.applyOrigin(function () { | |
const {a, b, c, d, e, f} = this.params | |
this.params.b = tanA * a + b | |
this.params.d = tanA * c + d | |
this.params.f = tanA * e + f | |
}, 0, y0) | |
} | |
matrix (A, B, C, D, E, F) { | |
const {a, b, c, d, e, f} = this.params | |
this.params.a = A * a + C * b | |
this.params.b = B * a + D * b | |
this.params.c = A * c + C * d | |
this.params.d = B * c + D * d | |
this.params.e = A * e + C * f + E | |
this.params.f = B * e + D * f + F | |
return this | |
} | |
clone () { | |
const t = new TransformHelper() | |
Object.assign(t.params, this.params) | |
return t | |
} | |
inverse () { | |
const t = new TransformHelper() | |
Object.assign(t.params, this.inverseParams) | |
return t | |
} | |
chain (transform) { | |
const t = this.clone() | |
const {a, b, c, d, e, f} = transform.params | |
return t.matrix(a, b, c, d, e, f) | |
} | |
textCorrection (origin = [0, 0]) { | |
const {a, b, c, d} = this.inverseParams | |
return new TransformHelper() | |
.translate(-origin[0], -origin[1]) | |
.matrix(a, b, c, d, 0, 0) | |
.translate(origin[0], origin[1]) | |
} | |
decompose () { | |
const {a, b, c, d, e: translateX, f: translateY} = this.params | |
let scaleX, skewX, scaleY, rotate | |
if (a === 0 && b === 0 && c === 0 && d === 0) { | |
scaleX = skewX = scaleY = rotate = 0 | |
} else if (c === 0 && d === 0) { | |
rotate = Math.atan2(b, a) * 180 / Math.PI | |
scaleX = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)) | |
skewX = scaleY = 0 | |
} else if (a === 0 && b === 0) { | |
rotate = -Math.atan2(c, d) * 180 / Math.PI | |
scaleY = Math.sqrt(Math.pow(c, 2) + Math.pow(d, 2)) | |
scaleX = skewX = 0 | |
} else { | |
const det = a * d - b * c | |
scaleX = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)) | |
rotate = Math.atan2(b, a) * 180 / Math.PI | |
if (a < 0 && det < 0) { | |
scaleX = -scaleX | |
rotate = rotate >= 0 ? rotate - 180 : rotate + 180 | |
} | |
skewX = Math.atan2(a * c + b * d, det) * 180 / Math.PI | |
scaleY = det / scaleX | |
} | |
if (rotate < -180 + epsilon) rotate = -rotate | |
return {scaleX, skewX, scaleY, rotate, translateX, translateY, toString: decomposedToString} | |
} | |
static recompose (decomposed) { | |
const {scaleX, skewX, scaleY, rotate, translateX, translateY} = decomposed | |
return new TransformHelper() | |
.scale(scaleX, scaleY) | |
.skewX(skewX) | |
.rotate(rotate) | |
.translate(translateX, translateY) | |
} | |
static solve (from, to) { | |
return from.inverse().chain(to) | |
} | |
static parse (str) { | |
const parsed = new TransformHelper() | |
if (!str) return parsed | |
const transformations = [] | |
const pattern = /(translate|scale|rotate|skewX|skewY|matrix)\(([\s\S]+?)\)/g | |
let match | |
while ((match = pattern.exec(str)) != null) { | |
transformations.push({ | |
type: match[1], | |
params: match[2].split(/[\s,]+/).map(Number) | |
}) | |
} | |
transformations.reverse().forEach(f => { | |
parsed[f.type].apply(parsed, f.params) | |
}) | |
return parsed | |
} | |
get inverseParams () { | |
const {a, b, c, d, e, f} = this.params | |
const disc = a * d - b * c | |
return { | |
a: d / disc, | |
b: -b / disc, | |
c: -c / disc, | |
d: a / disc, | |
e: (c * f - d * e) / disc, | |
f: (b * e - a * f) / disc | |
} | |
} | |
apply ([x, y]) { | |
const {a, b, c, d, e, f} = this.params | |
return [ | |
a * x + c * y + e, | |
b * x + d * y + f | |
] | |
} | |
unapply ([x, y]) { | |
const {a, b, c, d, e, f} = this.inverseParams | |
return [ | |
a * x + c * y + e, | |
b * x + d * y + f | |
] | |
} | |
isIdentity () { | |
const params = this.params | |
return Object.keys(params).every(key => params[key] === IDENTITY[key]) | |
} | |
isCloneOf (t) { | |
const params = this.params | |
return Object.keys(params).every(key => params[key] === t.params[key]) | |
} | |
isInverseOf (t) { | |
const params = this.inverseParams | |
return Object.keys(params).every(key => params[key] === t.params[key]) | |
} | |
toString (dp = 5) { | |
const {a, b, c, d, e, f} = this.params | |
return `matrix(${[a, b, c, d, e, f].map(round(5)).join(' ')})` | |
} | |
} | |
export function interpolateTransform (from, to) { | |
from = from instanceof TransformHelper ? from : TransformHelper.parse(from) | |
to = to instanceof TransformHelper ? to : TransformHelper.parse(to) | |
const fromParams = Object.assign(from.params) | |
const toParams = Object.assign(to.params) | |
return t => { | |
const interpolated = new TransformHelper() | |
Object.keys(toParams).forEach(key => { | |
interpolated.params[key] = (1 - t) * fromParams[key] + t * toParams[key] | |
}) | |
return interpolated | |
} | |
} | |
export function interpolateTransform2 (from, to) { | |
from = from instanceof TransformHelper ? from : TransformHelper.parse(from) | |
to = to instanceof TransformHelper ? to : TransformHelper.parse(to) | |
const {e, f} = from.params | |
from = from.clone().translate(-e, -f) | |
to = to.clone().translate(-e, -f) | |
const fromParams = { | |
scaleX: 1, | |
scaleY: 1, | |
skewX: 0, | |
rotate: 0, | |
translateX: 0, | |
translateY: 0 | |
} | |
const toParams = TransformHelper.solve(from, to).decompose() | |
return t => { | |
const interpolatedParams = {} | |
Object.keys(toParams).forEach(key => { | |
interpolatedParams[key] = (1 - t) * fromParams[key] + t * toParams[key] | |
}) | |
return from.chain(TransformHelper.recompose(interpolatedParams)).translate(e, f) | |
} | |
} | |
function round (dp) { | |
if (dp === 0) return v => v.toFixed(0) | |
return v => { | |
let n = 0 | |
let rounded = v.toFixed(dp).split('') | |
while (rounded[rounded.length - 1] === '0' && n++ < dp) { | |
rounded.pop() | |
} | |
if (n === dp) rounded.pop() | |
return rounded.join('') | |
} | |
} | |
function decomposedToString () { | |
const {scaleX, skewX, scaleY, rotate, translateX, translateY} = this | |
const transformations = [] | |
if (translateX !== 0 || translateY !== 0) { | |
transformations.push(`translate(${translateX} ${translateY})`) | |
} | |
if (rotate !== 0) { | |
transformations.push(`rotate${rotate}`) | |
} | |
if (skewX !== 0) { | |
transformations.push(`skewX(${skewX})`) | |
} | |
if (scaleX !== 0 || scaleY !== 0) { | |
transformations.push(`scale(${scaleX} ${scaleY})`) | |
} | |
return transformations.join(' ') | |
} | |
/* | |
const test = new Transform() | |
test.params = { | |
a: 1, | |
b: 2, | |
c: 3, | |
d: 4, | |
e: 5, | |
f: 6 | |
} | |
const original = [1, 1] | |
const applied = test.apply(original) | |
const reversed = test.unapply(applied) | |
console.log(original, applied, reversed) | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment