Spaces:
Sleeping
Sleeping
Deploy Deep Analog web app
Browse files- static/index.html +256 -0
static/index.html
CHANGED
|
@@ -221,6 +221,27 @@
|
|
| 221 |
.dl-btn:hover { border-color: var(--accent); }
|
| 222 |
.dl-btn .kind { color: var(--accent); font-size: 11px; letter-spacing: .08em; }
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
/* ---------- privacy modal ---------- */
|
| 225 |
#privacyModal {
|
| 226 |
position: fixed; inset: 0; z-index: 950;
|
|
@@ -548,6 +569,10 @@
|
|
| 548 |
initCompare(cmp);
|
| 549 |
}
|
| 550 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
const dls = document.createElement('div');
|
| 552 |
dls.className = 'downloads';
|
| 553 |
dls.appendChild(dlButton('Download LUT', '.CUBE 33³',
|
|
@@ -590,6 +615,237 @@
|
|
| 590 |
cmp.addEventListener('pointerup', () => { dragging = false; });
|
| 591 |
}
|
| 592 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
// ------------------------------------------------ privacy modal
|
| 594 |
['privacyOpen', 'privacyOpen2'].forEach(id => {
|
| 595 |
const el = $(id);
|
|
|
|
| 221 |
.dl-btn:hover { border-color: var(--accent); }
|
| 222 |
.dl-btn .kind { color: var(--accent); font-size: 11px; letter-spacing: .08em; }
|
| 223 |
|
| 224 |
+
/* ---------- 3D LUT visualization ---------- */
|
| 225 |
+
.viz-wrap { margin-top: 20px; }
|
| 226 |
+
.viz-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; }
|
| 227 |
+
.viz-head .t { font-size: 11.5px; color: var(--text-dim); letter-spacing: .12em; text-transform: uppercase; }
|
| 228 |
+
.viz-head .hint { font-size: 11px; color: var(--text-dim); }
|
| 229 |
+
.viz-canvas {
|
| 230 |
+
width: 100%; height: 420px; display: block;
|
| 231 |
+
border-radius: 10px; border: 1px solid var(--line);
|
| 232 |
+
background: radial-gradient(600px 380px at 50% 45%, #17171c, #0a0a0c 80%);
|
| 233 |
+
cursor: grab; touch-action: none;
|
| 234 |
+
}
|
| 235 |
+
.viz-canvas:active { cursor: grabbing; }
|
| 236 |
+
.viz-controls { display: flex; align-items: center; gap: 14px; margin-top: 12px; }
|
| 237 |
+
.viz-controls .vlbl { font-size: 11px; letter-spacing: .1em; color: var(--text-dim); text-transform: uppercase; white-space: nowrap; }
|
| 238 |
+
.viz-controls input[type=range] { flex: 1; }
|
| 239 |
+
.viz-play {
|
| 240 |
+
background: var(--bg-3); border: 1px solid var(--line); border-radius: 8px;
|
| 241 |
+
color: var(--accent); font-size: 12px; padding: 6px 12px; cursor: pointer;
|
| 242 |
+
}
|
| 243 |
+
.viz-play:hover { border-color: var(--accent); }
|
| 244 |
+
|
| 245 |
/* ---------- privacy modal ---------- */
|
| 246 |
#privacyModal {
|
| 247 |
position: fixed; inset: 0; z-index: 950;
|
|
|
|
| 569 |
initCompare(cmp);
|
| 570 |
}
|
| 571 |
|
| 572 |
+
// 3D visualization of the predicted LUT (both modes)
|
| 573 |
+
try { body.appendChild(makeVizCard(data.cube_lut)); }
|
| 574 |
+
catch (e) { /* WebGL unavailable — skip viz silently */ }
|
| 575 |
+
|
| 576 |
const dls = document.createElement('div');
|
| 577 |
dls.className = 'downloads';
|
| 578 |
dls.appendChild(dlButton('Download LUT', '.CUBE 33³',
|
|
|
|
| 615 |
cmp.addEventListener('pointerup', () => { dragging = false; });
|
| 616 |
}
|
| 617 |
|
| 618 |
+
// ------------------------------------------------ 3D LUT visualization
|
| 619 |
+
// Self-contained WebGL point cloud — no libraries, no external requests.
|
| 620 |
+
// Shows the LUT lattice morphing from identity (perfect cube) to the
|
| 621 |
+
// predicted film transform. Drag to rotate, scroll to zoom.
|
| 622 |
+
|
| 623 |
+
function parseCube(text) {
|
| 624 |
+
let N = 0; const vals = [];
|
| 625 |
+
const lines = text.split('\n');
|
| 626 |
+
for (let i = 0; i < lines.length; i++) {
|
| 627 |
+
const t = lines[i].trim();
|
| 628 |
+
if (!t || t[0] === 'T' || t[0] === 'D') continue; // TITLE / DOMAIN_*
|
| 629 |
+
if (t[0] === 'L') { N = parseInt(t.split(/\s+/)[1], 10); continue; }
|
| 630 |
+
const p = t.split(/\s+/);
|
| 631 |
+
if (p.length === 3) vals.push(+p[0], +p[1], +p[2]);
|
| 632 |
+
}
|
| 633 |
+
return { N, out: new Float32Array(vals) };
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
function makeVizCard(cubeText) {
|
| 637 |
+
const wrap = document.createElement('div');
|
| 638 |
+
wrap.className = 'viz-wrap';
|
| 639 |
+
wrap.innerHTML =
|
| 640 |
+
'<div class="viz-head"><span class="t">Color space — 3D LUT</span>' +
|
| 641 |
+
'<span class="hint">drag to rotate · scroll to zoom</span></div>' +
|
| 642 |
+
'<canvas class="viz-canvas"></canvas>' +
|
| 643 |
+
'<div class="viz-controls">' +
|
| 644 |
+
'<button class="viz-play">❚❚</button>' +
|
| 645 |
+
'<span class="vlbl">Original</span>' +
|
| 646 |
+
'<input type="range" min="0" max="1" step="0.001" value="0">' +
|
| 647 |
+
'<span class="vlbl" style="color:var(--accent)">Film</span></div>';
|
| 648 |
+
|
| 649 |
+
const canvas = wrap.querySelector('canvas');
|
| 650 |
+
const slider = wrap.querySelector('input[type=range]');
|
| 651 |
+
const playBtn = wrap.querySelector('.viz-play');
|
| 652 |
+
|
| 653 |
+
const { N, out } = parseCube(cubeText);
|
| 654 |
+
if (!N || out.length !== N * N * N * 3) throw new Error('bad cube');
|
| 655 |
+
|
| 656 |
+
// identity lattice: red varies fastest (.cube convention)
|
| 657 |
+
const n3 = N * N * N;
|
| 658 |
+
const idn = new Float32Array(n3 * 3);
|
| 659 |
+
for (let i = 0; i < n3; i++) {
|
| 660 |
+
idn[i * 3] = (i % N) / (N - 1);
|
| 661 |
+
idn[i * 3 + 1] = (Math.floor(i / N) % N) / (N - 1);
|
| 662 |
+
idn[i * 3 + 2] = Math.floor(i / (N * N)) / (N - 1);
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
const gl = canvas.getContext('webgl', { antialias: true, alpha: true });
|
| 666 |
+
if (!gl) throw new Error('no webgl');
|
| 667 |
+
|
| 668 |
+
function compile(vs, fs) {
|
| 669 |
+
function sh(type, src) {
|
| 670 |
+
const s = gl.createShader(type);
|
| 671 |
+
gl.shaderSource(s, src); gl.compileShader(s);
|
| 672 |
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
| 673 |
+
throw new Error(gl.getShaderInfoLog(s));
|
| 674 |
+
return s;
|
| 675 |
+
}
|
| 676 |
+
const p = gl.createProgram();
|
| 677 |
+
gl.attachShader(p, sh(gl.VERTEX_SHADER, vs));
|
| 678 |
+
gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs));
|
| 679 |
+
gl.linkProgram(p);
|
| 680 |
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
| 681 |
+
throw new Error(gl.getProgramInfoLog(p));
|
| 682 |
+
return p;
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
const ptProg = compile(
|
| 686 |
+
'attribute vec3 aId; attribute vec3 aOut;' +
|
| 687 |
+
'uniform mat4 uMVP; uniform float uMorph; uniform float uSize;' +
|
| 688 |
+
'varying vec3 vC;' +
|
| 689 |
+
'void main(){' +
|
| 690 |
+
' vec3 p = mix(aId, aOut, uMorph);' +
|
| 691 |
+
' vC = p;' +
|
| 692 |
+
' gl_Position = uMVP * vec4(p - 0.5, 1.0);' +
|
| 693 |
+
' gl_PointSize = uSize / gl_Position.w;' +
|
| 694 |
+
'}',
|
| 695 |
+
'precision mediump float; varying vec3 vC;' +
|
| 696 |
+
'void main(){' +
|
| 697 |
+
' vec2 d = gl_PointCoord - 0.5;' +
|
| 698 |
+
' float r = length(d);' +
|
| 699 |
+
' if (r > 0.5) discard;' +
|
| 700 |
+
' float a = smoothstep(0.5, 0.1, r);' +
|
| 701 |
+
' gl_FragColor = vec4(vC * (0.35 + 0.65 * a), a * 0.9);' +
|
| 702 |
+
'}');
|
| 703 |
+
|
| 704 |
+
const lnProg = compile(
|
| 705 |
+
'attribute vec3 aPos; uniform mat4 uMVP; varying vec3 vC;' +
|
| 706 |
+
'void main(){ vC = aPos; gl_Position = uMVP * vec4(aPos - 0.5, 1.0); }',
|
| 707 |
+
'precision mediump float; varying vec3 vC;' +
|
| 708 |
+
'void main(){ gl_FragColor = vec4(vC * 0.85 + 0.15, 0.22); }');
|
| 709 |
+
|
| 710 |
+
// buffers
|
| 711 |
+
function buf(data) {
|
| 712 |
+
const b = gl.createBuffer();
|
| 713 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, b);
|
| 714 |
+
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
|
| 715 |
+
return b;
|
| 716 |
+
}
|
| 717 |
+
const bId = buf(idn), bOut = buf(out);
|
| 718 |
+
|
| 719 |
+
// reference cube wireframe (12 edges)
|
| 720 |
+
const E = [];
|
| 721 |
+
const c = [[0,0,0],[1,0,0],[0,1,0],[0,0,1],[1,1,0],[1,0,1],[0,1,1],[1,1,1]];
|
| 722 |
+
[[0,1],[0,2],[0,3],[1,4],[1,5],[2,4],[2,6],[3,5],[3,6],[4,7],[5,7],[6,7]]
|
| 723 |
+
.forEach(([a,b2]) => { E.push(...c[a], ...c[b2]); });
|
| 724 |
+
const bEdge = buf(new Float32Array(E));
|
| 725 |
+
|
| 726 |
+
// mat4 helpers (column-major)
|
| 727 |
+
function mul(a, b) {
|
| 728 |
+
const o = new Float32Array(16);
|
| 729 |
+
for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) {
|
| 730 |
+
let s = 0;
|
| 731 |
+
for (let k = 0; k < 4; k++) s += a[k * 4 + j] * b[i * 4 + k];
|
| 732 |
+
o[i * 4 + j] = s;
|
| 733 |
+
}
|
| 734 |
+
return o;
|
| 735 |
+
}
|
| 736 |
+
function persp(fov, asp, n2, f) {
|
| 737 |
+
const t = 1 / Math.tan(fov / 2);
|
| 738 |
+
return new Float32Array([t / asp,0,0,0, 0,t,0,0, 0,0,(f+n2)/(n2-f),-1, 0,0,2*f*n2/(n2-f),0]);
|
| 739 |
+
}
|
| 740 |
+
function rotY(a) { const s=Math.sin(a),co=Math.cos(a);
|
| 741 |
+
return new Float32Array([co,0,-s,0, 0,1,0,0, s,0,co,0, 0,0,0,1]); }
|
| 742 |
+
function rotX(a) { const s=Math.sin(a),co=Math.cos(a);
|
| 743 |
+
return new Float32Array([1,0,0,0, 0,co,s,0, 0,-s,co,0, 0,0,0,1]); }
|
| 744 |
+
function trans(z) {
|
| 745 |
+
return new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,z,1]); }
|
| 746 |
+
|
| 747 |
+
// state
|
| 748 |
+
let yaw = 0.7, pitch = 0.45, dist = 2.6;
|
| 749 |
+
let dragging = false, lastX = 0, lastY = 0, lastInteract = 0;
|
| 750 |
+
let morph = 0, playing = true, phase = 0;
|
| 751 |
+
|
| 752 |
+
canvas.addEventListener('pointerdown', e => {
|
| 753 |
+
dragging = true; lastX = e.clientX; lastY = e.clientY;
|
| 754 |
+
lastInteract = performance.now();
|
| 755 |
+
canvas.setPointerCapture(e.pointerId);
|
| 756 |
+
});
|
| 757 |
+
canvas.addEventListener('pointermove', e => {
|
| 758 |
+
if (!dragging) return;
|
| 759 |
+
yaw += (e.clientX - lastX) * 0.008;
|
| 760 |
+
pitch = Math.max(-1.45, Math.min(1.45, pitch + (e.clientY - lastY) * 0.008));
|
| 761 |
+
lastX = e.clientX; lastY = e.clientY;
|
| 762 |
+
lastInteract = performance.now();
|
| 763 |
+
});
|
| 764 |
+
canvas.addEventListener('pointerup', () => { dragging = false; });
|
| 765 |
+
canvas.addEventListener('wheel', e => {
|
| 766 |
+
e.preventDefault();
|
| 767 |
+
dist = Math.max(1.5, Math.min(4.5, dist + e.deltaY * 0.002));
|
| 768 |
+
lastInteract = performance.now();
|
| 769 |
+
}, { passive: false });
|
| 770 |
+
|
| 771 |
+
slider.addEventListener('input', () => {
|
| 772 |
+
playing = false; playBtn.textContent = '▶';
|
| 773 |
+
morph = +slider.value;
|
| 774 |
+
});
|
| 775 |
+
playBtn.addEventListener('click', () => {
|
| 776 |
+
playing = !playing;
|
| 777 |
+
playBtn.textContent = playing ? '❚❚' : '▶';
|
| 778 |
+
});
|
| 779 |
+
|
| 780 |
+
const aId = gl.getAttribLocation(ptProg, 'aId');
|
| 781 |
+
const aOut = gl.getAttribLocation(ptProg, 'aOut');
|
| 782 |
+
const aPos = gl.getAttribLocation(lnProg, 'aPos');
|
| 783 |
+
|
| 784 |
+
let alive = true;
|
| 785 |
+
const obs = new MutationObserver(() => {
|
| 786 |
+
if (!document.body.contains(canvas)) { alive = false; obs.disconnect(); }
|
| 787 |
+
});
|
| 788 |
+
obs.observe(document.body, { childList: true, subtree: true });
|
| 789 |
+
|
| 790 |
+
function frame(t) {
|
| 791 |
+
if (!alive) return;
|
| 792 |
+
// resize
|
| 793 |
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
| 794 |
+
const w = canvas.clientWidth, h = canvas.clientHeight;
|
| 795 |
+
if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
|
| 796 |
+
canvas.width = w * dpr; canvas.height = h * dpr;
|
| 797 |
+
}
|
| 798 |
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
| 799 |
+
|
| 800 |
+
// auto-rotate when idle
|
| 801 |
+
if (!dragging && t - lastInteract > 2500) yaw += 0.0035;
|
| 802 |
+
|
| 803 |
+
// morph ping-pong with holds at each end
|
| 804 |
+
if (playing) {
|
| 805 |
+
phase = (phase + 1 / 360) % 2; // 6 s full cycle
|
| 806 |
+
const p = phase < 1 ? phase : 2 - phase; // 0→1→0
|
| 807 |
+
const q = Math.max(0, Math.min(1, (p - 0.15) / 0.7));
|
| 808 |
+
morph = q * q * (3 - 2 * q); // smoothstep + end holds
|
| 809 |
+
slider.value = morph;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
const mvp = mul(persp(0.72, w / h, 0.1, 20),
|
| 813 |
+
mul(trans(-dist), mul(rotX(pitch), rotY(yaw))));
|
| 814 |
+
|
| 815 |
+
gl.clearColor(0, 0, 0, 0);
|
| 816 |
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
| 817 |
+
gl.disable(gl.DEPTH_TEST);
|
| 818 |
+
gl.enable(gl.BLEND);
|
| 819 |
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE); // additive glow
|
| 820 |
+
|
| 821 |
+
// wireframe
|
| 822 |
+
gl.useProgram(lnProg);
|
| 823 |
+
gl.uniformMatrix4fv(gl.getUniformLocation(lnProg, 'uMVP'), false, mvp);
|
| 824 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, bEdge);
|
| 825 |
+
gl.enableVertexAttribArray(aPos);
|
| 826 |
+
gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0);
|
| 827 |
+
gl.drawArrays(gl.LINES, 0, 24);
|
| 828 |
+
|
| 829 |
+
// points
|
| 830 |
+
gl.useProgram(ptProg);
|
| 831 |
+
gl.uniformMatrix4fv(gl.getUniformLocation(ptProg, 'uMVP'), false, mvp);
|
| 832 |
+
gl.uniform1f(gl.getUniformLocation(ptProg, 'uMorph'), morph);
|
| 833 |
+
gl.uniform1f(gl.getUniformLocation(ptProg, 'uSize'), 9.0 * dpr);
|
| 834 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, bId);
|
| 835 |
+
gl.enableVertexAttribArray(aId);
|
| 836 |
+
gl.vertexAttribPointer(aId, 3, gl.FLOAT, false, 0, 0);
|
| 837 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, bOut);
|
| 838 |
+
gl.enableVertexAttribArray(aOut);
|
| 839 |
+
gl.vertexAttribPointer(aOut, 3, gl.FLOAT, false, 0, 0);
|
| 840 |
+
gl.drawArrays(gl.POINTS, 0, n3);
|
| 841 |
+
|
| 842 |
+
requestAnimationFrame(frame);
|
| 843 |
+
}
|
| 844 |
+
requestAnimationFrame(frame);
|
| 845 |
+
|
| 846 |
+
return wrap;
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
// ------------------------------------------------ privacy modal
|
| 850 |
['privacyOpen', 'privacyOpen2'].forEach(id => {
|
| 851 |
const el = $(id);
|