EtonMu commited on
Commit
629d0dd
·
verified ·
1 Parent(s): a9a71e9

Deploy Deep Analog web app

Browse files
.gitattributes CHANGED
@@ -1 +1,4 @@
1
  *.pth filter=lfs diff=lfs merge=lfs -text
 
 
 
 
1
  *.pth filter=lfs diff=lfs merge=lfs -text
2
+ static/demo/ref.jpg filter=lfs diff=lfs merge=lfs -text
3
+ static/demo/result.jpg filter=lfs diff=lfs merge=lfs -text
4
+ static/demo/target.jpg filter=lfs diff=lfs merge=lfs -text
static/demo/ref.jpg ADDED

Git LFS Details

  • SHA256: d2be39f811e85ce0782b20ea9f6f6106a6744de9b849c8d5e5faae028a0804fa
  • Pointer size: 131 Bytes
  • Size of remote file: 185 kB
static/demo/result.jpg ADDED

Git LFS Details

  • SHA256: 91d5cfa84e85b577fb49e3646cbf1924f9c6f6896a69429c807f2dfc89cdbce0
  • Pointer size: 131 Bytes
  • Size of remote file: 295 kB
static/demo/target.jpg ADDED

Git LFS Details

  • SHA256: 2d9ed625ed7d5c333432880da24e3eb03a43e1ea341a0a44c65f254bb0128c1a
  • Pointer size: 131 Bytes
  • Size of remote file: 254 kB
static/index.html CHANGED
@@ -242,6 +242,60 @@
242
  }
243
  .viz-play:hover { border-color: var(--accent); }
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  /* ---------- privacy modal ---------- */
246
  #privacyModal {
247
  position: fixed; inset: 0; z-index: 950;
@@ -340,6 +394,33 @@
340
  <div class="result-card" id="resultBody"></div>
341
  </div>
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  <footer>
344
  Deep Analog — open-set film emulation · research preview.<br>
345
  Your images are processed in memory and never stored. <a class="privacy-link" id="privacyOpen2">How privacy works here</a>.
@@ -569,7 +650,9 @@
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
 
@@ -615,6 +698,226 @@
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
@@ -637,7 +940,12 @@
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">' +
@@ -846,6 +1154,43 @@
846
  return wrap;
847
  }
848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
  // ------------------------------------------------ privacy modal
