Zoom around cursor
This commit is contained in:
@@ -118,8 +118,6 @@ export class FlamechartView extends Component<FlamechartViewProps, void> {
|
||||
}
|
||||
|
||||
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<FlamechartViewProps, void> {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
74
math.ts
74
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 [
|
||||
|
||||
Reference in New Issue
Block a user