From 8fa4e0c6723f112a577f4da84122a1a22ac826a1 Mon Sep 17 00:00:00 2001 From: Jamie Wong Date: Sat, 25 Nov 2017 01:50:48 -0800 Subject: [PATCH] Zoom around cursor --- flamechart.tsx | 48 ++++++++++++++++++++------------ math.ts | 74 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 102 insertions(+), 20 deletions(-) diff --git a/flamechart.tsx b/flamechart.tsx index cfa113c..3ca6e56 100644 --- a/flamechart.tsx +++ b/flamechart.tsx @@ -118,8 +118,6 @@ export class FlamechartView extends Component { } private worldSpaceToViewSpace() { - const viewportRect = this.worldSpaceViewportRect - return AffineTransform.betweenRects( this.worldSpaceViewportRect, new Rect(new Vec2(0, 0), this.viewportSize()) @@ -149,38 +147,52 @@ export class FlamechartView extends Component { } } - private pan(deltaX: number, deltaY: number) { - const worldSpaceDelta = new Vec2( - deltaX / this.worldSpaceToViewSpace().getScale().x, - deltaY / this.worldSpaceToViewSpace().getScale().y - ) - const newOrigin = this.worldSpaceViewportRect.origin.plus(worldSpaceDelta) + private pan(viewSpaceDelta: Vec2) { + const worldSpaceDelta = this.worldSpaceToViewSpace().inverseTransformVector(viewSpaceDelta) + if (!worldSpaceDelta) return + + // TODO(jlfwong): Add back clamping of panning bounds + /* const worldSpacePanningBounds = new Rect( new Vec2(0, 0), this.configSpaceToWorldSpace().transformVector(this.configSpaceSize()).minus(this.worldSpaceViewportRect.size) ) + */ - this.worldSpaceViewportRect = this.worldSpaceViewportRect - .withOrigin(worldSpacePanningBounds.closestPointTo(newOrigin)) + this.worldSpaceViewportRect = AffineTransform.withTranslation(worldSpaceDelta) + .transformRect(this.worldSpaceViewportRect) } - private zoom(deltaY: number) { - const viewportRect = this.worldSpaceViewportRect + private zoom(worldSpaceZoomCenter: Vec2, multiplier: number) { + /* + multiplier = clamp(multiplier, + 5 / this.worldSpaceViewportRect.width(), + this.viewportWidth() / this.worldSpaceViewportRect.width() + ) + */ - const multiplier = 1 + 0.001 * deltaY - const newWidth = viewportRect.size.x * multiplier + const zoomTransform = AffineTransform + .withTranslation(worldSpaceZoomCenter.times(-1)) + .scaledBy(new Vec2(multiplier, 1)) + .translatedBy(worldSpaceZoomCenter) - this.worldSpaceViewportRect = this.worldSpaceViewportRect - .withSize(viewportRect.size.withX(clamp(newWidth, 5, this.viewportWidth()))) + + this.worldSpaceViewportRect = zoomTransform.transformRect(this.worldSpaceViewportRect) } private onWheel = (ev: WheelEvent) => { ev.preventDefault() if (ev.metaKey) { - this.zoom(ev.deltaY) + const viewSpaceZoomCenter = new Vec2(ev.offsetX, ev.offsetY) + const worldSpaceZoomCenter = this.worldSpaceToViewSpace().inverseTransformPosition(viewSpaceZoomCenter) + + if (worldSpaceZoomCenter) { + const multiplier = 1 + (ev.deltaY / 100) + this.zoom(worldSpaceZoomCenter, multiplier) + } } else { - this.pan(ev.deltaX, ev.deltaY) + this.pan(new Vec2(ev.deltaX, ev.deltaY)) } this.renderGL() diff --git a/math.ts b/math.ts index 677e5de..bf57dc8 100644 --- a/math.ts +++ b/math.ts @@ -46,6 +46,7 @@ export class AffineTransform { static withScale(s: Vec2) { return (new AffineTransform).withScale(s) } + scaledBy(s: Vec2) { return AffineTransform.withScale(s).times(this) } getScale() { return new Vec2(this.m00, this.m11) } withTranslation(t: Vec2) { @@ -61,14 +62,16 @@ export class AffineTransform { return (new AffineTransform).withTranslation(t) } getTranslation() { return new Vec2(this.m02, this.m12) } + translatedBy(t: Vec2) { return AffineTransform.withTranslation(t).times(this) } static betweenRects(from: Rect, to: Rect) { return AffineTransform - .withTranslation(to.origin.minus(from.origin)) - .withScale(new Vec2( + .withTranslation(from.origin.times(-1)) + .scaledBy(new Vec2( to.size.x / from.size.x, to.size.y / from.size.y )) + .translatedBy(to.origin) } times(other: AffineTransform) { @@ -82,6 +85,54 @@ export class AffineTransform { return new AffineTransform(m00, m01, m02, m10, m11, m12) } + timesScalar(s: number) { + const {m00, m01, m02, m10, m11, m12} = this + return new AffineTransform(s*m00, s*m01, s*m02, s*m10, s*m11, s*m12) + } + + det() { + const {m00, m01, m02, m10, m11, m12} = this + const m20 = 0 + const m21 = 0 + const m22 = 1 + + return ( + m00 * (m11 * m22 - m12 * m21) - + m01 * (m10 * m22 - m12 * m20) + + m02 * (m10 * m21 - m11 * m20) + ) + } + + adj() { + const {m00, m01, m02, m10, m11, m12} = this + const m20 = 0 + const m21 = 0 + const m22 = 1 + + // Adjugate matrix (a) is the transpose of the + // cofactor matrix (c). + // + // 00 01 02 + // 10 11 12 + // 20 21 22 + + const a00 = /* c00 = */ +(m11 * m22 - m12 * m21) + const a01 = /* c10 = */ -(m01 * m22 - m02 * m21) + const a02 = /* c20 = */ +(m01 * m12 - m02 * m11) + const a10 = /* c01 = */ -(m10 * m22 - m12 * m20) + const a11 = /* c11 = */ +(m00 * m22 - m02 * m20) + const a12 = /* c21 = */ -(m00 * m12 - m02 * m10) + + return new AffineTransform(a00, a01, a02, a10, a11, a12) + } + + inverted(): AffineTransform | null { + const det = this.det() + if (det === 0) return null + const adj = this.adj() + return adj.timesScalar(1 / det) + } + transformVector(v: Vec2) { return new Vec2( v.x * this.m00 + v.y * this.m01, @@ -89,6 +140,12 @@ export class AffineTransform { ) } + inverseTransformVector(v: Vec2): Vec2 | null { + const inv = this.inverted() + if (!inv) return null + return inv.transformVector(v) + } + transformPosition(v: Vec2) { return new Vec2( v.x * this.m00 + v.y * this.m01 + this.m02, @@ -96,6 +153,19 @@ export class AffineTransform { ) } + inverseTransformPosition(v: Vec2): Vec2 | null { + const inv = this.inverted() + if (!inv) return null + return inv.transformPosition(v) + } + + transformRect(r: Rect) { + return new Rect( + this.transformPosition(r.origin), + this.transformVector(r.size) + ) + } + flatten(): [number, number, number, number, number, number, number, number, number] { // Flatten into GLSL format return [