UNPKG

@hasen-ts/vlens

Version:

Data Centric Routing & Rendering Mini-Framework

407 lines (344 loc) 10.9 kB
export interface Point { x: number; y: number; } export interface Size { width: number; height: number; } export interface Rect extends Point, Size {} export interface Frame { top: number; left: number; width: number; height: number; } export interface Placement { top?: number; left?: number; right?: number; bottom?: number; width?: number; height?: number; minWidth?: number; maxWidth?: number; minHeight?: number; maxHeight?: number; } export interface SizeConstraints { maxWidth?: number; maxHeight?: number; } export function zeroSize(): Size { return { width: 0, height: 0 }; } export function zeroPoint(): Point { return { x: 0, y: 0 }; } export function zeroFrame(): Frame { return { top: 0, left: 0, width: 0, height: 0, }; } export function zeroRect(): Rect { return { ...zeroPoint(), ...zeroSize(), }; } export function constrainedSize(size: Size, cons: SizeConstraints): Size { let width = size.width; let height = size.height; if (cons.maxHeight !== undefined && height > cons.maxHeight) { let factor = cons.maxHeight / height; height = cons.maxHeight; width = Math.floor(width * factor); // console.log({width, height}); } if (cons.maxWidth !== undefined && width > cons.maxWidth) { let factor = cons.maxWidth / width; width = cons.maxWidth; height = Math.floor(height * factor); // console.log({width, height}); } return { width, height }; } export function proportionalZoomDelta(size: Size, zoomDelta: Point): Point { const aspectRatio = size.width / size.height; let result = structuredClone(zoomDelta) if (zoomDelta.x >= 0 && zoomDelta.y >= 0) { if (zoomDelta.x > zoomDelta.y) { result.y = result.x * aspectRatio } else { result.x = result.y * aspectRatio } } else { if (zoomDelta.y > zoomDelta.x) { result.y = result.x * aspectRatio } else { result.x = result.y * aspectRatio } } return result } export function getWindowSize(): Size { return { width: document.body.clientWidth, height: document.body.clientHeight, }; } export function constrainedFrame(position: Point, size: Size, windowSize: Size): Frame { if (position.x + size.width > windowSize.width) { position.x = windowSize.width - size.width; if (position.x < 0) { position.x = 0; } } if (position.y + size.height > windowSize.height) { position.y = windowSize.height - size.height; if (position.y < 0) { position.y = 0; } } let frame: Frame = { top: position.y, left: position.x, width: size.width, height: size.height, }; return frame; } export function pointMinus(p0: Point, p1: Point): Point { return { x: p0.x - p1.x, y: p0.y - p1.y, }; } export function pointPlus(p0: Point, p1: Point): Point { return { x: p1.x + p0.x, y: p1.y + p0.y, }; } export type Line = [Point, Point]; export function vectorLength(vec: Point): number { return Math.sqrt(vec.x * vec.x + vec.y * vec.y); } export function pointDist(pt0: Point, pt1: Point): number { return vectorLength(pointMinus(pt1, pt0)) } export function vectorResize(vec: Point, newLen: number): Point { let len = Math.sqrt(vec.x * vec.x + vec.y * vec.y); let factor = newLen / len return { x: vec.x * factor, y: vec.y * factor, } } export function pointLerp(a: Point, b: Point, t: number): Point { const x = a.x + (b.x - a.x) * t; const y = a.y + (b.y - a.y) * t; return { x, y }; } export function pointMiddle(a: Point, b: Point): Point { return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, } } export function lineLength(line: Line): number { return pointDist(...line) } export function normalVector(pt0: Point, pt1: Point): Point { let vec = pointMinus(pt1, pt0); let normal: Point = { x: -vec.y, y: vec.x }; // surprisingly simple but correct! let length = vectorLength(normal); if (length === 0) { return zeroPoint(); } normal.x /= length; normal.y /= length; return normal; } export function parallelTransition(line: Line, distance: number): Line { let [pt0, pt1] = line; let normal = normalVector(pt0, pt1); normal.x *= distance; normal.y *= distance; return [pointPlus(pt0, normal), pointPlus(pt1, normal)]; } // generated by ChatGPT export function distanceFromPointToLine(p: Point, line: Line): number { const [a, b] = line; const lineLength = Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2); if (lineLength === 0) return Math.sqrt((p.x - a.x) ** 2 + (p.y - a.y) ** 2); const t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / (lineLength ** 2); const closestPoint = t < 0 ? a : t > 1 ? b : { x: a.x + t * (b.x - a.x), y: a.y + t * (b.y - a.y) }; return Math.sqrt((p.x - closestPoint.x) ** 2 + (p.y - closestPoint.y) ** 2); } // Interpolates or extrapolates a point on a line defined by a segment, based on factor t. // Generated by ChatGPT export function interpolateLine(line: Line, t: number): Point { const [pt0, pt1] = line; // Linearly interpolate or extrapolate the x and y coordinates const x = pt0.x + (pt1.x - pt0.x) * t; const y = pt0.y + (pt1.y - pt0.y) * t; return { x, y }; } export function pointInRect(point: Point, rect: Rect) { return ( point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height ); } // Provided by ChatGPT (including the comments!) // /** * Checks if two rectangles intersect using the Separating Axis Theorem (SAT). * According to SAT, two convex shapes are separated if and only if there exists an axis * along which the shapes do not overlap. For axis-aligned rectangles, this simplifies to * checking overlap along the x and y axes. * * @param rect1 - The first rectangle. * @param rect2 - The second rectangle. * @returns true if the rectangles intersect, false otherwise. */ export function rectsIntersect(rect1: Rect, rect2: Rect): boolean { return ( rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x && rect1.y < rect2.y + rect2.height && rect1.y + rect1.height > rect2.y ); } // some math export function pointScale(p: Point, f: number): Point { return { x: p.x * f, y: p.y * f, }; } export function sizeScale(s: Size, f: number): Size { return { width: s.width * f, height: s.height * f, } } export function frameScale(r: Frame, factor: number): Frame { return { top: Math.round(r.top * factor), left: Math.round(r.left * factor), width: Math.round(r.width * factor), height: Math.round(r.height * factor), }; } // returns negative number if point is inside rect, and positive if outside // this is basically (almost) a signed distance function that does not respect the corner radius export function distanceToRect(point: Point, rect: Rect): number { let half_width_x = rect.width / 2; let half_width_y = rect.height / 2; let center_x = rect.x + half_width_x; let center_y = rect.y + half_width_y; let dist_x = Math.abs(point.x - center_x) - half_width_x; let dist_y = Math.abs(point.y - center_y) - half_width_y; return Math.max(dist_x, dist_y); } // ------------------------------------------------------------------------------------------------- // Matrix 2D Transformations // ------------------------------------------------------------------------------------------------- // Generated by Claude export type M3 = Float32Array & { length: 9 } export function identityM3(): M3 { return new Float32Array([ 1, 0, 0, 0, 1, 0, 0, 0, 1 ]) as M3 } export function asContextTransform(m: M3): number[] { return [ m[0], m[1], m[3], m[4], m[2], m[5] ] } export type Vec2 = Float32Array & { length: 2 } export function vec2(x: number, y: number): Vec2 { return new Float32Array([x, y]) as Vec2 } export function translateM3(m: M3, vec: Vec2) { // Generated by Copilot // modify the matrix `m` so it's translated by the offset indicated by vec const tx = vec[0]; const ty = vec[1]; m[6] = m[0] * tx + m[3] * ty + m[6]; m[7] = m[1] * tx + m[4] * ty + m[7]; m[8] = m[2] * tx + m[5] * ty + m[8]; } export function scaleM3(m: M3, f: number) { // Generated by Copilot // modify the matrix `m` so it's scaled by factor f m[0] *= f; m[1] *= f; m[3] *= f; m[4] *= f; } export function rotationM3(turns: number): M3 { // Generated by Copilot // matrix representing a rotation in turns const angle = turns * 2 * Math.PI; // convert turns to radians const cos = Math.cos(angle); const sin = Math.sin(angle); return new Float32Array([ cos, -sin, 0, sin, cos, 0, 0, 0, 1 ]) as M3; } export function translationM3(v: Vec2): M3 { // translation matrix from the input vector return new Float32Array([ 1, 0, v[0], 0, 1, v[1], 0, 0, 1 ]) as M3; } export function matmul(m1: M3, m2: M3): M3 { // Generated by Copilot // matrix multiplication const result = new Float32Array(9) as M3; result[0] = m1[0] * m2[0] + m1[1] * m2[3] + m1[2] * m2[6]; result[1] = m1[0] * m2[1] + m1[1] * m2[4] + m1[2] * m2[7]; result[2] = m1[0] * m2[2] + m1[1] * m2[5] + m1[2] * m2[8]; result[3] = m1[3] * m2[0] + m1[4] * m2[3] + m1[5] * m2[6]; result[4] = m1[3] * m2[1] + m1[4] * m2[4] + m1[5] * m2[7]; result[5] = m1[3] * m2[2] + m1[4] * m2[5] + m1[5] * m2[8]; result[6] = m1[6] * m2[0] + m1[7] * m2[3] + m1[8] * m2[6]; result[7] = m1[6] * m2[1] + m1[7] * m2[4] + m1[8] * m2[7]; result[8] = m1[6] * m2[2] + m1[7] * m2[5] + m1[8] * m2[8]; return result; } export function rotationAroundPoint(pivot: Vec2, turns: number): M3 { let m = translationM3(vec2(-pivot[0], -pivot[1])) m = matmul(m, rotationM3(turns)) m = matmul(m, translationM3(pivot)) return m } export function rotateM3(m: M3, turns: number) { m.set(matmul(m, rotationM3(turns))) } export function vec2Plus(v1: Vec2, v2: Vec2): Vec2 { return vec2(v1[0] + v2[0], v1[1] + v2[1]); } export function vec2mul(v: Vec2, f: number): Vec2 { return vec2(v[0] * f, v[1] * f); } export function vec2Transform(v: Vec2, m: M3): Vec2 { const x = m[0] * v[0] + m[3] * v[1] + m[6]; const y = m[1] * v[0] + m[4] * v[1] + m[7]; return vec2(x, y); }