850
  ['privacyOpen', 'privacyOpen2'].forEach(id => {
851
  const el = $(id);
 
242
  }
243
  .viz-play:hover { border-color: var(--accent); }
244
 
245
+ /* ---------- help tooltip ---------- */
246
+ .help {
247
+ position: relative; display: inline-flex; align-items: center; justify-content: center;
248
+ width: 16px; height: 16px; margin-left: 8px;
249
+ border: 1px solid var(--text-dim); border-radius: 50%;
250
+ color: var(--text-dim); font-size: 10.5px; font-weight: 600;
251
+ cursor: help; vertical-align: middle; user-select: none;
252
+ }
253
+ .help:hover { border-color: var(--accent); color: var(--accent); }
254
+ .help .tip {
255
+ position: absolute; bottom: 26px; left: 50%; transform: translateX(-50%);
256
+ width: min(320px, 76vw);
257
+ background: var(--bg-3); border: 1px solid var(--line); border-radius: 10px;
258
+ padding: 12px 14px;
259
+ font-size: 12.5px; font-weight: 400; line-height: 1.6; color: #c9c9cf;
260
+ text-transform: none; letter-spacing: normal; text-align: left;
261
+ opacity: 0; pointer-events: none; transition: opacity .15s;
262
+ z-index: 30;
263
+ }
264
+ .help:hover .tip { opacity: 1; }
265
+
266
+ /* ---------- look recipe (color mapping) ---------- */
267
+ .flow-wrap { margin-top: 26px; }
268
+ .flow-grid { display: grid; grid-template-columns: 210px 1fr; gap: 18px; align-items: stretch; }
269
+ @media (max-width: 720px) { .flow-grid { grid-template-columns: 1fr; } }
270
+ .flow-ref { display: flex; flex-direction: column; gap: 8px; }
271
+ .flow-ref img { width: 100%; border-radius: 8px; display: block; border: 1px solid var(--line); }
272
+ .flow-ref .pal { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; }
273
+ .flow-ref .pal span { display: block; aspect-ratio: 1; border-radius: 5px; border: 1px solid rgba(255,255,255,.08); }
274
+ .flow-ref .cap { font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-dim); }
275
+ .flow-canvas {
276
+ width: 100%; height: 100%; min-height: 250px; display: block;
277
+ border-radius: 10px; border: 1px solid var(--line);
278
+ background: radial-gradient(500px 300px at 50% 50%, #17171c, #0a0a0c 85%);
279
+ }
280
+
281
+ /* ---------- demo section ---------- */
282
+ .demo { margin-top: 72px; }
283
+ .demo > h2 { font-size: 13px; letter-spacing: .16em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 18px; }
284
+ .demo-card { background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 22px; }
285
+ .demo-grid { display: grid; grid-template-columns: 320px 1fr; gap: 20px; align-items: start; }
286
+ @media (max-width: 760px) { .demo-grid { grid-template-columns: 1fr; } }
287
+ .demo-step { font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 8px; }
288
+ .demo-step b { color: var(--accent); font-weight: 700; }
289
+ .demo-ref img { width: 100%; border-radius: 8px; display: block; }
290
+ .demo-note { font-size: 11px; color: var(--text-dim); margin-top: 8px; line-height: 1.5; }
291
+ .demo-actions { margin-top: 16px; display: flex; align-items: center; gap: 14px; }
292
+ .try-btn {
293
+ background: var(--accent-soft); color: var(--accent);
294
+ border: 1px solid var(--accent); border-radius: 10px;
295
+ font-size: 13.5px; font-weight: 600; padding: 10px 20px; cursor: pointer;
296
+ }
297
+ .try-btn:hover { background: var(--accent); color: #17110a; }
298
+
299
  /* ---------- privacy modal ---------- */
300
  #privacyModal {
301
  position: fixed; inset: 0; z-index: 950;
 
394
  <div class="result-card" id="resultBody"></div>
395
  </div>
396
 
397
+ <section class="demo" id="demoSection" style="display:none">
398
+ <h2>See it in action</h2>
399
+ <div class="demo-card">
400
+ <div class="demo-grid">
401
+ <div class="demo-ref">
402
+ <div class="demo-step"><b>1</b>&nbsp; Pick a reference — any film look you love</div>
403
+ <img id="demoRefImg" alt="Reference film still">
404
+ <div class="demo-note">Still from <i>Happy Together</i> (1997), dir. Wong Kar-wai — shown for demonstration.</div>
405
+ </div>
406
+ <div>
407
+ <div class="demo-step"><b>2</b>&nbsp; Add your photo → <b>3</b>&nbsp; get it rendered in that look. Drag the divider to compare.</div>
408
+ <div class="compare" id="demoCompare">
409
+ <img class="base-img" id="demoResultImg" alt="Rendered result">
410
+ <img class="top-img" id="demoTargetImg" alt="Original photo">
411
+ <div class="divider"></div>
412
+ <span class="cmp-label" style="left:12px">RENDERED</span>
413
+ <span class="cmp-label" style="right:12px">ORIGINAL</span>
414
+ </div>
415
+ <div class="demo-actions">
416
+ <button class="try-btn" id="tryDemoBtn">Try this example yourself →</button>
417
+ <span style="font-size:12px;color:var(--text-dim)">loads both images into the app above</span>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ </section>
423
+
424
  <footer>
425
  Deep Analog — open-set film emulation · research preview.<br>
426
  Your images are processed in memory and never stored. <a class="privacy-link" id="privacyOpen2">How privacy works here</a>.
 
650
  initCompare(cmp);
651
  }
652
 
653
+ // Visualizations of the predicted LUT (both modes)
654
+ try { body.appendChild(makeFlowCard(data.cube_lut, refFile)); }
655
+ catch (e) { /* canvas unavailable — skip silently */ }
656
  try { body.appendChild(makeVizCard(data.cube_lut)); }
657
  catch (e) { /* WebGL unavailable — skip viz silently */ }
658
 
 
698
  cmp.addEventListener('pointerup', () => { dragging = false; });
699
  }
