diff --git a/diorama.js b/diorama.js
index c2bab7ef..a288f034 100644
--- a/diorama.js
+++ b/diorama.js
@@ -4,6 +4,9 @@ import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js';
export class Diorama {
constructor(elementId, world, requestRenderCallback) {
this.element = document.getElementById(elementId);
+ this.canvas = document.createElement('canvas')
+ this.element.appendChild(this.canvas)
+ this.ctx2d = this.canvas.getContext('2d'); // Lightweight 2D drawing surface
this.world = world;
this.requestRender = requestRenderCallback;
@@ -32,7 +35,9 @@ export class Diorama {
}
initUI() {
+ // Because the element is a canvas itself, buttons are appended to its parent container
this.element.style.position = "relative";
+
this.resetBtn = document.createElement('button');
this.resetBtn.textContent = 'Reset View';
this.resetBtn.style.cssText = `
@@ -48,8 +53,10 @@ export class Diorama {
this.loadState();
this.resetBtn.style.display = 'none';
});
-
this.element.appendChild(this.resetBtn);
+
+ this.touchTimer = null;
+ this.touchTimedOut = false;
}
saveState() {
@@ -128,8 +135,6 @@ export class Diorama {
return this.viewProjMatrix;
}
- // --- INTERACTION MATH ---
-
orbit(dx, dy) {
this.theta -= dx * 0.4;
this.phi += dy * 0.4;
@@ -147,19 +152,23 @@ export class Diorama {
const upY = Math.cos(p);
const upZ = -Math.sin(p) * Math.cos(t);
- const panSpeed = this.radius * 0.0025;
+ const rect = this.canvas.getBoundingClientRect();
+ const panSpeed = this.radius / rect.height;
this.target[0] += (-rightX * dx + upX * dy) * panSpeed;
this.target[1] += (upY * dy) * panSpeed;
this.target[2] += (-rightZ * dx + upZ * dy) * panSpeed;
}
- zoom(delta) {
- this.radius += delta * 0.05;
+ zoomRatio(ratio) {
+ this.radius *= ratio;
this.radius = Math.max(1, Math.min(50, this.radius));
}
- // --- EVENT LISTENERS ---
+ zoomLinear(delta) {
+ this.radius += delta * 0.05;
+ this.radius = Math.max(1, Math.min(50, this.radius));
+ }
attachEvents() {
this.element.addEventListener('contextmenu', e => e.preventDefault());
@@ -199,20 +208,27 @@ export class Diorama {
if (this.dragButton === 0) this.orbit(dx, dy);
else if (this.dragButton === 1) this.pan(dx, dy);
- else if (this.dragButton === 2) this.zoom(dy);
+ else if (this.dragButton === 2) this.zoomLinear(dy);
notifyChange();
});
// --- TOUCH EVENTS ---
this.element.addEventListener('touchstart', (e) => {
- e.preventDefault();
- this.isDragging = true;
+ if (this.touchTimer) clearTimeout(this.touchTimer);
+ this.touchTimedOut = false;
if (e.touches.length === 1) {
- this.touchMode = 'orbit';
- this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
+ this.touchTimer = setTimeout(() => {
+ this.touchTimedOut = true;
+ this.isDragging = true;
+ this.touchMode = 'orbit';
+ this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
+ this.element.style.cursor = 'grabbing';
+ }, 200);
} else if (e.touches.length >= 2) {
+ this.touchTimedOut = true;
+ this.isDragging = true;
this.touchMode = 'pan-zoom';
const t1 = e.touches[0], t2 = e.touches[1];
this.lastMouse = {
@@ -221,11 +237,20 @@ export class Diorama {
};
this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
}
- }, {passive: false});
+ }, {passive: true});
this.element.addEventListener('touchmove', (e) => {
+ if (!this.touchTimedOut) {
+ if (this.touchTimer) {
+ clearTimeout(this.touchTimer);
+ this.touchTimer = null;
+ }
+ this.isDragging = false;
+ return;
+ }
+
if (!this.isDragging) return;
- e.preventDefault();
+ if (e.cancelable) e.preventDefault();
if (this.touchMode === 'orbit' && e.touches.length === 1) {
const dx = e.touches[0].clientX - this.lastMouse.x;
@@ -242,20 +267,26 @@ export class Diorama {
const dx = cx - this.lastMouse.x;
const dy = cy - this.lastMouse.y;
- const dDist = dist - this.lastPinchDist;
+ const zRatio = this.lastPinchDist > 0 ? (this.lastPinchDist / dist) : 1;
this.lastMouse = {x: cx, y: cy};
this.lastPinchDist = dist;
this.pan(dx, dy);
- this.zoom(-dDist);
+ this.zoomRatio(zRatio);
notifyChange();
}
}, {passive: false});
const onTouchEnd = (e) => {
+ if (this.touchTimer) {
+ clearTimeout(this.touchTimer);
+ this.touchTimer = null;
+ }
+
if (e.touches.length === 0) {
this.isDragging = false;
+ this.touchMode = '';
} else if (e.touches.length === 1) {
this.touchMode = 'orbit';
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
diff --git a/engine.js b/engine.js
index dd59a6cd..2ba779ba 100644
--- a/engine.js
+++ b/engine.js
@@ -62,20 +62,15 @@ function compileShader(gl, type, src) {
export class Engine {
constructor() {
+ // One solitary hidden canvas to drive all WebGL multi-target logic
this.canvas = document.createElement('canvas');
- this.canvas.style.position = 'fixed';
- this.canvas.style.top = '0';
- this.canvas.style.left = '0';
- this.canvas.style.width = '100vw';
- this.canvas.style.height = '100vh';
- this.canvas.style.zIndex = '-1';
+ this.canvas.style.display = 'none';
document.body.appendChild(this.canvas);
this.gl = this.canvas.getContext('webgl2', {antialias: true, alpha: true});
const gl = this.gl;
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
- gl.enable(gl.SCISSOR_TEST);
const vs = compileShader(gl, gl.VERTEX_SHADER, VS_SRC);
const fs = compileShader(gl, gl.FRAGMENT_SHADER, FS_SRC);
@@ -93,7 +88,6 @@ export class Engine {
this.atlasTexture = gl.createTexture();
- // --- Initialize the new Resource Pipeline ---
this.cache = new Cache('assets');
this.atlas = new TextureHandler(256);
this.cache.register('blockstates', new BlockstateHandler());
@@ -103,22 +97,18 @@ export class Engine {
this.dioramas = [];
this.worldMeshes = new Map();
this.renderRequested = false;
-
this.observedWorlds = new Set();
this.updateRequested = false;
- window.addEventListener('resize', () => {
- this.resize();
- this.requestRender();
- });
- window.addEventListener('scroll', () => this.requestRender(), {passive: true});
- this.resize();
+ window.addEventListener('resize', () => this.requestRender());
+
+ // to make sure offscreen dioramas are rendered as soon as they become visible
+ window.addEventListener('scroll', () => this.requestRender());
}
requestUpdate() {
if (!this.updateRequested) {
this.updateRequested = true;
- // Add 'async' here and 'await' updateAll
Promise.resolve().then(async () => {
await this.updateAll();
this.updateRequested = false;
@@ -136,14 +126,9 @@ export class Engine {
}
}
- resize() {
- this.canvas.width = window.innerWidth;
- this.canvas.height = window.innerHeight;
- }
-
addDiorama(diorama) {
this.dioramas.push(diorama);
- if (!this.observedWorlds.has(diorama.worlds)) {
+ if (!this.observedWorlds.has(diorama.world)) {
this.observedWorlds.add(diorama.world);
diorama.world.subscribe(() => this.requestUpdate());
}
@@ -153,14 +138,12 @@ export class Engine {
const uniqueWorlds = new Set(this.dioramas.map(d => d.world));
if (uniqueWorlds.size === 0) return;
- // --- SWEEP 1: Collect & Fetch Blockstates ---
const uniqueBlockIds = new Set();
for (const world of uniqueWorlds) {
for (const block of world.blocks.values()) uniqueBlockIds.add(block.id);
}
await Promise.all(Array.from(uniqueBlockIds).map(id => this.cache.get(id, 'blockstates')));
- // --- SWEEP 2: Resolve Parts & Fetch Models ---
const uniqueModelIds = new Set();
const parsedWorlds = new Map();
@@ -176,7 +159,6 @@ export class Engine {
}
await Promise.all(Array.from(uniqueModelIds).map(id => this.cache.get(id, 'models')));
- // --- SWEEP 3: Pool Instances & Fetch Textures ---
const textureTasks = [];
const worldPools = new Map();
@@ -209,7 +191,6 @@ export class Engine {
for (const face of Object.values(el.faces || {})) {
const texPath = blockModel.resolveTexture(face.texture);
if (texPath) {
- // Synchronously allocates UV, asynchronously fetches image
textureTasks.push(this.cache.get(texPath, 'textures', 'png'));
}
}
@@ -221,7 +202,6 @@ export class Engine {
await Promise.all(textureTasks);
this.updateAtlasTexture();
- // --- SWEEP 4: Bake Geometry ---
for (const world of uniqueWorlds) {
const newMeshes = [];
for (const pool of worldPools.get(world).values()) {
@@ -295,23 +275,39 @@ export class Engine {
render() {
const gl = this.gl;
- gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
- gl.scissor(0, 0, gl.canvas.width, gl.canvas.height);
- gl.clearColor(0.0, 0.0, 0.0, 0.0);
- gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(this.program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture);
gl.uniform1i(this.uniforms.texture, 0);
+ // High-Performance Multi-Target Loop
for (const diorama of this.dioramas) {
- const rect = diorama.element.getBoundingClientRect();
- if (rect.bottom < 0 || rect.top > gl.canvas.height || rect.right < 0 || rect.left > gl.canvas.width) continue;
+ const rect = diorama.canvas.getBoundingClientRect();
- const bottom = gl.canvas.height - rect.bottom;
- gl.viewport(rect.left, bottom, rect.width, rect.height);
- gl.scissor(rect.left, bottom, rect.width, rect.height);
+ // Fast Frustum Culling via native DOM properties
+ if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) continue;
+
+ // Sync layout device-pixel ratios explicitly to prevent jagged texturing
+ const dpr = window.devicePixelRatio || 1;
+ const targetW = Math.floor(rect.width * dpr);
+ const targetH = Math.floor(rect.height * dpr);
+
+ // Guard: Only update diorama canvas internal buffers if physical dimensions shifted
+ if (diorama.canvas.width !== targetW || diorama.canvas.height !== targetH) {
+ diorama.canvas.width = targetW;
+ diorama.canvas.height = targetH;
+ }
+
+ // Apply size changes to hidden canvas ONLY when growing
+ if (this.canvas.width < targetW || this.canvas.height < targetH) {
+ this.canvas.width = targetW;
+ this.canvas.height = targetH;
+ }
+
+ gl.viewport(0, 0, targetW, targetH);
+ gl.clearColor(0.0, 0.0, 0.0, 0.0);
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const aspect = rect.width / rect.height;
const viewProj = diorama.updateMatrices(aspect);
@@ -325,6 +321,13 @@ export class Engine {
gl.bindVertexArray(mesh.vao);
gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount);
}
+
+ diorama.ctx2d.clearRect(0, 0, targetW, targetH);
+ diorama.ctx2d.drawImage(
+ this.canvas,
+ 0, 0, targetW, targetH, // Source sub-rect coordinates from WebGL
+ 0, 0, targetW, targetH // Destination coordinates on 2D surface
+ );
}
}
}
\ No newline at end of file
diff --git a/index.html b/index.html
index e9337d80..f8558483 100644
--- a/index.html
+++ b/index.html
@@ -16,14 +16,30 @@
}
.figure {
+ position: relative;
width: 100%;
height: 20em;
}
+
+ .figure > canvas {
+ display: block;
+ width: 100%;
+ height: 100%;
+ }
Visualizer
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper ut mauris sed
+ tempor. Nunc
+ vehicula tempor purus, non vestibulum libero ornare non. Morbi fringilla sapien diam, sed
+ rhoncus lacus egestas
+ vel.
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper ut mauris sed
tempor. Nunc
@@ -42,6 +58,46 @@
Mauris ullamcorper accumsan dui vel pulvinar.
+
+ Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
+ aliquam tellus,
+ vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
+ ligula eu maximus.
+ Mauris ullamcorper accumsan dui vel pulvinar.
+
+
+
+ Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
+ aliquam tellus,
+ vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
+ ligula eu maximus.
+ Mauris ullamcorper accumsan dui vel pulvinar.
+
+
+
+ Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
+ aliquam tellus,
+ vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
+ ligula eu maximus.
+ Mauris ullamcorper accumsan dui vel pulvinar.
+
+
+
+ Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
+ aliquam tellus,
+ vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
+ ligula eu maximus.
+ Mauris ullamcorper accumsan dui vel pulvinar.
+
+
+
+ Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
+ aliquam tellus,
+ vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
+ ligula eu maximus.
+ Mauris ullamcorper accumsan dui vel pulvinar.
+
+
@@ -53,6 +109,18 @@
dis parturient montes, nascetur ridiculus mus.
+
+ Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque
+ penatibus et magnis
+ dis parturient montes, nascetur ridiculus mus.
+
+
+
+ Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque
+ penatibus et magnis
+ dis parturient montes, nascetur ridiculus mus.
+
+