tapspace
Version:
A zoomable user interface lib for web apps
73 lines (71 loc) • 3.35 kB
JavaScript
// TODO use an affineplane method instead of this file.
const plane3 = require('affineplane').plane3
module.exports = (tran, anchor) => {
// Repair deformed offset caused by transform-origin CSS property.
// See lengthy notes below for details.
//
// Parameters:
// tran
// a plane3, the plane transition
// anchor
// a point2, the anchor point, the offset.
//
// Return
// a plane3, the corrected plane transform
//
// The CSS transform-origin property brings complications.
// We are forced to use the property for animated transitions
// to look good. This is because, when the transformation
// contains translation in addition to scale and rotation,
// the browsers interpolate the animation so that the origin
// translates along a straight line. The scale and rotation
// are interpolated around it.
// For example, if the transform-origin is at point 0,0
// then that corner travels the straight line, making
// the rotation look wobbly in respect to the rotation center.
// In tapspace, when we rotate a component about its middle,
// the transform contains lots of translation under the hood.
// Therefore the resulting animation wobbles regardless of
// the starting and ending placements are correct.
// To prevent the wobble, we must tell the browser which
// point of the element should move along the straight line
// of the translation. The transform-origin property is just
// for that purpose. If we set the origin at the component
// center then the rotation center point travels a straight line
// regardless the scaling and rotation. We use the component
// anchor point for that purpose.
// The main downside from using the transform origin is
// that it modifies the effective transformation. The error
// becomes very apparent when the transformation contains
// rotation. Therefore we must undo the error. The math
// follows:
// Let TSR be our transformation matrix with translation
// scaling and rotation. It works correctly when transform
// origin is at 0,0. Let F_zero denote this.
// F_zero(TSR) = TSR
// The MDN documentation on the transform-origin tells
// how it modifies the transformation. Let F_orig be
// the effective transformation with origin at {ox,oy}.
// Let G be translation from zero to {ox,oy} and
// iG its inversion, e.g. from {ox,oy} to zero.
// According to MDN, the F_orig is computed as:
// F_orig(TSR) = G * TSR * iG
// We can see that when origin is at (0,0), then G = I
// and F_orig = F_zero. Now let us find adjusted TSR,
// A_tsr, so that with it F_orig equals F_zero.
// F_orig(A_tsr) = G * A_tsr * iG = F_zero(TSR)
// <=> A_tsr = iG * TSR * G
// In other words, the adjusted transformation inverts
// the effects of transform-origin. The magic in this is
// that while the error to the placement is countered,
// the animations now work as intended because transform
// origin is used.
// For more, see paper note 2022-06-21-02.
const ax = anchor.x
const ay = anchor.y
// Countering translations
const g = { a: 1, b: 0, x: ax, y: ay, z: 0 }
const ig = { a: 1, b: 0, x: -ax, y: -ay, z: 0 }
// Adjusted projection. TODO OPTIMIZE avoid function calls.
return plane3.compose(ig, plane3.compose(tran, g))
}