700
 
701
+ // ------------------------------------------------ look recipe (color mapping)
702
+ // Canvas ribbons: everyday input colors (top) flow into what the
703
+ // predicted LUT turns them into (bottom). Left: the reference image
704
+ // and its dominant palette — the source the model learned from.
705
+
706
+ function lutSampler(out, N) {
707
+ const f = N - 1;
708
+ function at(i, j, k, ch) { return out[((k * N + j) * N + i) * 3 + ch]; }
709
+ return function (r, g, b) {
710
+ const x = Math.min(Math.max(r, 0), 1) * f;
711
+ const y = Math.min(Math.max(g, 0), 1) * f;
712
+ const z = Math.min(Math.max(b, 0), 1) * f;
713
+ const x0 = Math.min(Math.floor(x), N - 2);
714
+ const y0 = Math.min(Math.floor(y), N - 2);
715
+ const z0 = Math.min(Math.floor(z), N - 2);
716
+ const dx = x - x0, dy = y - y0, dz = z - z0;
717
+ const res = [0, 0, 0];
718
+ for (let ch = 0; ch < 3; ch++) {
719
+ const c00 = at(x0,y0,z0,ch)*(1-dx) + at(x0+1,y0,z0,ch)*dx;
720
+ const c10 = at(x0,y0+1,z0,ch)*(1-dx) + at(x0+1,y0+1,z0,ch)*dx;
721
+ const c01 = at(x0,y0,z0+1,ch)*(1-dx) + at(x0+1,y0,z0+1,ch)*dx;
722
+ const c11 = at(x0,y0+1,z0+1,ch)*(1-dx) + at(x0+1,y0+1,z0+1,ch)*dx;
723
+ res[ch] = (c00*(1-dy) + c10*dy)*(1-dz) + (c01*(1-dy) + c11*dy)*dz;
724
+ }
725
+ return res;
726
+ };
727
+ }
728
+
729
+ function hsv2rgb(h, s, v) {
730
+ const i = Math.floor(h * 6), fr = h * 6 - i;
731
+ const p = v * (1 - s), q = v * (1 - fr * s), t = v * (1 - (1 - fr) * s);
732
+ const m = [[v,t,p],[q,v,p],[p,v,t],[p,q,v],[t,p,v],[v,p,q]][i % 6];
733
+ return m;
734
+ }
735
+
736
+ function rgbHex(c) {
737
+ return '#' + c.map(v => Math.round(Math.min(Math.max(v,0),1) * 255)
738
+ .toString(16).padStart(2, '0')).join('').toUpperCase();
739
+ }
740
+
741
+ function extractPalette(file, done) {
742
+ createImageBitmap(file).then(bmp => {
743
+ const S = 48;
744
+ const cv = document.createElement('canvas');
745
+ cv.width = S; cv.height = S;
746
+ const cx = cv.getContext('2d');
747
+ cx.drawImage(bmp, 0, 0, S, S);
748
+ const d = cx.getImageData(0, 0, S, S).data;
749
+ const bins = new Map();
750
+ for (let i = 0; i < d.length; i += 4) {
751
+ const key = ((d[i] >> 4) << 8) | ((d[i+1] >> 4) << 4) | (d[i+2] >> 4);
752
+ let b2 = bins.get(key);
753
+ if (!b2) { b2 = [0, 0, 0, 0]; bins.set(key, b2); }
754
+ b2[0] += d[i]; b2[1] += d[i+1]; b2[2] += d[i+2]; b2[3]++;
755
+ }
756
+ const sorted = [...bins.values()].sort((a, b2) => b2[3] - a[3]);
757
+ const picks = [];
758
+ for (const b2 of sorted) {
759
+ const c = [b2[0]/b2[3]/255, b2[1]/b2[3]/255, b2[2]/b2[3]/255];
760
+ if (picks.every(p =>
761
+ Math.hypot(p[0]-c[0], p[1]-c[1], p[2]-c[2]) > 0.16)) picks.push(c);
762
+ if (picks.length >= 6) break;
763
+ }
764
+ done(picks);
765
+ }).catch(() => done(null));
766
+ }
767
+
768
+ function makeFlowCard(cubeText, refFileMaybe) {
769
+ const { N, out } = parseCube(cubeText);
770
+ if (!N) throw new Error('bad cube');
771
+ const sample = lutSampler(out, N);
772
+
773
+ const wrap = document.createElement('div');
774
+ wrap.className = 'flow-wrap';
775
+ wrap.innerHTML =
776
+ '<div class="viz-head"><span class="t">Look recipe — reference → output' +
777
+ '<span class="help">?<span class="tip">The model reads the reference’s palette ' +
778
+ '(left) and distills a recipe from it. Each ribbon takes one everyday color (top) ' +
779
+ 'and shows what this look turns it into (bottom) — hover any ribbon to inspect ' +
780
+ 'the exact values. Together, the ribbons are the DNA of the look.</span></span></span>' +
781
+ '<span class="hint">hover a ribbon</span></div>' +
782
+ '<div class="flow-grid">' +
783
+ '<div class="flow-ref" style="display:none">' +
784
+ '<span class="cap">Reference — what the model saw</span>' +
785
+ '<img alt="Reference"><div class="pal"></div></div>' +
786
+ '<canvas class="flow-canvas"></canvas></div>';
787
+
788
+ const refBox = wrap.querySelector('.flow-ref');
789
+ const grid = wrap.querySelector('.flow-grid');
790
+ const canvas = wrap.querySelector('canvas');
791
+
792
+ // left column: reference thumb + extracted palette (when decodable)
793
+ if (refFileMaybe && PREVIEWABLE.includes(refFileMaybe.type)) {
794
+ wrap.querySelector('.flow-ref img').src = URL.createObjectURL(refFileMaybe);
795
+ extractPalette(refFileMaybe, colors => {
796
+ if (!colors || colors.length < 3) return;
797
+ const pal = wrap.querySelector('.pal');
798
+ colors.forEach(c => {
799
+ const s = document.createElement('span');
800
+ s.style.background = rgbHex(c);
801
+ s.title = rgbHex(c);
802
+ pal.appendChild(s);
803
+ });
804
+ refBox.style.display = 'flex';
805
+ });
806
+ } else {
807
+ grid.style.gridTemplateColumns = '1fr';
808
+ }
809
+
810
+ // ribbon data: 10 hues + 3 grays + skin tone
811
+ const inputs = [];
812
+ for (let i = 0; i < 10; i++) inputs.push(hsv2rgb(i / 10, 0.62, 0.85));
813
+ inputs.push([0.16,0.16,0.16], [0.5,0.5,0.5], [0.85,0.85,0.85], [0.85,0.66,0.52]);
814
+ const pairs = inputs.map(c => ({ cin: c, cout: sample(c[0], c[1], c[2]) }));
815
+
816
+ const ctx = canvas.getContext('2d');
817
+ let hoverIdx = -1, reveal = 0, raf = null;
818
+
819
+ function draw() {
820
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
821
+ const w = canvas.clientWidth, h = Math.max(canvas.clientHeight, 250);
822
+ if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
823
+ canvas.width = w * dpr; canvas.height = h * dpr;
824
+ }
825
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
826
+ ctx.clearRect(0, 0, w, h);
827
+
828
+ const n = pairs.length, mx = 34;
829
+ const step = (w - mx * 2) / (n - 1);
830
+ const topY = 34, botY = h - 34, sw = 20;
831
+
832
+ ctx.font = '10px ui-monospace, Menlo, monospace';
833
+ ctx.fillStyle = 'rgba(139,139,147,.9)';
834
+ ctx.fillText('ANY PHOTO — original colors', mx - 10, 14);
835
+ ctx.fillText('AFTER THIS LOOK', mx - 10, h - 8);
836
+
837
+ pairs.forEach((p, i) => {
838
+ const x = mx + step * i;
839
+ const prog = Math.min(Math.max(reveal * n * 1.6 - i, 0), 1);
840
+ const dim = hoverIdx >= 0 && hoverIdx !== i;
841
+ ctx.globalAlpha = (dim ? 0.18 : 0.95) * prog;
842
+
843
+ // ribbon
844
+ const grad = ctx.createLinearGradient(0, topY, 0, botY);
845
+ grad.addColorStop(0, rgbHex(p.cin));
846
+ grad.addColorStop(1, rgbHex(p.cout));
847
+ ctx.strokeStyle = grad;
848
+ ctx.lineWidth = hoverIdx === i ? 15 : 11;
849
+ ctx.lineCap = 'round';
850
+ const sway = Math.sin(i * 1.7) * 10;
851
+ ctx.beginPath();
852
+ ctx.moveTo(x, topY + sw / 2 + 4);
853
+ const endY = topY + sw / 2 + 4 + (botY - topY - sw - 8) * prog;
854
+ ctx.bezierCurveTo(x + sway, topY + (botY - topY) * 0.4,
855
+ x - sway, topY + (botY - topY) * 0.6,
856
+ x, endY);
857
+ ctx.stroke();
858
+
859
+ // swatches
860
+ ctx.globalAlpha = dim ? 0.3 : 1;
861
+ ctx.fillStyle = rgbHex(p.cin);
862
+ rr(ctx, x - sw/2, topY - sw/2, sw, sw, 5); ctx.fill();
863
+ ctx.globalAlpha = (dim ? 0.3 : 1) * prog;
864
+ ctx.fillStyle = rgbHex(p.cout);
865
+ rr(ctx, x - sw/2, botY - sw/2, sw, sw, 5); ctx.fill();
866
+ ctx.globalAlpha = 1;
867
+ });
868
+
869
+ // hover readout
870
+ if (hoverIdx >= 0) {
871
+ const p = pairs[hoverIdx];
872
+ const x = mx + step * hoverIdx;
873
+ ctx.font = '11px ui-monospace, Menlo, monospace';
874
+ ctx.textAlign = x > w - 90 ? 'right' : 'left';
875
+ const tx = x > w - 90 ? x - 16 : x + 16;
876
+ ctx.fillStyle = 'rgba(236,236,236,.95)';
877
+ ctx.fillText(rgbHex(p.cin), tx, topY + 4);
878
+ ctx.fillText(rgbHex(p.cout), tx, botY + 4);
879
+ ctx.textAlign = 'left';
880
+ }
881
+ }
882
+
883
+ function rr(c, x, y, w2, h2, r) {
884
+ c.beginPath();
885
+ c.moveTo(x + r, y);
886
+ c.arcTo(x + w2, y, x + w2, y + h2, r);
887
+ c.arcTo(x + w2, y + h2, x, y + h2, r);
888
+ c.arcTo(x, y + h2, x, y, r);
889
+ c.arcTo(x, y, x + w2, y, r);
890
+ c.closePath();
891
+ }
892
+
893
+ function animate() {
894
+ reveal = Math.min(reveal + 0.02, 1);
895
+ draw();
896
+ if (reveal < 1) raf = requestAnimationFrame(animate);
897
+ }
898
+ // start when it enters the viewport
899
+ const io = new IntersectionObserver(entries => {
900
+ if (entries[0].isIntersecting) { io.disconnect(); animate(); }
901
+ }, { threshold: 0.25 });
902
+ io.observe(canvas);
903
+
904
+ canvas.addEventListener('pointermove', e => {
905
+ const rect = canvas.getBoundingClientRect();
906
+ const n = pairs.length, mx = 34;
907
+ const step = (rect.width - mx * 2) / (n - 1);
908
+ const idx = Math.round((e.clientX - rect.left - mx) / step);
909
+ hoverIdx = (idx >= 0 && idx < n) ? idx : -1;
910
+ if (reveal >= 1) draw();
911
+ });
912
+ canvas.addEventListener('pointerleave', () => {
913
+ hoverIdx = -1;
914
+ if (reveal >= 1) draw();
915
+ });
916
+ window.addEventListener('resize', () => { if (reveal > 0) draw(); });
917
+
918
+ return wrap;
919
+ }
920
+
921
  // ------------------------------------------------ 3D LUT visualization
