multi target renderer instead of background scissor
This commit is contained in:
63
diorama.js
63
diorama.js
@@ -4,6 +4,9 @@ import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js';
|
|||||||
export class Diorama {
|
export class Diorama {
|
||||||
constructor(elementId, world, requestRenderCallback) {
|
constructor(elementId, world, requestRenderCallback) {
|
||||||
this.element = document.getElementById(elementId);
|
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.world = world;
|
||||||
this.requestRender = requestRenderCallback;
|
this.requestRender = requestRenderCallback;
|
||||||
|
|
||||||
@@ -32,7 +35,9 @@ export class Diorama {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initUI() {
|
initUI() {
|
||||||
|
// Because the element is a canvas itself, buttons are appended to its parent container
|
||||||
this.element.style.position = "relative";
|
this.element.style.position = "relative";
|
||||||
|
|
||||||
this.resetBtn = document.createElement('button');
|
this.resetBtn = document.createElement('button');
|
||||||
this.resetBtn.textContent = 'Reset View';
|
this.resetBtn.textContent = 'Reset View';
|
||||||
this.resetBtn.style.cssText = `
|
this.resetBtn.style.cssText = `
|
||||||
@@ -48,8 +53,10 @@ export class Diorama {
|
|||||||
this.loadState();
|
this.loadState();
|
||||||
this.resetBtn.style.display = 'none';
|
this.resetBtn.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
this.element.appendChild(this.resetBtn);
|
this.element.appendChild(this.resetBtn);
|
||||||
|
|
||||||
|
this.touchTimer = null;
|
||||||
|
this.touchTimedOut = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveState() {
|
saveState() {
|
||||||
@@ -128,8 +135,6 @@ export class Diorama {
|
|||||||
return this.viewProjMatrix;
|
return this.viewProjMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- INTERACTION MATH ---
|
|
||||||
|
|
||||||
orbit(dx, dy) {
|
orbit(dx, dy) {
|
||||||
this.theta -= dx * 0.4;
|
this.theta -= dx * 0.4;
|
||||||
this.phi += dy * 0.4;
|
this.phi += dy * 0.4;
|
||||||
@@ -147,19 +152,23 @@ export class Diorama {
|
|||||||
const upY = Math.cos(p);
|
const upY = Math.cos(p);
|
||||||
const upZ = -Math.sin(p) * Math.cos(t);
|
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[0] += (-rightX * dx + upX * dy) * panSpeed;
|
||||||
this.target[1] += (upY * dy) * panSpeed;
|
this.target[1] += (upY * dy) * panSpeed;
|
||||||
this.target[2] += (-rightZ * dx + upZ * dy) * panSpeed;
|
this.target[2] += (-rightZ * dx + upZ * dy) * panSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
zoom(delta) {
|
zoomRatio(ratio) {
|
||||||
this.radius += delta * 0.05;
|
this.radius *= ratio;
|
||||||
this.radius = Math.max(1, Math.min(50, this.radius));
|
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() {
|
attachEvents() {
|
||||||
this.element.addEventListener('contextmenu', e => e.preventDefault());
|
this.element.addEventListener('contextmenu', e => e.preventDefault());
|
||||||
@@ -199,20 +208,27 @@ export class Diorama {
|
|||||||
|
|
||||||
if (this.dragButton === 0) this.orbit(dx, dy);
|
if (this.dragButton === 0) this.orbit(dx, dy);
|
||||||
else if (this.dragButton === 1) this.pan(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();
|
notifyChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- TOUCH EVENTS ---
|
// --- TOUCH EVENTS ---
|
||||||
this.element.addEventListener('touchstart', (e) => {
|
this.element.addEventListener('touchstart', (e) => {
|
||||||
e.preventDefault();
|
if (this.touchTimer) clearTimeout(this.touchTimer);
|
||||||
this.isDragging = true;
|
this.touchTimedOut = false;
|
||||||
|
|
||||||
if (e.touches.length === 1) {
|
if (e.touches.length === 1) {
|
||||||
this.touchMode = 'orbit';
|
this.touchTimer = setTimeout(() => {
|
||||||
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
|
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) {
|
} else if (e.touches.length >= 2) {
|
||||||
|
this.touchTimedOut = true;
|
||||||
|
this.isDragging = true;
|
||||||
this.touchMode = 'pan-zoom';
|
this.touchMode = 'pan-zoom';
|
||||||
const t1 = e.touches[0], t2 = e.touches[1];
|
const t1 = e.touches[0], t2 = e.touches[1];
|
||||||
this.lastMouse = {
|
this.lastMouse = {
|
||||||
@@ -221,11 +237,20 @@ export class Diorama {
|
|||||||
};
|
};
|
||||||
this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
|
this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
|
||||||
}
|
}
|
||||||
}, {passive: false});
|
}, {passive: true});
|
||||||
|
|
||||||
this.element.addEventListener('touchmove', (e) => {
|
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;
|
if (!this.isDragging) return;
|
||||||
e.preventDefault();
|
if (e.cancelable) e.preventDefault();
|
||||||
|
|
||||||
if (this.touchMode === 'orbit' && e.touches.length === 1) {
|
if (this.touchMode === 'orbit' && e.touches.length === 1) {
|
||||||
const dx = e.touches[0].clientX - this.lastMouse.x;
|
const dx = e.touches[0].clientX - this.lastMouse.x;
|
||||||
@@ -242,20 +267,26 @@ export class Diorama {
|
|||||||
|
|
||||||
const dx = cx - this.lastMouse.x;
|
const dx = cx - this.lastMouse.x;
|
||||||
const dy = cy - this.lastMouse.y;
|
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.lastMouse = {x: cx, y: cy};
|
||||||
this.lastPinchDist = dist;
|
this.lastPinchDist = dist;
|
||||||
|
|
||||||
this.pan(dx, dy);
|
this.pan(dx, dy);
|
||||||
this.zoom(-dDist);
|
this.zoomRatio(zRatio);
|
||||||
notifyChange();
|
notifyChange();
|
||||||
}
|
}
|
||||||
}, {passive: false});
|
}, {passive: false});
|
||||||
|
|
||||||
const onTouchEnd = (e) => {
|
const onTouchEnd = (e) => {
|
||||||
|
if (this.touchTimer) {
|
||||||
|
clearTimeout(this.touchTimer);
|
||||||
|
this.touchTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.touches.length === 0) {
|
if (e.touches.length === 0) {
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
|
this.touchMode = '';
|
||||||
} else if (e.touches.length === 1) {
|
} else if (e.touches.length === 1) {
|
||||||
this.touchMode = 'orbit';
|
this.touchMode = 'orbit';
|
||||||
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
|
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
|
||||||
|
|||||||
75
engine.js
75
engine.js
@@ -62,20 +62,15 @@ function compileShader(gl, type, src) {
|
|||||||
|
|
||||||
export class Engine {
|
export class Engine {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// One solitary hidden canvas to drive all WebGL multi-target logic
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
this.canvas.style.position = 'fixed';
|
this.canvas.style.display = 'none';
|
||||||
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';
|
|
||||||
document.body.appendChild(this.canvas);
|
document.body.appendChild(this.canvas);
|
||||||
|
|
||||||
this.gl = this.canvas.getContext('webgl2', {antialias: true, alpha: true});
|
this.gl = this.canvas.getContext('webgl2', {antialias: true, alpha: true});
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
gl.enable(gl.DEPTH_TEST);
|
gl.enable(gl.DEPTH_TEST);
|
||||||
gl.enable(gl.CULL_FACE);
|
gl.enable(gl.CULL_FACE);
|
||||||
gl.enable(gl.SCISSOR_TEST);
|
|
||||||
|
|
||||||
const vs = compileShader(gl, gl.VERTEX_SHADER, VS_SRC);
|
const vs = compileShader(gl, gl.VERTEX_SHADER, VS_SRC);
|
||||||
const fs = compileShader(gl, gl.FRAGMENT_SHADER, FS_SRC);
|
const fs = compileShader(gl, gl.FRAGMENT_SHADER, FS_SRC);
|
||||||
@@ -93,7 +88,6 @@ export class Engine {
|
|||||||
|
|
||||||
this.atlasTexture = gl.createTexture();
|
this.atlasTexture = gl.createTexture();
|
||||||
|
|
||||||
// --- Initialize the new Resource Pipeline ---
|
|
||||||
this.cache = new Cache('assets');
|
this.cache = new Cache('assets');
|
||||||
this.atlas = new TextureHandler(256);
|
this.atlas = new TextureHandler(256);
|
||||||
this.cache.register('blockstates', new BlockstateHandler());
|
this.cache.register('blockstates', new BlockstateHandler());
|
||||||
@@ -103,22 +97,18 @@ export class Engine {
|
|||||||
this.dioramas = [];
|
this.dioramas = [];
|
||||||
this.worldMeshes = new Map();
|
this.worldMeshes = new Map();
|
||||||
this.renderRequested = false;
|
this.renderRequested = false;
|
||||||
|
|
||||||
this.observedWorlds = new Set();
|
this.observedWorlds = new Set();
|
||||||
this.updateRequested = false;
|
this.updateRequested = false;
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => this.requestRender());
|
||||||
this.resize();
|
|
||||||
this.requestRender();
|
// to make sure offscreen dioramas are rendered as soon as they become visible
|
||||||
});
|
window.addEventListener('scroll', () => this.requestRender());
|
||||||
window.addEventListener('scroll', () => this.requestRender(), {passive: true});
|
|
||||||
this.resize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestUpdate() {
|
requestUpdate() {
|
||||||
if (!this.updateRequested) {
|
if (!this.updateRequested) {
|
||||||
this.updateRequested = true;
|
this.updateRequested = true;
|
||||||
// Add 'async' here and 'await' updateAll
|
|
||||||
Promise.resolve().then(async () => {
|
Promise.resolve().then(async () => {
|
||||||
await this.updateAll();
|
await this.updateAll();
|
||||||
this.updateRequested = false;
|
this.updateRequested = false;
|
||||||
@@ -136,14 +126,9 @@ export class Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
|
||||||
this.canvas.width = window.innerWidth;
|
|
||||||
this.canvas.height = window.innerHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
addDiorama(diorama) {
|
addDiorama(diorama) {
|
||||||
this.dioramas.push(diorama);
|
this.dioramas.push(diorama);
|
||||||
if (!this.observedWorlds.has(diorama.worlds)) {
|
if (!this.observedWorlds.has(diorama.world)) {
|
||||||
this.observedWorlds.add(diorama.world);
|
this.observedWorlds.add(diorama.world);
|
||||||
diorama.world.subscribe(() => this.requestUpdate());
|
diorama.world.subscribe(() => this.requestUpdate());
|
||||||
}
|
}
|
||||||
@@ -153,14 +138,12 @@ export class Engine {
|
|||||||
const uniqueWorlds = new Set(this.dioramas.map(d => d.world));
|
const uniqueWorlds = new Set(this.dioramas.map(d => d.world));
|
||||||
if (uniqueWorlds.size === 0) return;
|
if (uniqueWorlds.size === 0) return;
|
||||||
|
|
||||||
// --- SWEEP 1: Collect & Fetch Blockstates ---
|
|
||||||
const uniqueBlockIds = new Set();
|
const uniqueBlockIds = new Set();
|
||||||
for (const world of uniqueWorlds) {
|
for (const world of uniqueWorlds) {
|
||||||
for (const block of world.blocks.values()) uniqueBlockIds.add(block.id);
|
for (const block of world.blocks.values()) uniqueBlockIds.add(block.id);
|
||||||
}
|
}
|
||||||
await Promise.all(Array.from(uniqueBlockIds).map(id => this.cache.get(id, 'blockstates')));
|
await Promise.all(Array.from(uniqueBlockIds).map(id => this.cache.get(id, 'blockstates')));
|
||||||
|
|
||||||
// --- SWEEP 2: Resolve Parts & Fetch Models ---
|
|
||||||
const uniqueModelIds = new Set();
|
const uniqueModelIds = new Set();
|
||||||
const parsedWorlds = new Map();
|
const parsedWorlds = new Map();
|
||||||
|
|
||||||
@@ -176,7 +159,6 @@ export class Engine {
|
|||||||
}
|
}
|
||||||
await Promise.all(Array.from(uniqueModelIds).map(id => this.cache.get(id, 'models')));
|
await Promise.all(Array.from(uniqueModelIds).map(id => this.cache.get(id, 'models')));
|
||||||
|
|
||||||
// --- SWEEP 3: Pool Instances & Fetch Textures ---
|
|
||||||
const textureTasks = [];
|
const textureTasks = [];
|
||||||
const worldPools = new Map();
|
const worldPools = new Map();
|
||||||
|
|
||||||
@@ -209,7 +191,6 @@ export class Engine {
|
|||||||
for (const face of Object.values(el.faces || {})) {
|
for (const face of Object.values(el.faces || {})) {
|
||||||
const texPath = blockModel.resolveTexture(face.texture);
|
const texPath = blockModel.resolveTexture(face.texture);
|
||||||
if (texPath) {
|
if (texPath) {
|
||||||
// Synchronously allocates UV, asynchronously fetches image
|
|
||||||
textureTasks.push(this.cache.get(texPath, 'textures', 'png'));
|
textureTasks.push(this.cache.get(texPath, 'textures', 'png'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +202,6 @@ export class Engine {
|
|||||||
await Promise.all(textureTasks);
|
await Promise.all(textureTasks);
|
||||||
this.updateAtlasTexture();
|
this.updateAtlasTexture();
|
||||||
|
|
||||||
// --- SWEEP 4: Bake Geometry ---
|
|
||||||
for (const world of uniqueWorlds) {
|
for (const world of uniqueWorlds) {
|
||||||
const newMeshes = [];
|
const newMeshes = [];
|
||||||
for (const pool of worldPools.get(world).values()) {
|
for (const pool of worldPools.get(world).values()) {
|
||||||
@@ -295,23 +275,39 @@ export class Engine {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const gl = this.gl;
|
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.useProgram(this.program);
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture);
|
||||||
gl.uniform1i(this.uniforms.texture, 0);
|
gl.uniform1i(this.uniforms.texture, 0);
|
||||||
|
|
||||||
|
// High-Performance Multi-Target Loop
|
||||||
for (const diorama of this.dioramas) {
|
for (const diorama of this.dioramas) {
|
||||||
const rect = diorama.element.getBoundingClientRect();
|
const rect = diorama.canvas.getBoundingClientRect();
|
||||||
if (rect.bottom < 0 || rect.top > gl.canvas.height || rect.right < 0 || rect.left > gl.canvas.width) continue;
|
|
||||||
|
|
||||||
const bottom = gl.canvas.height - rect.bottom;
|
// Fast Frustum Culling via native DOM properties
|
||||||
gl.viewport(rect.left, bottom, rect.width, rect.height);
|
if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) continue;
|
||||||
gl.scissor(rect.left, bottom, rect.width, rect.height);
|
|
||||||
|
// 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 aspect = rect.width / rect.height;
|
||||||
const viewProj = diorama.updateMatrices(aspect);
|
const viewProj = diorama.updateMatrices(aspect);
|
||||||
@@ -325,6 +321,13 @@ export class Engine {
|
|||||||
gl.bindVertexArray(mesh.vao);
|
gl.bindVertexArray(mesh.vao);
|
||||||
gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount);
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
68
index.html
68
index.html
@@ -16,14 +16,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.figure {
|
.figure {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 20em;
|
height: 20em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.figure > canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<h1>Visualizer</h1>
|
<h1>Visualizer</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper ut mauris sed
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper ut mauris sed
|
||||||
tempor. Nunc
|
tempor. Nunc
|
||||||
@@ -42,6 +58,46 @@
|
|||||||
Mauris ullamcorper accumsan dui vel pulvinar.
|
Mauris ullamcorper accumsan dui vel pulvinar.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr;">
|
||||||
<div class="figure" id="demo-2"></div>
|
<div class="figure" id="demo-2"></div>
|
||||||
<div class="figure" id="demo-3"></div>
|
<div class="figure" id="demo-3"></div>
|
||||||
@@ -53,6 +109,18 @@
|
|||||||
dis parturient montes, nascetur ridiculus mus.
|
dis parturient montes, nascetur ridiculus mus.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque
|
||||||
|
penatibus et magnis
|
||||||
|
dis parturient montes, nascetur ridiculus mus.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque
|
||||||
|
penatibus et magnis
|
||||||
|
dis parturient montes, nascetur ridiculus mus.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div id="atlas-container"
|
<div id="atlas-container"
|
||||||
style="position: fixed; left: 0; top: 0; border: 1px solid white;"></div>
|
style="position: fixed; left: 0; top: 0; border: 1px solid white;"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user