@hasen-ts/vlens
Version:
Data Centric Routing & Rendering Mini-Framework
407 lines (344 loc) • 10.9 kB
text/typescript
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);
}