922
  // Self-contained WebGL point cloud — no libraries, no external requests.
923
  // Shows the LUT lattice morphing from identity (perfect cube) to the
 
940
  const wrap = document.createElement('div');
941
  wrap.className = 'viz-wrap';
942
  wrap.innerHTML =
943
+ '<div class="viz-head"><span class="t">Color space — 3D LUT' +
944
+ '<span class="help">?<span class="tip">A 3D LUT (look-up table) is a color recipe: ' +
945
+ 'for every color a photo can contain, it stores the color the film would turn it into. ' +
946
+ 'Each glowing dot below is one color — press play to watch the film’s recipe bend ' +
947
+ 'the entire color space. The .cube file you download is exactly this transform, ready ' +
948
+ 'for Photoshop, Lightroom, or DaVinci Resolve.</span></span></span>' +
949
  '<span class="hint">drag to rotate · scroll to zoom</span></div>' +
950
  '<canvas class="viz-canvas"></canvas>' +
951
  '<div class="viz-controls">' +
 
1154
  return wrap;
1155
  }
1156
 
1157
+ // ------------------------------------------------ demo section
1158
+ // Shown only if demo assets exist on the server (static/demo/*.jpg).
1159
+ (function initDemo() {
1160
+ const paths = {
1161
+ ref: 'static/demo/ref.jpg',
1162
+ target: 'static/demo/target.jpg',
1163
+ result: 'static/demo/result.jpg',
1164
+ };
1165
+ const probe = new Image();
1166
+ probe.onload = () => {
1167
+ $('demoRefImg').src = paths.ref;
1168
+ $('demoTargetImg').src = paths.target;
1169
+ $('demoResultImg').src = paths.result;
1170
+ $('demoSection').style.display = 'block';
1171
+ initCompare($('demoCompare'));
1172
+
1173
+ $('tryDemoBtn').addEventListener('click', async () => {
1174
+ try {
1175
+ $('tryDemoBtn').textContent = 'Loading…';
1176
+ const [rb, tb] = await Promise.all([
1177
+ fetch(paths.ref).then(r => r.blob()),
1178
+ fetch(paths.target).then(r => r.blob()),
1179
+ ]);
1180
+ refFile = new File([rb], 'demo-reference.jpg', { type: 'image/jpeg' });
1181
+ tgtFile = new File([tb], 'demo-photo.jpg', { type: 'image/jpeg' });
1182
+ fillZone('refZone', refFile, () => { refFile = null; clearZone('refZone'); update(); });
1183
+ fillZone('tgtZone', tgtFile, () => { tgtFile = null; clearZone('tgtZone'); update(); });
1184
+ update();
1185
+ window.scrollTo({ top: 0, behavior: 'smooth' });
1186
+ } finally {
1187
+ $('tryDemoBtn').textContent = 'Try this example yourself →';
1188
+ }
1189
+ });
1190
+ };
1191
+ probe.src = paths.ref;
1192
+ })();
1193
+
1194
  // ------------------------------------------------ privacy modal
1195
  ['privacyOpen', 'privacyOpen2'].forEach(id => {
1196
  const el = $(id);