Remove fisheye dewarp feature
Remove the per-camera fisheye dewarp tool: the WebGL dewarp module (shaders, init/render/toggle, per-channel dewarpState), the Dewarp button, its CSS, the dewarp branches interleaved into the shared wheel/dblclick/mousedown/mousemove/mouseup handlers, and the WebGL context cleanup in newSession(). Normal digital zoom/pan, drag-reorder, magnifier, and expand are unaffected. Also drop the dewarp mention from the README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ The app can run in two modes:
|
|||||||
- Synchronize multiple camera segments on a shared timeline
|
- Synchronize multiple camera segments on a shared timeline
|
||||||
- Scrub, zoom, pan, change playback speed, and frame-step footage
|
- Scrub, zoom, pan, change playback speed, and frame-step footage
|
||||||
- Reorder, hide/show, expand, and manually lay out camera tiles
|
- Reorder, hide/show, expand, and manually lay out camera tiles
|
||||||
- Use region zoom, scroll zoom, and fisheye dewarp tools
|
- Use region zoom and scroll zoom tools
|
||||||
- Verify signed exports offline and optionally confirm certificates with Alta's cloud verification endpoint
|
- Verify signed exports offline and optionally confirm certificates with Alta's cloud verification endpoint
|
||||||
- Preserve sessions across refreshes with IndexedDB
|
- Preserve sessions across refreshes with IndexedDB
|
||||||
|
|
||||||
|
|||||||
+2
-332
@@ -281,29 +281,6 @@
|
|||||||
}
|
}
|
||||||
.cam-cell .zoom-indicator.visible { display: block; }
|
.cam-cell .zoom-indicator.visible { display: block; }
|
||||||
|
|
||||||
/* Dewarp button & canvas */
|
|
||||||
.dewarp-btn {
|
|
||||||
position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
|
|
||||||
background: rgba(0,0,0,0.75); border: 1px solid var(--border-default); color: var(--text-secondary);
|
|
||||||
padding: 4px 12px; border-radius: 4px; font-size: 11px; font-weight: 600;
|
|
||||||
cursor: pointer; z-index: 5; opacity: 0; transition: opacity 0.15s;
|
|
||||||
display: flex; align-items: center; gap: 5px; letter-spacing: 0.3px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.cam-cell:hover .dewarp-btn { opacity: 1; pointer-events: auto; }
|
|
||||||
.dewarp-btn:hover { background: rgba(0,0,0,0.9); color: var(--text-primary); border-color: var(--accent-primary); box-shadow: var(--shadow-focus); }
|
|
||||||
.dewarp-btn.active { background: var(--accent-primary); color: #fff; border-color: var(--accent-primary); opacity: 1; pointer-events: auto; }
|
|
||||||
.dewarp-btn svg { width: 14px; height: 14px; fill: currentColor; }
|
|
||||||
.dewarp-canvas {
|
|
||||||
position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.dewarp-canvas.active { display: block; }
|
|
||||||
.cam-cell.dewarped video { visibility: hidden; }
|
|
||||||
.cam-cell.dewarped .no-signal { z-index: 0; }
|
|
||||||
.cam-cell.dewarped { cursor: grab; }
|
|
||||||
.cam-cell.dewarped.panning { cursor: grabbing; }
|
|
||||||
|
|
||||||
/* Magnifier selection rectangle */
|
/* Magnifier selection rectangle */
|
||||||
.mag-selection {
|
.mag-selection {
|
||||||
position: absolute; border: 2px solid var(--accent-primary); background: rgba(0,110,215,0.12);
|
position: absolute; border: 2px solid var(--accent-primary); background: rgba(0,110,215,0.12);
|
||||||
@@ -1873,26 +1850,11 @@
|
|||||||
cell.appendChild(segInfo);
|
cell.appendChild(segInfo);
|
||||||
ch._segInfoEl = segInfo;
|
ch._segInfoEl = segInfo;
|
||||||
|
|
||||||
// Dewarp button
|
|
||||||
const dewarpBtn = document.createElement('button');
|
|
||||||
dewarpBtn.className = 'dewarp-btn';
|
|
||||||
dewarpBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg> Dewarp';
|
|
||||||
dewarpBtn.title = 'Toggle fisheye dewarp';
|
|
||||||
dewarpBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
toggleDewarp(cell, ch.name);
|
|
||||||
});
|
|
||||||
cell.appendChild(dewarpBtn);
|
|
||||||
|
|
||||||
setupDragHandle(cell);
|
setupDragHandle(cell);
|
||||||
|
|
||||||
cell.addEventListener('click', (e) => {
|
cell.addEventListener('click', (e) => {
|
||||||
if (magActive) return;
|
if (magActive) return;
|
||||||
if (e.target.closest('.drag-handle')) return;
|
if (e.target.closest('.drag-handle')) return;
|
||||||
if (e.target.closest('.dewarp-btn')) return;
|
|
||||||
// Don't expand when dewarped (click is used for pan)
|
|
||||||
const _ds = dewarpState.get(ch.name);
|
|
||||||
if (_ds && _ds.active) return;
|
|
||||||
const z = getZoom(cell);
|
const z = getZoom(cell);
|
||||||
if (z.scale > 1) return;
|
if (z.scale > 1) return;
|
||||||
const wasExpanded = cell.classList.contains('expanded');
|
const wasExpanded = cell.classList.contains('expanded');
|
||||||
@@ -2138,10 +2100,9 @@
|
|||||||
if (isPlaying) seg.videoEl.play();
|
if (isPlaying) seg.videoEl.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Re-apply zoom to newly visible video (skip if dewarped)
|
// Re-apply zoom to newly visible video
|
||||||
if (ch.cellEl) {
|
if (ch.cellEl) {
|
||||||
const _ds = dewarpState.get(ch.name);
|
applyZoom(ch.cellEl);
|
||||||
if (!_ds || !_ds.active) applyZoom(ch.cellEl);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2505,14 +2466,6 @@
|
|||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Dewarp zoom: adjust FOV
|
|
||||||
const ds = getDewarpForCell(cell);
|
|
||||||
if (ds && ds.active) {
|
|
||||||
const delta = e.deltaY > 0 ? 0.05 : -0.05;
|
|
||||||
ds.fov = Math.max(0.2, Math.min(Math.PI, ds.fov + delta));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const z = getZoom(cell);
|
const z = getZoom(cell);
|
||||||
const rect = cell.getBoundingClientRect();
|
const rect = cell.getBoundingClientRect();
|
||||||
const mx = ((e.clientX - rect.left) / rect.width) * 100;
|
const mx = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
@@ -2533,15 +2486,6 @@
|
|||||||
cameraGrid.addEventListener('dblclick', e => {
|
cameraGrid.addEventListener('dblclick', e => {
|
||||||
const cell = e.target.closest('.cam-cell');
|
const cell = e.target.closest('.cam-cell');
|
||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
// Dewarp: reset view on double-click
|
|
||||||
const ds = getDewarpForCell(cell);
|
|
||||||
if (ds && ds.active) {
|
|
||||||
e.stopPropagation();
|
|
||||||
ds.yaw = 0;
|
|
||||||
ds.pitch = 0;
|
|
||||||
ds.fov = Math.PI * 0.5;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const z = getZoom(cell);
|
const z = getZoom(cell);
|
||||||
if (z.scale > 1) {
|
if (z.scale > 1) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -2557,24 +2501,9 @@
|
|||||||
cameraGrid.addEventListener('mousedown', e => {
|
cameraGrid.addEventListener('mousedown', e => {
|
||||||
if (magActive) return; // magnifier takes priority
|
if (magActive) return; // magnifier takes priority
|
||||||
if (e.target.closest('.drag-handle')) return; // drag handle takes priority
|
if (e.target.closest('.drag-handle')) return; // drag handle takes priority
|
||||||
if (e.target.closest('.dewarp-btn')) return; // dewarp button takes priority
|
|
||||||
const cell = e.target.closest('.cam-cell');
|
const cell = e.target.closest('.cam-cell');
|
||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
|
|
||||||
// Dewarp pan
|
|
||||||
const ds = getDewarpForCell(cell);
|
|
||||||
if (ds && ds.active) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
dewarpPanCell = cell;
|
|
||||||
dewarpPanStartX = e.clientX;
|
|
||||||
dewarpPanStartY = e.clientY;
|
|
||||||
dewarpPanStartYaw = ds.yaw;
|
|
||||||
dewarpPanStartPitch = ds.pitch;
|
|
||||||
cell.classList.add('panning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const z = getZoom(cell);
|
const z = getZoom(cell);
|
||||||
if (z.scale <= 1) return;
|
if (z.scale <= 1) return;
|
||||||
|
|
||||||
@@ -2589,18 +2518,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mousemove', e => {
|
document.addEventListener('mousemove', e => {
|
||||||
// Dewarp pan
|
|
||||||
if (dewarpPanCell) {
|
|
||||||
const ds = getDewarpForCell(dewarpPanCell);
|
|
||||||
if (ds) {
|
|
||||||
const rect = dewarpPanCell.getBoundingClientRect();
|
|
||||||
const sensitivity = ds.fov / rect.width;
|
|
||||||
ds.yaw = dewarpPanStartYaw - (e.clientX - dewarpPanStartX) * sensitivity;
|
|
||||||
ds.pitch = Math.max(-Math.PI * 0.45, Math.min(Math.PI * 0.45,
|
|
||||||
dewarpPanStartPitch + (e.clientY - dewarpPanStartY) * sensitivity));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!panCell) return;
|
if (!panCell) return;
|
||||||
const z = getZoom(panCell);
|
const z = getZoom(panCell);
|
||||||
const rect = panCell.getBoundingClientRect();
|
const rect = panCell.getBoundingClientRect();
|
||||||
@@ -2614,10 +2531,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => {
|
||||||
if (dewarpPanCell) {
|
|
||||||
dewarpPanCell.classList.remove('panning');
|
|
||||||
dewarpPanCell = null;
|
|
||||||
}
|
|
||||||
if (panCell) {
|
if (panCell) {
|
||||||
panCell.classList.remove('panning');
|
panCell.classList.remove('panning');
|
||||||
panCell = null;
|
panCell = null;
|
||||||
@@ -2709,237 +2622,6 @@
|
|||||||
toggleMagnifier();
|
toggleMagnifier();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Fisheye Dewarp (WebGL) ───
|
|
||||||
|
|
||||||
const dewarpState = new Map(); // keyed by channel name
|
|
||||||
|
|
||||||
function getDewarpState(chName) {
|
|
||||||
if (!dewarpState.has(chName)) {
|
|
||||||
dewarpState.set(chName, {
|
|
||||||
active: false,
|
|
||||||
canvas: null,
|
|
||||||
gl: null,
|
|
||||||
program: null,
|
|
||||||
texture: null,
|
|
||||||
animFrameId: null,
|
|
||||||
// View: yaw/pitch in radians, fov in radians
|
|
||||||
yaw: 0,
|
|
||||||
pitch: 0,
|
|
||||||
fov: Math.PI * 0.5, // 90° default FOV
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return dewarpState.get(chName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEWARP_VS = `
|
|
||||||
attribute vec2 aPos;
|
|
||||||
varying vec2 vUv;
|
|
||||||
void main() {
|
|
||||||
vUv = aPos * 0.5 + 0.5;
|
|
||||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Equidistant fisheye → rectilinear with pan/tilt/zoom
|
|
||||||
// Maps each output pixel to a direction, then finds the corresponding
|
|
||||||
// point on the fisheye source image.
|
|
||||||
const DEWARP_FS = `
|
|
||||||
precision highp float;
|
|
||||||
varying vec2 vUv;
|
|
||||||
uniform sampler2D uTex;
|
|
||||||
uniform float uYaw; // horizontal pan (radians)
|
|
||||||
uniform float uPitch; // vertical tilt (radians)
|
|
||||||
uniform float uFov; // field of view (radians)
|
|
||||||
uniform float uAspect; // video aspect ratio (w/h)
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// Map UV to normalized device coords centered at 0
|
|
||||||
vec2 ndc = (vUv - 0.5) * 2.0;
|
|
||||||
ndc.x *= uAspect;
|
|
||||||
|
|
||||||
// Convert screen point to 3D ray direction (rectilinear)
|
|
||||||
float halfFov = uFov * 0.5;
|
|
||||||
float d = 1.0 / tan(halfFov);
|
|
||||||
vec3 ray = normalize(vec3(ndc.x, ndc.y, d));
|
|
||||||
|
|
||||||
// Rotate ray by pitch (around X axis)
|
|
||||||
float cp = cos(uPitch), sp = sin(uPitch);
|
|
||||||
ray = vec3(ray.x, cp * ray.y - sp * ray.z, sp * ray.y + cp * ray.z);
|
|
||||||
|
|
||||||
// Rotate ray by yaw (around Y axis)
|
|
||||||
float cy = cos(uYaw), sy = sin(uYaw);
|
|
||||||
ray = vec3(cy * ray.x + sy * ray.z, ray.y, -sy * ray.x + cy * ray.z);
|
|
||||||
|
|
||||||
// 3D ray → equidistant fisheye coordinates
|
|
||||||
// theta = angle from optical axis (Z+), phi = azimuth
|
|
||||||
float theta = acos(clamp(ray.z, -1.0, 1.0));
|
|
||||||
float phi = atan(ray.y, ray.x);
|
|
||||||
|
|
||||||
// Equidistant projection: r = theta / (pi/2) maps hemisphere to unit circle
|
|
||||||
// We use pi to support full 360° fisheye
|
|
||||||
float r = theta / 3.14159265;
|
|
||||||
|
|
||||||
// Map to texture coordinates (fisheye circle centered in frame)
|
|
||||||
vec2 fishUv = vec2(0.5) + r * vec2(cos(phi), sin(phi));
|
|
||||||
|
|
||||||
// Account for non-square video: fisheye circle fits the shorter dimension
|
|
||||||
// If video is wider than tall, squeeze x; if taller, squeeze y
|
|
||||||
if (uAspect > 1.0) {
|
|
||||||
fishUv.x = 0.5 + (fishUv.x - 0.5) / uAspect;
|
|
||||||
} else {
|
|
||||||
fishUv.y = 0.5 + (fishUv.y - 0.5) * uAspect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp to valid range and fade edges
|
|
||||||
float dist = length(fishUv - 0.5) * 2.0;
|
|
||||||
if (fishUv.x < 0.0 || fishUv.x > 1.0 || fishUv.y < 0.0 || fishUv.y > 1.0 || dist > 1.0) {
|
|
||||||
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flip Y for WebGL texture coordinates
|
|
||||||
fishUv.y = 1.0 - fishUv.y;
|
|
||||||
gl_FragColor = texture2D(uTex, fishUv);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function compileShader(gl, type, src) {
|
|
||||||
const s = gl.createShader(type);
|
|
||||||
gl.shaderSource(s, src);
|
|
||||||
gl.compileShader(s);
|
|
||||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
|
||||||
avpLog('error', 'Shader compile error: ' + gl.getShaderInfoLog(s));
|
|
||||||
gl.deleteShader(s);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initDewarpGL(cell) {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.className = 'dewarp-canvas';
|
|
||||||
cell.appendChild(canvas);
|
|
||||||
|
|
||||||
const gl = canvas.getContext('webgl', { antialias: false, premultipliedAlpha: false });
|
|
||||||
if (!gl) { avpLog('error', 'WebGL not available'); return null; }
|
|
||||||
|
|
||||||
const vs = compileShader(gl, gl.VERTEX_SHADER, DEWARP_VS);
|
|
||||||
const fs = compileShader(gl, gl.FRAGMENT_SHADER, DEWARP_FS);
|
|
||||||
const prog = gl.createProgram();
|
|
||||||
gl.attachShader(prog, vs);
|
|
||||||
gl.attachShader(prog, fs);
|
|
||||||
gl.linkProgram(prog);
|
|
||||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
||||||
avpLog('error', 'Program link error: ' + gl.getProgramInfoLog(prog));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
gl.useProgram(prog);
|
|
||||||
|
|
||||||
// Full-screen quad
|
|
||||||
const buf = gl.createBuffer();
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
|
|
||||||
const aPos = gl.getAttribLocation(prog, 'aPos');
|
|
||||||
gl.enableVertexAttribArray(aPos);
|
|
||||||
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
|
||||||
|
|
||||||
// Texture
|
|
||||||
const tex = gl.createTexture();
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
||||||
|
|
||||||
return { canvas, gl, program: prog, texture: tex };
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDewarpFrame(chName) {
|
|
||||||
const ds = dewarpState.get(chName);
|
|
||||||
if (!ds || !ds.active) return;
|
|
||||||
|
|
||||||
const ch = channels.get(chName);
|
|
||||||
if (!ch || !ch.cellEl) return;
|
|
||||||
|
|
||||||
const video = ch.cellEl.querySelector('video:not(.hidden-video)');
|
|
||||||
if (!video || video.readyState < 2) {
|
|
||||||
ds.animFrameId = requestAnimationFrame(() => renderDewarpFrame(chName));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { canvas, gl, program, texture } = ds;
|
|
||||||
|
|
||||||
// Resize canvas to match cell
|
|
||||||
const rect = ch.cellEl.getBoundingClientRect();
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
const w = Math.round(rect.width * dpr);
|
|
||||||
const h = Math.round(rect.height * dpr);
|
|
||||||
if (canvas.width !== w || canvas.height !== h) {
|
|
||||||
canvas.width = w;
|
|
||||||
canvas.height = h;
|
|
||||||
gl.viewport(0, 0, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload video frame
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
|
|
||||||
|
|
||||||
// Set uniforms
|
|
||||||
gl.useProgram(program);
|
|
||||||
gl.uniform1f(gl.getUniformLocation(program, 'uYaw'), ds.yaw);
|
|
||||||
gl.uniform1f(gl.getUniformLocation(program, 'uPitch'), ds.pitch);
|
|
||||||
gl.uniform1f(gl.getUniformLocation(program, 'uFov'), ds.fov);
|
|
||||||
|
|
||||||
const videoAspect = video.videoWidth / (video.videoHeight || 1);
|
|
||||||
gl.uniform1f(gl.getUniformLocation(program, 'uAspect'), videoAspect);
|
|
||||||
|
|
||||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
||||||
|
|
||||||
ds.animFrameId = requestAnimationFrame(() => renderDewarpFrame(chName));
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleDewarp(cell, chName) {
|
|
||||||
const ds = getDewarpState(chName);
|
|
||||||
ds.active = !ds.active;
|
|
||||||
|
|
||||||
const btn = cell.querySelector('.dewarp-btn');
|
|
||||||
if (btn) btn.classList.toggle('active', ds.active);
|
|
||||||
|
|
||||||
if (ds.active) {
|
|
||||||
// Initialize WebGL if needed
|
|
||||||
if (!ds.gl) {
|
|
||||||
const init = initDewarpGL(cell);
|
|
||||||
if (!init) { ds.active = false; btn?.classList.remove('active'); return; }
|
|
||||||
Object.assign(ds, init);
|
|
||||||
}
|
|
||||||
ds.canvas.classList.add('active');
|
|
||||||
cell.classList.add('dewarped');
|
|
||||||
// Reset normal CSS zoom — dewarp has its own zoom
|
|
||||||
resetZoom(cell);
|
|
||||||
// Start render loop
|
|
||||||
renderDewarpFrame(chName);
|
|
||||||
} else {
|
|
||||||
// Stop render loop
|
|
||||||
if (ds.animFrameId) { cancelAnimationFrame(ds.animFrameId); ds.animFrameId = null; }
|
|
||||||
if (ds.canvas) ds.canvas.classList.remove('active');
|
|
||||||
cell.classList.remove('dewarped');
|
|
||||||
// Reset dewarp view
|
|
||||||
ds.yaw = 0;
|
|
||||||
ds.pitch = 0;
|
|
||||||
ds.fov = Math.PI * 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Dewarp zoom (scroll wheel) and pan (click+drag) ───
|
|
||||||
let dewarpPanCell = null;
|
|
||||||
let dewarpPanStartX = 0, dewarpPanStartY = 0;
|
|
||||||
let dewarpPanStartYaw = 0, dewarpPanStartPitch = 0;
|
|
||||||
|
|
||||||
function getDewarpForCell(cell) {
|
|
||||||
const chName = findChForCell(cell);
|
|
||||||
return chName ? dewarpState.get(chName) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Camera Reorder (drag & drop) ───
|
// ─── Camera Reorder (drag & drop) ───
|
||||||
|
|
||||||
let dragSrcCell = null;
|
let dragSrcCell = null;
|
||||||
@@ -3817,18 +3499,6 @@
|
|||||||
tlZoom = 1;
|
tlZoom = 1;
|
||||||
tlViewCenter = 0;
|
tlViewCenter = 0;
|
||||||
|
|
||||||
// Cleanup dewarp state
|
|
||||||
for (const [, ds] of dewarpState) {
|
|
||||||
if (ds.animFrameId) cancelAnimationFrame(ds.animFrameId);
|
|
||||||
if (ds.gl) {
|
|
||||||
const ext = ds.gl.getExtension('WEBGL_lose_context');
|
|
||||||
if (ext) ext.loseContext(); // force WebGL context release
|
|
||||||
ds.gl.deleteTexture(ds.texture);
|
|
||||||
ds.gl.deleteProgram(ds.program);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dewarpState.clear();
|
|
||||||
|
|
||||||
// Reset DOM — destroy any remaining video elements first
|
// Reset DOM — destroy any remaining video elements first
|
||||||
cameraGrid.querySelectorAll('video').forEach(v => destroyVideoEl(v));
|
cameraGrid.querySelectorAll('video').forEach(v => destroyVideoEl(v));
|
||||||
cameraGrid.innerHTML = '';
|
cameraGrid.innerHTML = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user