Skip to content

Instantly share code, notes, and snippets.

@andrew-kramer-inno
Last active March 2, 2026 15:44
Show Gist options
  • Select an option

  • Save andrew-kramer-inno/3f7697e92026ac98897ba609d4cfaea6 to your computer and use it in GitHub Desktop.

Select an option

Save andrew-kramer-inno/3f7697e92026ac98897ba609d4cfaea6 to your computer and use it in GitHub Desktop.
// =========================================================
// Isometric Engine Factory
//
// Creates a scoped engine bound to a specific SVG root and scale.
// Each consumer gets its own set of closured functions — no globals,
// no conflicts, no eval.
//
// Usage:
// const E = createIsoEngine(svgElement, { scale: 42 });
// const { iso, el, poly, group, gBox, ... } = E;
// =========================================================
(function () {
const root = window.StudioTycoon || (window.StudioTycoon = {});
root.randomSeed = 12345;
root.random = function () {
let t = root.randomSeed += 0x6D2B79F5;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
})();
function createIsoEngine(svgRoot, options = {}) {
const SVG_NS = 'http://www.w3.org/2000/svg';
const SCALE = options.scale || 42;
const CENTER_X = options.centerX || 400;
const CENTER_Y = options.centerY || 580;
const cam = (window.StudioTycoon && window.StudioTycoon.Camera) || {};
const pitch = options.pitch ?? cam.pitch ?? (Math.PI / 6);
const yaw = options.yaw ?? cam.yaw ?? 0;
const yawCenterX = options.yawCenterX ?? cam.yawCenterX ?? 5;
const yawCenterY = options.yawCenterY ?? cam.yawCenterY ?? 5;
const ISO_COS30 = Math.cos(pitch);
const ISO_SIN30 = Math.sin(pitch);
const VIEW_DIR = [-1, -1, 1];
// --- Core Primitives ---
function iso(worldX, worldY, z = 0) {
let x = worldX - yawCenterX;
let y = worldY - yawCenterY;
if (yaw !== 0) {
const rotX = x * Math.cos(yaw) - y * Math.sin(yaw);
const rotY = x * Math.sin(yaw) + y * Math.cos(yaw);
x = rotX;
y = rotY;
}
x += yawCenterX;
y += yawCenterY;
return {
x: (y - x) * SCALE * ISO_COS30 + CENTER_X,
y: -(x + y) * SCALE * ISO_SIN30 - z * SCALE + CENTER_Y
};
}
function el(tag, attrs = {}, parent = svgRoot) {
const e = document.createElementNS(SVG_NS, tag);
for (const [k, v] of Object.entries(attrs)) {
if (k === 'textContent') { e.textContent = v; continue; }
if (v !== undefined && v !== null) e.setAttribute(k, v);
}
if (typeof attrs.class === 'string') {
const timeSeconds = performance.now() / 1000;
const cls = attrs.class;
if (cls.match(/(?:^|\s)(bubble|typing-cursor|agent-character|holo-mag|floating-bubble|stamp-hologram)(?:\s|$)/)) {
e.style.setProperty('animation-delay', `-${timeSeconds.toFixed(3)}s`, 'important');
} else if (cls.match(/(?:^|\s)(holo-cyan)(?:\s|$)/)) {
e.style.setProperty('animation-delay', `-${(timeSeconds - 0.5).toFixed(3)}s`, 'important');
}
}
if (parent) parent.appendChild(e);
return e;
}
function poly(points, attrs = {}, parent = svgRoot) {
const screenPts = points.map(([x, y, z]) => {
const s = iso(x, y, z || 0);
return `${s.x.toFixed(2)},${s.y.toFixed(2)}`;
}).join(' ');
return el('polygon', { points: screenPts, ...attrs }, parent);
}
function ellipseFace(face, parent = svgRoot) {
const attrs = { cx: face.cx, cy: face.cy, rx: face.rx, ry: face.ry, ...face.attrs };
if (Math.abs(face.rotationDeg) > 1e-6) attrs.transform = `rotate(${face.rotationDeg} ${face.cx} ${face.cy})`;
return el('ellipse', attrs, parent);
}
function group(attrs = {}, parent = svgRoot) {
return el('g', attrs, parent);
}
function screenLineXPlane(x, y, z, lenPx, color, opacity = 0.85, parent = svgRoot, extraAttrs = {}) {
const worldLen = lenPx / (SCALE * ISO_COS30);
const p1 = iso(x, y, z);
const p2 = iso(x, y + worldLen, z);
const line = el('line', {
x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y,
stroke: color, 'stroke-width': 1.8, 'stroke-linecap': 'round', opacity
}, parent);
for (const [k, v] of Object.entries(extraAttrs)) line.setAttribute(k, v);
return line;
}
// --- Immediate Box Renderers ---
// Room-shell box (renders far faces: +Y, +X, top)
function isoBox(x, y, z, sx, sy, sz, colors, parent = svgRoot) {
const [top, left, right] = colors;
const g = group({}, parent);
poly([[x, y, z + sz], [x + sx, y, z + sz], [x + sx, y + sy, z + sz], [x, y + sy, z + sz]], { fill: top }, g);
poly([[x, y + sy, z], [x + sx, y + sy, z], [x + sx, y + sy, z + sz], [x, y + sy, z + sz]], { fill: left }, g);
poly([[x + sx, y, z], [x + sx, y + sy, z], [x + sx, y + sy, z + sz], [x + sx, y, z + sz]], { fill: right }, g);
return g;
}
// Solid object box (renders near faces: -Y, -X, top)
function isoSolidBox(x, y, z, sx, sy, sz, colors, parent = svgRoot) {
const [top, yFace, xFace] = colors;
const g = group({}, parent);
poly([[x, y, z], [x + sx, y, z], [x + sx, y, z + sz], [x, y, z + sz]], { fill: yFace }, g);
poly([[x, y, z], [x, y + sy, z], [x, y + sy, z + sz], [x, y, z + sz]], { fill: xFace }, g);
poly([[x, y, z + sz], [x + sx, y, z + sz], [x + sx, y + sy, z + sz], [x, y + sy, z + sz]], { fill: top }, g);
return g;
}
// --- 3D Transform Helpers ---
function makePitch(pivotX, pivotZ, angleDeg) {
const rad = angleDeg * Math.PI / 180;
const cos = Math.cos(rad), sin = Math.sin(rad);
return ([x, y, z]) => {
const dx = x - pivotX, dz = z - pivotZ;
return [pivotX + dx * cos - dz * sin, y, pivotZ + dx * sin + dz * cos];
};
}
function makeYaw(cx, cy, angleDeg) {
const rad = angleDeg * Math.PI / 180;
const cos = Math.cos(rad), sin = Math.sin(rad);
return ([x, y, z]) => {
const lx = x - cx, ly = y - cy;
return [cx + (lx * cos - ly * sin), cy + (lx * sin + ly * cos), z];
};
}
// --- 3D Face Generators (return face arrays for depth sorting) ---
function gBox(x, y, z, sx, sy, sz, colorMap, localPivotX = x + sx / 2, localPivotY = y + sy / 2, localAngle = 0, extraAttrs = {}, customTransform = null) {
const [cTop, cSideDark, cSideLight] = colorMap;
const rad = localAngle * Math.PI / 180;
const rot = (px, py, pz) => {
const lx = px - localPivotX, ly = py - localPivotY;
let p = [
localPivotX + lx * Math.cos(rad) - ly * Math.sin(rad),
localPivotY + lx * Math.sin(rad) + ly * Math.cos(rad),
pz
];
if (customTransform) p = customTransform(p);
return p;
};
const p = [
rot(x, y, z), rot(x + sx, y, z), rot(x + sx, y + sy, z), rot(x, y + sy, z),
rot(x, y, z + sz), rot(x + sx, y, z + sz), rot(x + sx, y + sy, z + sz), rot(x, y + sy, z + sz)
];
const center = rot(x + sx / 2, y + sy / 2, z + sz / 2);
return [
{ points: [p[0], p[1], p[5], p[4]], attrs: { fill: cSideDark, ...extraAttrs }, cullBackface: true, objectCenter: center },
{ points: [p[3], p[2], p[6], p[7]], attrs: { fill: cSideLight, ...extraAttrs }, cullBackface: true, objectCenter: center },
{ points: [p[0], p[4], p[7], p[3]], attrs: { fill: cSideLight, ...extraAttrs }, cullBackface: true, objectCenter: center },
{ points: [p[1], p[2], p[6], p[5]], attrs: { fill: cSideDark, ...extraAttrs }, cullBackface: true, objectCenter: center },
{ points: [p[4], p[5], p[6], p[7]], attrs: { fill: cTop, ...extraAttrs }, cullBackface: true, objectCenter: center },
{ points: [p[0], p[3], p[2], p[1]], attrs: { fill: '#020617', ...extraAttrs }, cullBackface: true, objectCenter: center }
];
}
function gCyl(cx, cy, z, r, h, colorMap, segments = 8, angleOffset = 0, extraAttrs = {}, customTransform = null) {
const faces = [];
const [cTop, cSideDark, cSideLight] = colorMap;
const ptsTop = [], ptsBot = [];
for (let i = 0; i < segments; i++) {
const a = angleOffset + (i * Math.PI * 2) / segments;
let ptT = [cx + r * Math.cos(a), cy + r * Math.sin(a), z + h];
let ptB = [cx + r * Math.cos(a), cy + r * Math.sin(a), z];
if (customTransform) { ptT = customTransform(ptT); ptB = customTransform(ptB); }
ptsTop.push(ptT); ptsBot.push(ptB);
}
const center = customTransform ? customTransform([cx, cy, z + h / 2]) : [cx, cy, z + h / 2];
faces.push({ points: ptsTop, attrs: { fill: cTop, ...extraAttrs }, cullBackface: true, objectCenter: center });
for (let i = 0; i < segments; i++) {
const nx = (i + 1) % segments;
const a = angleOffset + (i * Math.PI * 2) / segments;
const isLight = (Math.cos(a) - Math.sin(a)) > 0;
faces.push({
points: [ptsTop[i], ptsTop[nx], ptsBot[nx], ptsBot[i]],
attrs: { fill: isLight ? cSideLight : cSideDark, ...extraAttrs },
cullBackface: true,
objectCenter: center
});
}
return faces;
}
function gWedge(x, y, z, sx, sy, szFront, szBack, colorMap, localPivotX = x + sx / 2, localPivotY = y + sy / 2, localAngle = 0, extraAttrs = {}, customTransform = null) {
const [cTop, cSideDark, cSideLight] = colorMap;
const rad = localAngle * Math.PI / 180;
const rot = (px, py, pz) => {
const lx = px - localPivotX, ly = py - localPivotY;
let p = [
localPivotX + lx * Math.cos(rad) - ly * Math.sin(rad),
localPivotY + lx * Math.sin(rad) + ly * Math.cos(rad),
pz
];
if (customTransform) p = customTransform(p);
return p;
};
const p = [
rot(x, y, z), rot(x + sx, y, z), rot(x + sx, y + sy, z), rot(x, y + sy, z),
rot(x, y, z + szFront), rot(x + sx, y, z + szBack), rot(x + sx, y + sy, z + szBack), rot(x, y + sy, z + szFront)
];
return [
{ points: [p[0], p[1], p[5], p[4]], attrs: { fill: cSideDark, ...extraAttrs }, cullBackface: true },
{ points: [p[3], p[2], p[6], p[7]], attrs: { fill: cSideLight, ...extraAttrs }, cullBackface: true },
{ points: [p[0], p[4], p[7], p[3]], attrs: { fill: cSideLight, ...extraAttrs }, cullBackface: true },
{ points: [p[1], p[2], p[6], p[5]], attrs: { fill: cSideDark, ...extraAttrs }, cullBackface: true },
{ points: [p[4], p[5], p[6], p[7]], attrs: { fill: cTop, ...extraAttrs }, cullBackface: true },
{ points: [p[0], p[3], p[2], p[1]], attrs: { fill: '#020617', ...extraAttrs }, cullBackface: true }
];
}
// --- Plane Generators ---
function makePlane(x, y, z, sx, sy, color, localPivotX = x + sx / 2, localPivotY = y + sy / 2, localAngle = 0, extraAttrs = {}, customTransform = null) {
const rad = localAngle * Math.PI / 180;
const rot = (px, py, pz) => {
const lx = px - localPivotX, ly = py - localPivotY;
let p = [
localPivotX + lx * Math.cos(rad) - ly * Math.sin(rad),
localPivotY + lx * Math.sin(rad) + ly * Math.cos(rad),
pz
];
if (customTransform) p = customTransform(p);
return p;
};
return [{ points: [rot(x, y, z), rot(x + sx, y, z), rot(x + sx, y + sy, z), rot(x, y + sy, z)], attrs: { fill: color, ...extraAttrs }, cullBackface: false }];
}
function makeVerticalPlane(x, y, z, sy, sz, color, localAngle = 0, extraAttrs = {}, customTransform = null) {
const rad = localAngle * Math.PI / 180;
const cos = Math.cos(rad), sin = Math.sin(rad);
const cy = sy / 2, cz = sz / 2;
const rot = (dy, dz) => {
const ly = dy - cy, lz = dz - cz;
let p = [x, y + cy + (ly * cos - lz * sin), z + cz + (ly * sin + lz * cos)];
if (customTransform) p = customTransform(p);
return p;
};
return [{ points: [rot(0, 0), rot(sy, 0), rot(sy, sz), rot(0, sz)], attrs: { fill: color, ...extraAttrs }, cullBackface: false }];
}
function clipPolygonToYZBounds(points, minY, maxY, minZ, maxZ) {
const clipWithEdge = (polygon, isInside, intersect) => {
if (polygon.length === 0) return polygon;
const out = [];
let prev = polygon[polygon.length - 1];
let prevInside = isInside(prev);
for (const curr of polygon) {
const currInside = isInside(curr);
if (currInside) {
if (!prevInside) out.push(intersect(prev, curr));
out.push(curr);
} else if (prevInside) {
out.push(intersect(prev, curr));
}
prev = curr;
prevInside = currInside;
}
return out;
};
const intersectAtY = (a, b, yVal) => {
const denom = b[1] - a[1];
const t = Math.abs(denom) < 1e-9 ? 0 : (yVal - a[1]) / denom;
return [a[0] + (b[0] - a[0]) * t, yVal, a[2] + (b[2] - a[2]) * t];
};
const intersectAtZ = (a, b, zVal) => {
const denom = b[2] - a[2];
const t = Math.abs(denom) < 1e-9 ? 0 : (zVal - a[2]) / denom;
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, zVal];
};
let clipped = points.slice();
clipped = clipWithEdge(clipped, p => p[1] >= minY, (a, b) => intersectAtY(a, b, minY));
clipped = clipWithEdge(clipped, p => p[1] <= maxY, (a, b) => intersectAtY(a, b, maxY));
clipped = clipWithEdge(clipped, p => p[2] >= minZ, (a, b) => intersectAtZ(a, b, minZ));
clipped = clipWithEdge(clipped, p => p[2] <= maxZ, (a, b) => intersectAtZ(a, b, maxZ));
return clipped;
}
function makeVerticalPlaneClipped(x, y, z, sy, sz, color, localAngle = 0, extraAttrs = {}, customTransform = null, clipBounds = null) {
const rad = localAngle * Math.PI / 180;
const cos = Math.cos(rad), sin = Math.sin(rad);
const cy = sy / 2, cz = sz / 2;
const rotLocal = (dy, dz) => {
const ly = dy - cy, lz = dz - cz;
return [x, y + cy + (ly * cos - lz * sin), z + cz + (ly * sin + lz * cos)];
};
const localPoints = [rotLocal(0, 0), rotLocal(sy, 0), rotLocal(sy, sz), rotLocal(0, sz)];
const clippedLocal = clipBounds
? clipPolygonToYZBounds(localPoints, clipBounds.minY, clipBounds.maxY, clipBounds.minZ, clipBounds.maxZ)
: localPoints;
if (clippedLocal.length < 3) return [];
const points = customTransform ? clippedLocal.map(customTransform) : clippedLocal;
return [{ points, attrs: { fill: color, ...extraAttrs }, cullBackface: false }];
}
// --- Depth Sorting & Rendering ---
function faceNormal(points) {
const [a, b, c] = points;
const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2];
const vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
return [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
}
function faceCentroid(points) {
let cx = 0, cy = 0, cz = 0;
for (const [x, y, z] of points) { cx += x; cy += y; cz += z; }
const n = points.length || 1;
return [cx / n, cy / n, cz / n];
}
function isFrontFacing(points, objectCenter) {
let [nx, ny, nz] = faceNormal(points);
if (objectCenter) {
const [fx, fy, fz] = faceCentroid(points);
if (nx * (fx - objectCenter[0]) + ny * (fy - objectCenter[1]) + nz * (fz - objectCenter[2]) < 0) {
nx = -nx; ny = -ny; nz = -nz;
}
}
return nx * VIEW_DIR[0] + ny * VIEW_DIR[1] + nz * VIEW_DIR[2] > 1e-6;
}
function drawFacesByDepth(faces, parent = svgRoot) {
const visibleFaces = faces.filter(f => !f.cullBackface || isFrontFacing(f.points, f.objectCenter));
const keyed = visibleFaces.map((f, i) => {
let depth = 0, avgZ = 0, avgY = 0;
for (const [x, y, z] of f.points) { depth += x + y - z; avgZ += z; avgY += y; }
return { ...f, i, depth: depth / f.points.length, avgZ: avgZ / f.points.length, avgY: avgY / f.points.length };
});
keyed.sort((a, b) => (b.depth - a.depth) || (a.avgZ - b.avgZ) || (b.avgY - a.avgY) || (a.i - b.i));
keyed.forEach(f => {
if (f.kind === 'ellipse') ellipseFace(f, parent);
else poly(f.points, f.attrs, parent);
});
}
// --- Utilities ---
function worldOffsetToScreenTransform(dx, dy, dz = 0) {
let x = dx;
let y = dy;
if (yaw !== 0) {
const rotX = x * Math.cos(yaw) - y * Math.sin(yaw);
const rotY = x * Math.sin(yaw) + y * Math.cos(yaw);
x = rotX;
y = rotY;
}
return {
x: (y - x) * SCALE * ISO_COS30,
y: -(x + y) * SCALE * ISO_SIN30 - dz * SCALE
};
}
return {
SVG_NS, SCALE, CENTER_X, CENTER_Y, ISO_COS30, ISO_SIN30,
iso, el, poly, ellipseFace, group, screenLineXPlane,
isoBox, isoSolidBox,
makePitch, makeYaw,
gBox, gCyl, gWedge,
makePlane, makeVerticalPlane, clipPolygonToYZBounds, makeVerticalPlaneClipped,
faceNormal, faceCentroid, isFrontFacing,
drawFacesByDepth, renderLayer: drawFacesByDepth,
worldOffsetToScreenTransform
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment