Skip to content

Instantly share code, notes, and snippets.

@steveruizok
Last active July 29, 2021 07:14
Show Gist options
  • Save steveruizok/72ba6de2c6d535b8fdbc9d40111644fd to your computer and use it in GitHub Desktop.
Save steveruizok/72ba6de2c6d535b8fdbc9d40111644fd to your computer and use it in GitHub Desktop.
getTransformedBoundingBox
/**
* Get the relative bounds (usually a child) within a transformed bounding box.
*/
function getRelativeTransformedBoundingBox(
bounds: TLBounds,
initialBounds: TLBounds,
initialShapeBounds: TLBounds,
isFlippedX: boolean,
isFlippedY: boolean,
): TLBounds {
const nx =
(isFlippedX
? initialBounds.maxX - initialShapeBounds.maxX
: initialShapeBounds.minX - initialBounds.minX) / initialBounds.width
const ny =
(isFlippedY
? initialBounds.maxY - initialShapeBounds.maxY
: initialShapeBounds.minY - initialBounds.minY) / initialBounds.height
const nw = initialShapeBounds.width / initialBounds.width
const nh = initialShapeBounds.height / initialBounds.height
const minX = bounds.minX + bounds.width * nx
const minY = bounds.minY + bounds.height * ny
const width = bounds.width * nw
const height = bounds.height * nh
return {
minX,
minY,
maxX: minX + width,
maxY: minY + height,
width,
height,
}
}
function getTransformedBoundingBox(
bounds: TLBounds,
handle: TLBoundsCorner | TLBoundsEdge | 'center',
delta: number[],
rotation = 0,
isAspectRatioLocked = false,
): TLBounds & { scaleX: number; scaleY: number } {
// Create top left and bottom right corners.
const [ax0, ay0] = [bounds.minX, bounds.minY]
const [ax1, ay1] = [bounds.maxX, bounds.maxY]
// Create a second set of corners for the new box.
let [bx0, by0] = [bounds.minX, bounds.minY]
let [bx1, by1] = [bounds.maxX, bounds.maxY]
// If the drag is on the center, just translate the bounds.
if (handle === 'center') {
return {
minX: bx0 + delta[0],
minY: by0 + delta[1],
maxX: bx1 + delta[0],
maxY: by1 + delta[1],
width: bx1 - bx0,
height: by1 - by0,
scaleX: 1,
scaleY: 1,
}
}
// Counter rotate the delta. This lets us make changes as if
// the (possibly rotated) boxes were axis aligned.
const [dx, dy] = vec.rot(delta, -rotation)
/*
1. Delta
Use the delta to adjust the new box by changing its corners.
The dragging handle (corner or edge) will determine which
corners should change.
*/
switch (handle) {
case TLBoundsEdge.Top:
case TLBoundsCorner.TopLeft:
case TLBoundsCorner.TopRight: {
by0 += dy
break
}
case TLBoundsEdge.Bottom:
case TLBoundsCorner.BottomLeft:
case TLBoundsCorner.BottomRight: {
by1 += dy
break
}
}
switch (handle) {
case TLBoundsEdge.Left:
case TLBoundsCorner.TopLeft:
case TLBoundsCorner.BottomLeft: {
bx0 += dx
break
}
case TLBoundsEdge.Right:
case TLBoundsCorner.TopRight:
case TLBoundsCorner.BottomRight: {
bx1 += dx
break
}
}
const aw = ax1 - ax0
const ah = ay1 - ay0
const scaleX = (bx1 - bx0) / aw
const scaleY = (by1 - by0) / ah
const flipX = scaleX < 0
const flipY = scaleY < 0
const bw = Math.abs(bx1 - bx0)
const bh = Math.abs(by1 - by0)
/*
2. Aspect ratio
If the aspect ratio is locked, adjust the corners so that the
new box's aspect ratio matches the original aspect ratio.
*/
if (isAspectRatioLocked) {
const ar = aw / ah
const isTall = ar < bw / bh
const tw = bw * (scaleY < 0 ? 1 : -1) * (1 / ar)
const th = bh * (scaleX < 0 ? 1 : -1) * ar
switch (handle) {
case TLBoundsCorner.TopLeft: {
if (isTall) by0 = by1 + tw
else bx0 = bx1 + th
break
}
case TLBoundsCorner.TopRight: {
if (isTall) by0 = by1 + tw
else bx1 = bx0 - th
break
}
case TLBoundsCorner.BottomRight: {
if (isTall) by1 = by0 - tw
else bx1 = bx0 - th
break
}
case TLBoundsCorner.BottomLeft: {
if (isTall) by1 = by0 - tw
else bx0 = bx1 + th
break
}
case TLBoundsEdge.Bottom:
case TLBoundsEdge.Top: {
const m = (bx0 + bx1) / 2
const w = bh * ar
bx0 = m - w / 2
bx1 = m + w / 2
break
}
case TLBoundsEdge.Left:
case TLBoundsEdge.Right: {
const m = (by0 + by1) / 2
const h = bw / ar
by0 = m - h / 2
by1 = m + h / 2
break
}
}
}
/*
3. Rotation
If the bounds are rotated, get a vector from the rotated anchor
corner in the inital bounds to the rotated anchor corner in the
result's bounds. Subtract this vector from the result's corners,
so that the two anchor points (initial and result) will be equal.
*/
if (rotation % (Math.PI * 2) !== 0) {
let cv = [0, 0]
const c0 = vec.med([ax0, ay0], [ax1, ay1])
const c1 = vec.med([bx0, by0], [bx1, by1])
switch (handle) {
case TLBoundsCorner.TopLeft: {
cv = vec.sub(vec.rotWith([bx1, by1], c1, rotation), vec.rotWith([ax1, ay1], c0, rotation))
break
}
case TLBoundsCorner.TopRight: {
cv = vec.sub(vec.rotWith([bx0, by1], c1, rotation), vec.rotWith([ax0, ay1], c0, rotation))
break
}
case TLBoundsCorner.BottomRight: {
cv = vec.sub(vec.rotWith([bx0, by0], c1, rotation), vec.rotWith([ax0, ay0], c0, rotation))
break
}
case TLBoundsCorner.BottomLeft: {
cv = vec.sub(vec.rotWith([bx1, by0], c1, rotation), vec.rotWith([ax1, ay0], c0, rotation))
break
}
case TLBoundsEdge.Top: {
cv = vec.sub(
vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation),
vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation),
)
break
}
case TLBoundsEdge.Left: {
cv = vec.sub(
vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation),
vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation),
)
break
}
case TLBoundsEdge.Bottom: {
cv = vec.sub(
vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation),
vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation),
)
break
}
case TLBoundsEdge.Right: {
cv = vec.sub(
vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation),
vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation),
)
break
}
}
;[bx0, by0] = vec.sub([bx0, by0], cv)
;[bx1, by1] = vec.sub([bx1, by1], cv)
}
/*
4. Flips
If the axes are flipped (e.g. if the right edge has been dragged
left past the initial left edge) then swap points on that axis.
*/
if (bx1 < bx0) {
;[bx1, bx0] = [bx0, bx1]
}
if (by1 < by0) {
;[by1, by0] = [by0, by1]
}
return {
minX: bx0,
minY: by0,
maxX: bx1,
maxY: by1,
width: bx1 - bx0,
height: by1 - by0,
scaleX: ((bx1 - bx0) / (ax1 - ax0 || 1)) * (flipX ? -1 : 1),
scaleY: ((by1 - by0) / (ay1 - ay0 || 1)) * (flipY ? -1 : 1),
}
}
@idan
Copy link

idan commented Jul 29, 2021

I think it'd be easier to represent as a series of 3x3 matrices, and stored in state. Any affine transform is just one of the matrices.

This reads pretty solid to me: https://neutrium.net/mathematics/basics-of-affine-transformation/ but the way they explain how to generate the various transforms makes my brain hurt, I'll find a better source tomorrow.

@idan
Copy link

idan commented Jul 29, 2021

Ahh, right. In order to translate around a specific point (center, corner, etc) you are actually doing 3 operations

  1. translating the entire thing you want to rotate so as to put 0,0 on the spot you want as the origin of rotation
  2. rotate
  3. translate by the inverse of whatever you did in step 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment