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
|
||||
- Scrub, zoom, pan, change playback speed, and frame-step footage
|
||||
- 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
|
||||
- Preserve sessions across refreshes with IndexedDB
|
||||
|
||||
|
||||
+2
-332
@@ -281,29 +281,6 @@
|
||||
}
|
||||
.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 */
|
||||
.mag-selection {
|
||||
position: absolute; border: 2px solid var(--accent-primary); background: rgba(0,110,215,0.12);
|
||||
@@ -1873,26 +1850,11 @@
|
||||
cell.appendChild(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);
|
||||
|
||||
cell.addEventListener('click', (e) => {
|
||||
if (magActive) 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);
|
||||
if (z.scale > 1) return;
|
||||
const wasExpanded = cell.classList.contains('expanded');
|
||||
@@ -2138,10 +2100,9 @@
|
||||
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) {
|
||||
const _ds = dewarpState.get(ch.name);
|
||||
if (!_ds || !_ds.active) applyZoom(ch.cellEl);
|
||||
applyZoom(ch.cellEl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2505,14 +2466,6 @@
|
||||
if (!cell) return;
|
||||
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 rect = cell.getBoundingClientRect();
|
||||
const mx = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
@@ -2533,15 +2486,6 @@
|
||||
cameraGrid.addEventListener('dblclick', e => {
|
||||
const cell = e.target.closest('.cam-cell');
|
||||
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);
|
||||
if (z.scale > 1) {
|
||||
e.stopPropagation();
|
||||
@@ -2557,24 +2501,9 @@
|
||||
cameraGrid.addEventListener('mousedown', e => {
|
||||
if (magActive) return; // magnifier 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');
|
||||
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);
|
||||
if (z.scale <= 1) return;
|
||||
|
||||
@@ -2589,18 +2518,6 @@
|
||||
});
|
||||
|
||||
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;
|
||||
const z = getZoom(panCell);
|
||||
const rect = panCell.getBoundingClientRect();
|
||||
@@ -2614,10 +2531,6 @@
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (dewarpPanCell) {
|
||||
dewarpPanCell.classList.remove('panning');
|
||||
dewarpPanCell = null;
|
||||
}
|
||||
if (panCell) {
|
||||
panCell.classList.remove('panning');
|
||||
panCell = null;
|
||||
@@ -2709,237 +2622,6 @@
|
||||
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) ───
|
||||
|
||||
let dragSrcCell = null;
|
||||
@@ -3817,18 +3499,6 @@
|
||||
tlZoom = 1;
|
||||
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
|
||||
cameraGrid.querySelectorAll('video').forEach(v => destroyVideoEl(v));
|
||||
cameraGrid.innerHTML = '';
|
||||
|
||||
Reference in New Issue
Block a user