UNPKG

@zag-js/rect-utils

Version:

```sh yarn add @zag-js/geometry # or npm i @zag-js/geometry ```

725 lines (702 loc) • 21.4 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/affine-transform.ts var AffineTransform = class _AffineTransform { constructor([m00, m01, m02, m10, m11, m12] = [0, 0, 0, 0, 0, 0]) { __publicField(this, "m00"); __publicField(this, "m01"); __publicField(this, "m02"); __publicField(this, "m10"); __publicField(this, "m11"); __publicField(this, "m12"); __publicField(this, "rotate", (...args) => { return this.prepend(_AffineTransform.rotate(...args)); }); __publicField(this, "scale", (...args) => { return this.prepend(_AffineTransform.scale(...args)); }); __publicField(this, "translate", (...args) => { return this.prepend(_AffineTransform.translate(...args)); }); this.m00 = m00; this.m01 = m01; this.m02 = m02; this.m10 = m10; this.m11 = m11; this.m12 = m12; } applyTo(point) { const { x, y } = point; const { m00, m01, m02, m10, m11, m12 } = this; return { x: m00 * x + m01 * y + m02, y: m10 * x + m11 * y + m12 }; } prepend(other) { return new _AffineTransform([ this.m00 * other.m00 + this.m01 * other.m10, // m00 this.m00 * other.m01 + this.m01 * other.m11, // m01 this.m00 * other.m02 + this.m01 * other.m12 + this.m02, // m02 this.m10 * other.m00 + this.m11 * other.m10, // m10 this.m10 * other.m01 + this.m11 * other.m11, // m11 this.m10 * other.m02 + this.m11 * other.m12 + this.m12 // m12 ]); } append(other) { return new _AffineTransform([ other.m00 * this.m00 + other.m01 * this.m10, // m00 other.m00 * this.m01 + other.m01 * this.m11, // m01 other.m00 * this.m02 + other.m01 * this.m12 + other.m02, // m02 other.m10 * this.m00 + other.m11 * this.m10, // m10 other.m10 * this.m01 + other.m11 * this.m11, // m11 other.m10 * this.m02 + other.m11 * this.m12 + other.m12 // m12 ]); } get determinant() { return this.m00 * this.m11 - this.m01 * this.m10; } get isInvertible() { const det = this.determinant; return isFinite(det) && isFinite(this.m02) && isFinite(this.m12) && det !== 0; } invert() { const det = this.determinant; return new _AffineTransform([ this.m11 / det, // m00 -this.m01 / det, // m01 (this.m01 * this.m12 - this.m11 * this.m02) / det, // m02 -this.m10 / det, // m10 this.m00 / det, // m11 (this.m10 * this.m02 - this.m00 * this.m12) / det // m12 ]); } get array() { return [this.m00, this.m01, this.m02, this.m10, this.m11, this.m12, 0, 0, 1]; } get float32Array() { return new Float32Array(this.array); } // Static static get identity() { return new _AffineTransform([1, 0, 0, 0, 1, 0]); } static rotate(theta, origin) { const rotation = new _AffineTransform([Math.cos(theta), -Math.sin(theta), 0, Math.sin(theta), Math.cos(theta), 0]); if (origin && (origin.x !== 0 || origin.y !== 0)) { return _AffineTransform.multiply( _AffineTransform.translate(origin.x, origin.y), rotation, _AffineTransform.translate(-origin.x, -origin.y) ); } return rotation; } static scale(sx, sy = sx, origin = { x: 0, y: 0 }) { const scale = new _AffineTransform([sx, 0, 0, 0, sy, 0]); if (origin.x !== 0 || origin.y !== 0) { return _AffineTransform.multiply( _AffineTransform.translate(origin.x, origin.y), scale, _AffineTransform.translate(-origin.x, -origin.y) ); } return scale; } static translate(tx, ty) { return new _AffineTransform([1, 0, tx, 0, 1, ty]); } static multiply(...[first, ...rest]) { if (!first) return _AffineTransform.identity; return rest.reduce((result, item) => result.prepend(item), first); } get a() { return this.m00; } get b() { return this.m10; } get c() { return this.m01; } get d() { return this.m11; } get tx() { return this.m02; } get ty() { return this.m12; } get scaleComponents() { return { x: this.a, y: this.d }; } get translationComponents() { return { x: this.tx, y: this.ty }; } get skewComponents() { return { x: this.c, y: this.b }; } toString() { return `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.tx}, ${this.ty})`; } }; // src/align.ts function hAlign(a, ref, h) { let x = ref.minX; if (h === "left-inside") x = ref.minX; if (h === "left-outside") x = ref.minX - ref.width; if (h === "right-inside") x = ref.maxX - ref.width; if (h === "right-outside") x = ref.maxX; if (h === "center") x = ref.midX - ref.width / 2; return { ...a, x }; } function vAlign(a, ref, v) { let y = ref.minY; if (v === "top-inside") y = ref.minY; if (v === "top-outside") y = ref.minY - a.height; if (v === "bottom-inside") y = ref.maxY - a.height; if (v === "bottom-outside") y = ref.maxY; if (v === "center") y = ref.midY - a.height / 2; return { ...a, y }; } function alignRect(a, ref, options) { const { h, v } = options; return vAlign(hAlign(a, ref, h), ref, v); } // src/angle.ts function getPointAngle(rect, point, reference = rect.center) { const x = point.x - reference.x; const y = point.y - reference.y; const deg = Math.atan2(x, y) * (180 / Math.PI) + 180; return 360 - deg; } // src/clamp.ts var clamp = (value, min3, max2) => Math.min(Math.max(value, min3), max2); var clampPoint = (position, size, boundaryRect) => { const x = clamp(position.x, boundaryRect.x, boundaryRect.x + boundaryRect.width - size.width); const y = clamp(position.y, boundaryRect.y, boundaryRect.y + boundaryRect.height - size.height); return { x, y }; }; var defaultMinSize = { width: 0, height: 0 }; var defaultMaxSize = { width: Infinity, height: Infinity }; var clampSize = (size, minSize = defaultMinSize, maxSize = defaultMaxSize) => { return { width: Math.min(Math.max(size.width, minSize.width), maxSize.width), height: Math.min(Math.max(size.height, minSize.height), maxSize.height) }; }; // src/rect.ts var createPoint = (x, y) => ({ x, y }); var subtractPoints = (a, b) => { if (!b) return a; return createPoint(a.x - b.x, a.y - b.y); }; var addPoints = (a, b) => createPoint(a.x + b.x, a.y + b.y); function isPoint(v) { return Reflect.has(v, "x") && Reflect.has(v, "y"); } function createRect(r) { const { x, y, width, height } = r; const midX = x + width / 2; const midY = y + height / 2; return { x, y, width, height, minX: x, minY: y, maxX: x + width, maxY: y + height, midX, midY, center: createPoint(midX, midY) }; } function isRect(v) { return Reflect.has(v, "x") && Reflect.has(v, "y") && Reflect.has(v, "width") && Reflect.has(v, "height"); } function getRectCenters(v) { const top = createPoint(v.midX, v.minY); const right = createPoint(v.maxX, v.midY); const bottom = createPoint(v.midX, v.maxY); const left = createPoint(v.minX, v.midY); return { top, right, bottom, left }; } function getRectCorners(v) { const top = createPoint(v.minX, v.minY); const right = createPoint(v.maxX, v.minY); const bottom = createPoint(v.maxX, v.maxY); const left = createPoint(v.minX, v.maxY); return { top, right, bottom, left }; } function getRectEdges(v) { const c = getRectCorners(v); const top = [c.top, c.right]; const right = [c.right, c.bottom]; const bottom = [c.left, c.bottom]; const left = [c.top, c.left]; return { top, right, bottom, left }; } // src/intersection.ts function intersects(a, b) { return a.x < b.maxX && a.y < b.maxY && a.maxX > b.x && a.maxY > b.y; } function intersection(a, b) { const x = Math.max(a.x, b.x); const y = Math.max(a.y, b.y); const x2 = Math.min(a.x + a.width, b.x + b.width); const y2 = Math.min(a.y + a.height, b.y + b.height); return createRect({ x, y, width: x2 - x, height: y2 - y }); } function collisions(a, b) { return { top: a.minY <= b.minY, right: a.maxX >= b.maxX, bottom: a.maxY >= b.maxY, left: a.minX <= b.minX }; } // src/distance.ts function distance(a, b = { x: 0, y: 0 }) { return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); } function distanceFromPoint(r, p) { let x = 0; let y = 0; if (p.x < r.x) x = r.x - p.x; else if (p.x > r.maxX) x = p.x - r.maxX; if (p.y < r.y) y = r.y - p.y; else if (p.y > r.maxY) y = p.y - r.maxY; return { x, y, value: distance({ x, y }) }; } function distanceFromRect(a, b) { if (intersects(a, b)) return { x: 0, y: 0, value: 0 }; const left = a.x < b.x ? a : b; const right = b.x < a.x ? a : b; const upper = a.y < b.y ? a : b; const lower = b.y < a.y ? a : b; let x = left.x === right.x ? 0 : right.x - left.maxX; x = Math.max(0, x); let y = upper.y === lower.y ? 0 : lower.y - upper.maxY; y = Math.max(0, y); return { x, y, value: distance({ x, y }) }; } function distanceBtwEdges(a, b) { return { left: b.x - a.x, top: b.y - a.y, right: a.maxX - b.maxX, bottom: a.maxY - b.maxY }; } // src/closest.ts function closest(...pts) { return (a) => { const ds = pts.map((b) => distance(b, a)); const c = Math.min.apply(Math, ds); return pts[ds.indexOf(c)]; }; } function closestSideToRect(ref, r) { if (r.maxX <= ref.minX) return "left"; if (r.minX >= ref.maxX) return "right"; if (r.maxY <= ref.minY) return "top"; if (r.minY >= ref.maxY) return "bottom"; return "left"; } function closestSideToPoint(ref, p) { const { x, y } = p; const dl = x - ref.minX; const dr = ref.maxX - x; const dt = y - ref.minY; const db = ref.maxY - y; let closest2 = dl; let side = "left"; if (dr < closest2) { closest2 = dr; side = "right"; } if (dt < closest2) { closest2 = dt; side = "top"; } if (db < closest2) { side = "bottom"; } return side; } // src/constrain.ts var constrainRect = (rect, boundary) => { const left = Math.max(boundary.x, Math.min(rect.x, boundary.x + boundary.width - rect.width)); const top = Math.max(boundary.y, Math.min(rect.y, boundary.y + boundary.height - rect.height)); return { x: left, y: top, width: Math.min(rect.width, boundary.width), height: Math.min(rect.height, boundary.height) }; }; // src/contains.ts function containsPoint(r, p) { return r.minX <= p.x && p.x <= r.maxX && r.minY <= p.y && p.y <= r.maxY; } function containsRect(a, b) { return Object.values(getRectCorners(b)).every((c) => containsPoint(a, c)); } function contains(r, v) { return isRect(v) ? containsRect(r, v) : containsPoint(r, v); } // src/equality.ts var isSizeEqual = (a, b) => { return a.width === b?.width && a.height === b?.height; }; var isPointEqual = (a, b) => { return a.x === b?.x && a.y === b?.y; }; var isRectEqual = (a, b) => { return isPointEqual(a, b) && isSizeEqual(a, b); }; // src/from-element.ts var styleCache = /* @__PURE__ */ new WeakMap(); function getCacheComputedStyle(el) { if (!styleCache.has(el)) { const win = el.ownerDocument.defaultView || window; styleCache.set(el, win.getComputedStyle(el)); } return styleCache.get(el); } function getElementRect(el, opts = {}) { return createRect(getClientRect(el, opts)); } function getClientRect(el, opts = {}) { const { excludeScrollbar = false, excludeBorders = false } = opts; const { x, y, width, height } = el.getBoundingClientRect(); const r = { x, y, width, height }; const style = getCacheComputedStyle(el); const { borderLeftWidth, borderTopWidth, borderRightWidth, borderBottomWidth } = style; const borderXWidth = sum(borderLeftWidth, borderRightWidth); const borderYWidth = sum(borderTopWidth, borderBottomWidth); if (excludeBorders) { r.width -= borderXWidth; r.height -= borderYWidth; r.x += px(borderLeftWidth); r.y += px(borderTopWidth); } if (excludeScrollbar) { const scrollbarWidth = el.offsetWidth - el.clientWidth - borderXWidth; const scrollbarHeight = el.offsetHeight - el.clientHeight - borderYWidth; r.width -= scrollbarWidth; r.height -= scrollbarHeight; } return r; } var px = (v) => parseFloat(v.replace("px", "")); var sum = (...vals) => vals.reduce((sum2, v) => sum2 + (v ? px(v) : 0), 0); // src/from-points.ts function getRectFromPoints(...pts) { const xs = pts.map((p) => p.x); const ys = pts.map((p) => p.y); const x = Math.min(...xs); const y = Math.min(...ys); const width = Math.max(...xs) - x; const height = Math.max(...ys) - y; return createRect({ x, y, width, height }); } // src/union.ts var { min, max } = Math; function union(...rs) { const pMin = { x: min(...rs.map((r) => r.minX)), y: min(...rs.map((r) => r.minY)) }; const pMax = { x: max(...rs.map((r) => r.maxX)), y: max(...rs.map((r) => r.maxY)) }; return getRectFromPoints(pMin, pMax); } // src/from-range.ts function fromRange(range) { let rs = []; const rects = Array.from(range.getClientRects()); if (rects.length) { rs = rs.concat(rects.map(createRect)); return union.apply(void 0, rs); } let start = range.startContainer; if (start.nodeType === Node.TEXT_NODE) { start = start.parentNode; } if (start instanceof HTMLElement) { const r = getElementRect(start); rs.push({ ...r, x: r.maxX, width: 0 }); } return union.apply(void 0, rs); } // src/from-rotation.ts function toRad(d) { return d % 360 * Math.PI / 180; } function rotate(a, d, c) { const r = toRad(d); const sin = Math.sin(r); const cos = Math.cos(r); const x = a.x - c.x; const y = a.y - c.y; return { x: c.x + x * cos - y * sin, y: c.y + x * sin + y * cos }; } function getRotationRect(r, deg) { const rr = Object.values(getRectCorners(r)).map((p) => rotate(p, deg, r.center)); const xs = rr.map((p) => p.x); const ys = rr.map((p) => p.y); const minX = Math.min(...xs); const minY = Math.min(...ys); const maxX = Math.max(...xs); const maxY = Math.max(...ys); return createRect({ x: minX, y: minY, width: maxX - minX, height: maxY - minY }); } // src/from-window.ts function getWindowRect(win, opts = {}) { return createRect(getViewportRect(win, opts)); } function getViewportRect(win, opts) { const { excludeScrollbar = false } = opts; const { innerWidth, innerHeight, document: doc, visualViewport } = win; const width = visualViewport?.width || innerWidth; const height = visualViewport?.height || innerHeight; const rect = { x: 0, y: 0, width, height }; if (excludeScrollbar) { const scrollbarWidth = innerWidth - doc.documentElement.clientWidth; const scrollbarHeight = innerHeight - doc.documentElement.clientHeight; rect.width -= scrollbarWidth; rect.height -= scrollbarHeight; } return rect; } // src/operations.ts var isSymmetric = (v) => "dx" in v || "dy" in v; function inset(r, i) { const v = isSymmetric(i) ? { left: i.dx, right: i.dx, top: i.dy, bottom: i.dy } : i; const { top = 0, right = 0, bottom = 0, left = 0 } = v; return createRect({ x: r.x + left, y: r.y + top, width: r.width - left - right, height: r.height - top - bottom }); } function expand(r, v) { const value = typeof v === "number" ? { dx: -v, dy: -v } : v; return inset(r, value); } function shrink(r, v) { const value = typeof v === "number" ? { dx: -v, dy: -v } : v; return inset(r, value); } function shift(r, o) { const { x = 0, y = 0 } = o; return createRect({ x: r.x + x, y: r.y + y, width: r.width, height: r.height }); } // src/polygon.ts function getElementPolygon(rectValue, placement) { const rect = createRect(rectValue); const { top, right, left, bottom } = getRectCorners(rect); const [base] = placement.split("-"); return { top: [left, top, right, bottom], right: [top, right, bottom, left], bottom: [top, left, bottom, right], left: [right, top, left, bottom] }[base]; } function isPointInPolygon(polygon, point) { const { x, y } = point; let c = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i].x; const yi = polygon[i].y; const xj = polygon[j].x; const yj = polygon[j].y; if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) { c = !c; } } return c; } function createPolygonElement() { const id = "debug-polygon"; const existingPolygon = document.getElementById(id); if (existingPolygon) { return existingPolygon; } const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); Object.assign(svg.style, { top: "0", left: "0", width: "100%", height: "100%", opacity: "0.15", position: "fixed", pointerEvents: "none", fill: "red" }); const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); polygon.setAttribute("id", id); polygon.setAttribute("points", "0,0 0,0"); svg.appendChild(polygon); document.body.appendChild(svg); return polygon; } function debugPolygon(polygon) { const el = createPolygonElement(); const points = polygon.map((point) => `${point.x},${point.y}`).join(" "); el.setAttribute("points", points); return () => { el.remove(); }; } // src/compass.ts var compassDirectionMap = { n: { x: 0.5, y: 0 }, ne: { x: 1, y: 0 }, e: { x: 1, y: 0.5 }, se: { x: 1, y: 1 }, s: { x: 0.5, y: 1 }, sw: { x: 0, y: 1 }, w: { x: 0, y: 0.5 }, nw: { x: 0, y: 0 } }; var oppositeDirectionMap = { n: "s", ne: "sw", e: "w", se: "nw", s: "n", sw: "ne", w: "e", nw: "se" }; // src/resize.ts var { sign, abs, min: min2 } = Math; function getRectExtentPoint(rect, direction) { const { minX, minY, maxX, maxY, midX, midY } = rect; const x = direction.includes("w") ? minX : direction.includes("e") ? maxX : midX; const y = direction.includes("n") ? minY : direction.includes("s") ? maxY : midY; return { x, y }; } function getOppositeDirection(direction) { return oppositeDirectionMap[direction]; } function resizeRect(rect, offset, direction, opts) { const { scalingOriginMode, lockAspectRatio } = opts; const extent = getRectExtentPoint(rect, direction); const oppositeDirection = getOppositeDirection(direction); const oppositeExtent = getRectExtentPoint(rect, oppositeDirection); if (scalingOriginMode === "center") { offset = { x: offset.x * 2, y: offset.y * 2 }; } const newExtent = { x: extent.x + offset.x, y: extent.y + offset.y }; const multiplier = { x: compassDirectionMap[direction].x * 2 - 1, y: compassDirectionMap[direction].y * 2 - 1 }; const newSize = { width: newExtent.x - oppositeExtent.x, height: newExtent.y - oppositeExtent.y }; const scaleX = multiplier.x * newSize.width / rect.width; const scaleY = multiplier.y * newSize.height / rect.height; const largestMagnitude = abs(scaleX) > abs(scaleY) ? scaleX : scaleY; const scale = lockAspectRatio ? { x: largestMagnitude, y: largestMagnitude } : { x: extent.x === oppositeExtent.x ? 1 : scaleX, y: extent.y === oppositeExtent.y ? 1 : scaleY }; if (extent.y === oppositeExtent.y) { scale.y = abs(scale.y); } else if (sign(scale.y) !== sign(scaleY)) { scale.y *= -1; } if (extent.x === oppositeExtent.x) { scale.x = abs(scale.x); } else if (sign(scale.x) !== sign(scaleX)) { scale.x *= -1; } switch (scalingOriginMode) { case "extent": return transformRect(rect, AffineTransform.scale(scale.x, scale.y, oppositeExtent), false); case "center": return transformRect( rect, AffineTransform.scale(scale.x, scale.y, { x: rect.midX, y: rect.midY }), false ); } } function createRectFromPoints(initialPoint, finalPoint, normalized = true) { if (normalized) { return { x: min2(finalPoint.x, initialPoint.x), y: min2(finalPoint.y, initialPoint.y), width: abs(finalPoint.x - initialPoint.x), height: abs(finalPoint.y - initialPoint.y) }; } return { x: initialPoint.x, y: initialPoint.y, width: finalPoint.x - initialPoint.x, height: finalPoint.y - initialPoint.y }; } function transformRect(rect, transform, normalized = true) { const p1 = transform.applyTo({ x: rect.minX, y: rect.minY }); const p2 = transform.applyTo({ x: rect.maxX, y: rect.maxY }); return createRectFromPoints(p1, p2, normalized); } export { AffineTransform, addPoints, alignRect, clampPoint, clampSize, closest, closestSideToPoint, closestSideToRect, collisions, constrainRect, contains, containsPoint, containsRect, createPoint, createRect, debugPolygon, distance, distanceBtwEdges, distanceFromPoint, distanceFromRect, expand, fromRange, getElementPolygon, getElementRect, getPointAngle, getRectCenters, getRectCorners, getRectEdges, getRectFromPoints, getRotationRect, getViewportRect, getWindowRect, inset, intersection, intersects, isPoint, isPointEqual, isPointInPolygon, isRect, isRectEqual, isSizeEqual, isSymmetric, resizeRect, rotate, shift, shrink, subtractPoints, toRad, union };