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. +

+