DesignGym / web /js /util.js
AdithyaSK's picture
AdithyaSK HF Staff
Serve Crello corpus aafdb0e86816 from the mounted bucket
df84e45 verified
Raw History Blame Contribute Delete
3.88 kB
// Small DOM and maths helpers shared by every module.
export function h(tag, attrs = {}, ...children) {
const el = document.createElement(tag);
for (const [k, v] of Object.entries(attrs || {})) {
if (v === undefined || v === null || v === false) continue;
if (k === "class") el.className = v;
else if (k === "style" && typeof v === "object") Object.assign(el.style, v);
else if (k === "html") el.innerHTML = v;
else if (k === "dataset") Object.assign(el.dataset, v);
else if (k.startsWith("on") && typeof v === "function") el.addEventListener(k.slice(2).toLowerCase(), v);
else if (v === true) el.setAttribute(k, "");
else el.setAttribute(k, v);
}
for (const c of children.flat(Infinity)) {
if (c === undefined || c === null || c === false) continue;
el.append(c instanceof Node ? c : document.createTextNode(String(c)));
}
return el;
}
export const $ = (sel, root = document) => root.querySelector(sel);
export const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
export const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
export const round2 = (v) => Math.round(v * 100) / 100;
export function fmt(v, digits = 0) {
if (v === null || v === undefined || Number.isNaN(v)) return "";
const r = Number(v.toFixed(digits));
return String(Object.is(r, -0) ? 0 : r);
}
export function debounce(fn, ms) {
let t = null;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
}
export class Emitter {
constructor() { this._h = new Map(); }
on(evt, fn) {
if (!this._h.has(evt)) this._h.set(evt, new Set());
this._h.get(evt).add(fn);
return () => this._h.get(evt).delete(fn);
}
emit(evt, ...args) {
for (const fn of this._h.get(evt) || []) {
try { fn(...args); } catch (e) { console.error(e); }
}
}
}
export const rad = (deg) => (deg * Math.PI) / 180;
export const deg = (r) => (r * 180) / Math.PI;
export function rotate(px, py, a) {
const c = Math.cos(a), s = Math.sin(a);
return [px * c - py * s, px * s + py * c];
}
// Axis-aligned bounds of an element's rotated box, in canvas pixels.
export function bounds(el) {
if (!el.angle) return { x0: el.x, y0: el.y, x1: el.x + el.width, y1: el.y + el.height };
const cx = el.x + el.width / 2, cy = el.y + el.height / 2;
const a = rad(el.angle);
const hw = (Math.abs(el.width * Math.cos(a)) + Math.abs(el.height * Math.sin(a))) / 2;
const hh = (Math.abs(el.width * Math.sin(a)) + Math.abs(el.height * Math.cos(a))) / 2;
return { x0: cx - hw, y0: cy - hh, x1: cx + hw, y1: cy + hh };
}
export function unionBounds(els) {
const bs = els.map(bounds);
return {
x0: Math.min(...bs.map((b) => b.x0)), y0: Math.min(...bs.map((b) => b.y0)),
x1: Math.max(...bs.map((b) => b.x1)), y1: Math.max(...bs.map((b) => b.y1)),
};
}
export function cssColor(hex) {
if (!hex) return "transparent";
if (hex.length === 9) {
const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16), a = parseInt(hex.slice(7, 9), 16) / 255;
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(3)})`;
}
return hex;
}
export function elementLabel(el) {
if (el.name) return el.name;
if (el.kind === "text") {
const t = (el.text || "").replace(/\s+/g, " ").trim();
return t.length > 28 ? t.slice(0, 26) + "…" : t || "Text";
}
if (el.kind === "shape") return el.shape === "ellipse" ? "Ellipse" : "Rectangle";
return `Image ${el.asset_id}`;
}
export function isMac() {
return /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
}
export function shortcut(s) {
if (!s) return "";
const mac = isMac();
return s.replace(/Mod/g, mac ? "⌘" : "Ctrl").replace(/Shift/g, mac ? "⇧" : "Shift")
.replace(/Alt/g, mac ? "⌥" : "Alt").replace(/\+/g, mac ? "" : "+");
}