171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
import type { Effect, Stroke } from './scene-graph'
|
|
import type { Rect, Vector } from './types'
|
|
|
|
export function degToRad(degrees: number): number {
|
|
return (degrees * Math.PI) / 180
|
|
}
|
|
|
|
export function radToDeg(radians: number): number {
|
|
return (radians * 180) / Math.PI
|
|
}
|
|
|
|
export function rotatePoint(px: number, py: number, cx: number, cy: number, rad: number): Vector {
|
|
const cos = Math.cos(rad)
|
|
const sin = Math.sin(rad)
|
|
return {
|
|
x: cx + (px - cx) * cos - (py - cy) * sin,
|
|
y: cy + (px - cx) * sin + (py - cy) * cos
|
|
}
|
|
}
|
|
|
|
export function rotatedCorners(
|
|
cx: number,
|
|
cy: number,
|
|
hw: number,
|
|
hh: number,
|
|
rotationDeg: number
|
|
): [Vector, Vector, Vector, Vector] {
|
|
const rad = degToRad(rotationDeg)
|
|
const cos = Math.cos(rad)
|
|
const sin = Math.sin(rad)
|
|
return [
|
|
{ x: cx + -hw * cos - -hh * sin, y: cy + -hw * sin + -hh * cos },
|
|
{ x: cx + hw * cos - -hh * sin, y: cy + hw * sin + -hh * cos },
|
|
{ x: cx + hw * cos - hh * sin, y: cy + hw * sin + hh * cos },
|
|
{ x: cx + -hw * cos - hh * sin, y: cy + -hw * sin + hh * cos }
|
|
]
|
|
}
|
|
|
|
export function rotatedBBox(
|
|
x: number,
|
|
y: number,
|
|
w: number,
|
|
h: number,
|
|
rotationDeg: number
|
|
): { left: number; right: number; top: number; bottom: number; centerX: number; centerY: number } {
|
|
if (rotationDeg === 0) {
|
|
return {
|
|
left: x,
|
|
right: x + w,
|
|
top: y,
|
|
bottom: y + h,
|
|
centerX: x + w / 2,
|
|
centerY: y + h / 2
|
|
}
|
|
}
|
|
const corners = rotatedCorners(x + w / 2, y + h / 2, w / 2, h / 2, rotationDeg)
|
|
let left = Infinity,
|
|
right = -Infinity,
|
|
top = Infinity,
|
|
bottom = -Infinity
|
|
for (const c of corners) {
|
|
left = Math.min(left, c.x)
|
|
right = Math.max(right, c.x)
|
|
top = Math.min(top, c.y)
|
|
bottom = Math.max(bottom, c.y)
|
|
}
|
|
return { left, right, top, bottom, centerX: (left + right) / 2, centerY: (top + bottom) / 2 }
|
|
}
|
|
|
|
export function computeBounds(items: Iterable<Rect>): Rect {
|
|
let minX = Infinity,
|
|
minY = Infinity,
|
|
maxX = -Infinity,
|
|
maxY = -Infinity
|
|
for (const item of items) {
|
|
minX = Math.min(minX, item.x)
|
|
minY = Math.min(minY, item.y)
|
|
maxX = Math.max(maxX, item.x + item.width)
|
|
maxY = Math.max(maxY, item.y + item.height)
|
|
}
|
|
if (minX === Infinity) return { x: 0, y: 0, width: 0, height: 0 }
|
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
}
|
|
|
|
function strokeOverflow(strokes?: Stroke[]): number {
|
|
let overflow = 0
|
|
for (const stroke of strokes ?? []) {
|
|
if (!stroke.visible) continue
|
|
let extra = 0
|
|
if (stroke.align === 'OUTSIDE') extra = stroke.weight
|
|
else if (stroke.align === 'CENTER') extra = stroke.weight / 2
|
|
overflow = Math.max(overflow, extra)
|
|
}
|
|
return overflow
|
|
}
|
|
|
|
function effectOverflow(effects?: Effect[]) {
|
|
let left = 0
|
|
let right = 0
|
|
let top = 0
|
|
let bottom = 0
|
|
|
|
for (const effect of effects ?? []) {
|
|
if (!effect.visible) continue
|
|
if (
|
|
effect.type !== 'DROP_SHADOW' &&
|
|
effect.type !== 'LAYER_BLUR' &&
|
|
effect.type !== 'FOREGROUND_BLUR'
|
|
) {
|
|
continue
|
|
}
|
|
const blurSpread = effect.radius + effect.spread
|
|
left = Math.max(left, blurSpread + Math.max(0, -effect.offset.x))
|
|
right = Math.max(right, blurSpread + Math.max(0, effect.offset.x))
|
|
top = Math.max(top, blurSpread + Math.max(0, -effect.offset.y))
|
|
bottom = Math.max(bottom, blurSpread + Math.max(0, effect.offset.y))
|
|
}
|
|
|
|
return { left, right, top, bottom }
|
|
}
|
|
|
|
export function computeAbsoluteBounds(
|
|
nodes: Iterable<{ id: string; width: number; height: number }>,
|
|
getAbsolutePosition: (id: string) => Vector
|
|
): Rect {
|
|
let minX = Infinity,
|
|
minY = Infinity,
|
|
maxX = -Infinity,
|
|
maxY = -Infinity
|
|
for (const n of nodes) {
|
|
const abs = getAbsolutePosition(n.id)
|
|
minX = Math.min(minX, abs.x)
|
|
minY = Math.min(minY, abs.y)
|
|
maxX = Math.max(maxX, abs.x + n.width)
|
|
maxY = Math.max(maxY, abs.y + n.height)
|
|
}
|
|
if (minX === Infinity) return { x: 0, y: 0, width: 0, height: 0 }
|
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
}
|
|
|
|
export function computeVisualBounds(
|
|
nodes: Iterable<{
|
|
id: string
|
|
width: number
|
|
height: number
|
|
rotation?: number
|
|
strokes?: Stroke[]
|
|
effects?: Effect[]
|
|
}>,
|
|
getAbsolutePosition: (id: string) => Vector
|
|
): Rect {
|
|
let minX = Infinity,
|
|
minY = Infinity,
|
|
maxX = -Infinity,
|
|
maxY = -Infinity
|
|
|
|
for (const n of nodes) {
|
|
const abs = getAbsolutePosition(n.id)
|
|
const bbox = rotatedBBox(abs.x, abs.y, n.width, n.height, n.rotation ?? 0)
|
|
const stroke = strokeOverflow(n.strokes)
|
|
const effects = effectOverflow(n.effects)
|
|
minX = Math.min(minX, bbox.left - stroke - effects.left)
|
|
minY = Math.min(minY, bbox.top - stroke - effects.top)
|
|
maxX = Math.max(maxX, bbox.right + stroke + effects.right)
|
|
maxY = Math.max(maxY, bbox.bottom + stroke + effects.bottom)
|
|
}
|
|
|
|
if (minX === Infinity) return { x: 0, y: 0, width: 0, height: 0 }
|
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
}
|