EtonMu commited on
Commit
1abf83f
·
verified ·
1 Parent(s): 4a768d7

Deploy Deep Analog web app

Browse files
.gitattributes CHANGED
@@ -1,4 +1,4 @@
1
  *.pth filter=lfs diff=lfs merge=lfs -text
2
- static/movies/mov_03.jpg filter=lfs diff=lfs merge=lfs -text
3
- static/movies/mov_04.jpg filter=lfs diff=lfs merge=lfs -text
4
- static/movies/mov_08.jpg filter=lfs diff=lfs merge=lfs -text
 
1
  *.pth filter=lfs diff=lfs merge=lfs -text
2
+ static/lab/tar_1.jpg filter=lfs diff=lfs merge=lfs -text
3
+ static/lab/tar_4.jpg filter=lfs diff=lfs merge=lfs -text
4
+ static/lab/tar_6.jpg filter=lfs diff=lfs merge=lfs -text
static/index.html CHANGED
@@ -319,6 +319,27 @@
319
  pointer-events: none;
320
  }
321
  .movie-card:hover .use { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
  /* ---------- film lab ---------- */
324
  .lab-films { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 16px; }
@@ -447,7 +468,7 @@
447
  <div class="result-card" id="resultBody"></div>
448
  </div>
449
 
450
- <section class="demo" id="demoSection" style="display:none">
451
  <h2>See it in action</h2>
452
  <div class="demo-card">
453
  <div class="demo-grid">
@@ -476,33 +497,43 @@
476
  </div>
477
  </section>
478
 
479
- <section class="demo" id="moviesSection" style="display:none">
480
- <h2>Movie looksstart from a still you love</h2>
481
- <p class="section-sub">Click any still to load it as your reference, then drop in your own photo.
482
- Stills from the films credited below, shown for demonstration.</p>
483
- <div class="movies-grid" id="moviesGrid"></div>
484
- </section>
485
-
486
- <section class="demo" id="labSection" style="display:none">
487
- <h2>Film lab — explore the looks</h2>
488
- <p class="section-sub">Five real film stocks × seven scenes, pre-rendered for instant comparison.
489
  All test photographs shot by <b style="color:var(--text)">Eton Mu</b>.</p>
490
  <div class="demo-card">
491
- <div class="demo-step" style="margin-bottom:12px">Pick a film stock — click its name for the full story of that emulsion</div>
492
- <div class="lab-films" id="labFilms"></div>
493
- <div class="demo-step" style="margin-bottom:10px">Pick a photo</div>
 
 
 
 
 
 
 
 
494
  <div class="lab-inputs" id="labInputs"></div>
495
- <p class="lab-caption" id="labCaption"></p>
496
- <div class="compare" id="labCompare">
497
- <img class="base-img" alt="Rendered">
498
- <img class="top-img" alt="Original">
499
- <div class="divider"></div>
500
- <span class="cmp-label" style="left:12px">RENDERED</span>
501
- <span class="cmp-label" style="right:12px">ORIGINAL</span>
502
  </div>
503
- <div class="demo-actions">
504
- <button class="try-btn" id="tryLabBtn">Open this pair in the app →</button>
505
- <span style="font-size:12px;color:var(--text-dim)">predicts the LUT live from this reference</span>
 
 
 
 
 
 
 
 
506
  </div>
507
  </div>
508
  </section>
@@ -1240,91 +1271,41 @@
1240
  return wrap;
1241
  }
1242
 
1243
- // ------------------------------------------------ demo section
1244
- // Shown only if demo assets exist on the server (static/demo/*.jpg).
1245
  (function initDemo() {
1246
  const paths = {
1247
  ref: 'static/demo/ref.jpg',
1248
  target: 'static/demo/target.jpg',
1249
  result: 'static/demo/result.jpg',
1250
  };
1251
- const probe = new Image();
1252
- probe.onload = () => {
1253
- $('demoRefImg').src = paths.ref;
1254
- $('demoInputImg').src = paths.target;
1255
- $('demoTargetImg').src = paths.target;
1256
- $('demoResultImg').src = paths.result;
1257
- $('demoSection').style.display = 'block';
1258
- initCompare($('demoCompare'));
1259
-
1260
- $('tryDemoBtn').addEventListener('click', async () => {
1261
- try {
1262
- $('tryDemoBtn').textContent = 'Loading…';
1263
- const [rb, tb] = await Promise.all([
1264
- fetch(paths.ref).then(r => r.blob()),
1265
- fetch(paths.target).then(r => r.blob()),
1266
- ]);
1267
- refFile = new File([rb], 'demo-reference.jpg', { type: 'image/jpeg' });
1268
- tgtFile = new File([tb], 'demo-photo.jpg', { type: 'image/jpeg' });
1269
- fillZone('refZone', refFile, () => { refFile = null; clearZone('refZone'); update(); });
1270
- fillZone('tgtZone', tgtFile, () => { tgtFile = null; clearZone('tgtZone'); update(); });
1271
- update();
1272
- window.scrollTo({ top: 0, behavior: 'smooth' });
1273
- } finally {
1274
- $('tryDemoBtn').textContent = 'Try this example yourself →';
1275
- }
1276
- });
1277
- };
1278
- probe.src = paths.ref;
1279
- })();
1280
-
1281
- // ------------------------------------------------ movie looks
1282
- (function initMovies() {
1283
- const MOVIES = [
1284
- { file: 'static/demo/ref.jpg', title: 'Happy Together', meta: '1997 · Wong Kar-wai' },
1285
- { file: 'static/movies/mov_01.jpg', title: 'In the Mood for Love', meta: '2000 · Wong Kar-wai' },
1286
- { file: 'static/movies/mov_02.jpg', title: 'Days of Being Wild', meta: '1990 · Wong Kar-wai' },
1287
- { file: 'static/movies/mov_03.jpg', title: 'Close-Up', meta: '1990 · Abbas Kiarostami' },
1288
- { file: 'static/movies/mov_04.jpg', title: 'The Grand Budapest Hotel', meta: '2014 · Wes Anderson' },
1289
- { file: 'static/movies/mov_05.jpg', title: 'Barry Lyndon', meta: '1975 · Stanley Kubrick' },
1290
- { file: 'static/movies/mov_06.jpg', title: 'The Good, the Bad and the Ugly', meta: '1966 · Sergio Leone' },
1291
- { file: 'static/movies/mov_07.jpg', title: 'A Touch of Zen', meta: '1971 · King Hu' },
1292
- { file: 'static/movies/mov_08.jpg', title: 'Kagemusha', meta: '1980 · Akira Kurosawa' },
1293
- ];
1294
-
1295
- function build() {
1296
- const grid = $('moviesGrid');
1297
- MOVIES.forEach(m => {
1298
- const card = document.createElement('div');
1299
- card.className = 'movie-card';
1300
- card.innerHTML =
1301
- '<img class="still" loading="lazy" alt="">' +
1302
- '<span class="use">USE AS REFERENCE</span>' +
1303
- '<div class="cap"><div class="title"></div><div class="meta"></div></div>';
1304
- card.querySelector('img').src = m.file;
1305
- card.querySelector('img').alt = m.title;
1306
- card.querySelector('.title').textContent = m.title;
1307
- card.querySelector('.meta').textContent = m.meta;
1308
- card.addEventListener('click', async () => {
1309
- const blob = await fetch(m.file).then(r => r.blob());
1310
- refFile = new File([blob], 'reference.jpg', { type: 'image/jpeg' });
1311
- fillZone('refZone', refFile, () => { refFile = null; clearZone('refZone'); update(); });
1312
- update();
1313
- window.scrollTo({ top: 0, behavior: 'smooth' });
1314
- });
1315
- grid.appendChild(card);
1316
- });
1317
- }
1318
-
1319
- const probe = new Image();
1320
- probe.onload = () => { $('moviesSection').style.display = 'block'; build(); };
1321
- probe.src = 'static/movies/mov_01.jpg';
1322
  })();
1323
 
1324
- // ------------------------------------------------ film lab
1325
- // Interactive gallery over the pre-rendered experiment set
1326
- // (static/lab/*). Each stock links to a full introduction page.
1327
- (function initLab() {
1328
  const FILMS = [
1329
  { id: 'vision3', name: 'Kodak Vision3 250D 5207', short: '250D',
1330
  label: '#E8B23A', text: '#181205',
@@ -1342,12 +1323,32 @@
1342
  label: '#A8503A', text: '#f7e8e2',
1343
  url: 'https://www.pandacamera.com/products/escura-showa-400-iso-30exp-135-35mm-color-negative-film' },
1344
  ];
1345
- const INPUTS = [
1346
- ['01', 'Tokyo alley'], ['02', 'Bicycle corridor'], ['03', 'Asakusa & Skytree'],
1347
- ['04', 'Rainy night, NYC'], ['05', 'Window still life'], ['06', 'Portrait at the bar'],
1348
- ['07', 'Pond reflection'],
 
 
 
 
 
 
1349
  ];
1350
- let film = FILMS[0], input = INPUTS[0];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1351
 
1352
  function canisterSvg(f) {
1353
  return '<svg class="canister" width="34" height="46" viewBox="0 0 34 46" aria-hidden="true">' +
@@ -1362,67 +1363,106 @@
1362
  '<rect x="3" y="40" width="28" height="4" rx="2" fill="#333338"/></svg>';
1363
  }
1364
 
1365
- function refresh(resetDivider) {
 
 
 
 
 
 
 
 
1366
  document.querySelectorAll('#labFilms .film-card').forEach((el, i) =>
1367
- el.classList.toggle('sel', FILMS[i] === film));
1368
  document.querySelectorAll('#labInputs img').forEach((el, i) =>
1369
  el.classList.toggle('sel', INPUTS[i] === input));
1370
- const cmp = $('labCompare');
1371
- cmp.querySelector('.base-img').src = 'static/lab/' + input[0] + '_' + film.id + '.jpg';
1372
- cmp.querySelector('.top-img').src = 'static/lab/input_' + input[0] + '.jpg';
1373
- $('labCaption').innerHTML = '<b>' + input[1] + '</b> rendered as <b>' + film.name + '</b>';
 
 
 
 
 
 
 
 
 
1374
  }
1375
 
1376
- function build() {
1377
- const filmsBox = $('labFilms');
1378
- FILMS.forEach(f => {
1379
- const card = document.createElement('div');
1380
- card.className = 'film-card';
1381
- card.innerHTML =
1382
- '<img class="thumb" alt="" src="static/lab/ref_' + f.id + '.jpg">' +
1383
- '<div class="meta">' + canisterSvg(f) +
1384
- '<a class="stock" target="_blank" rel="noopener">' + f.name +
1385
- ' <span class="ext">↗</span></a></div>';
1386
- const a = card.querySelector('a.stock');
1387
- a.href = f.url;
1388
- a.title = 'About ' + f.name;
1389
- a.addEventListener('click', e => e.stopPropagation());
1390
- card.addEventListener('click', () => { film = f; refresh(); });
1391
- filmsBox.appendChild(card);
1392
- });
1393
- const inputsBox = $('labInputs');
1394
- INPUTS.forEach(inp => {
1395
- const im = document.createElement('img');
1396
- im.src = 'static/lab/input_' + inp[0] + '.jpg';
1397
- im.alt = inp[1]; im.title = inp[1];
1398
- im.addEventListener('click', () => { input = inp; refresh(); });
1399
- inputsBox.appendChild(im);
1400
- });
1401
- initCompare($('labCompare'));
1402
  refresh();
1403
-
1404
- $('tryLabBtn').addEventListener('click', async () => {
1405
- try {
1406
- $('tryLabBtn').textContent = 'Loading…';
1407
- const [rb, tb] = await Promise.all([
1408
- fetch('static/lab/ref_' + film.id + '.jpg').then(r => r.blob()),
1409
- fetch('static/lab/input_' + input[0] + '.jpg').then(r => r.blob()),
1410
- ]);
1411
- refFile = new File([rb], film.id + '-reference.jpg', { type: 'image/jpeg' });
1412
- tgtFile = new File([tb], 'photo.jpg', { type: 'image/jpeg' });
1413
- fillZone('refZone', refFile, () => { refFile = null; clearZone('refZone'); update(); });
1414
- fillZone('tgtZone', tgtFile, () => { tgtFile = null; clearZone('tgtZone'); update(); });
1415
- update();
1416
- window.scrollTo({ top: 0, behavior: 'smooth' });
1417
- } finally {
1418
- $('tryLabBtn').textContent = 'Open this pair in the app →';
1419
- }
1420
- });
1421
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1422
 
1423
- const probe = new Image();
1424
- probe.onload = () => { $('labSection').style.display = 'block'; build(); };
1425
- probe.src = 'static/lab/ref_vision3.jpg';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1426
  })();
1427
 
1428
  // ------------------------------------------------ privacy modal
 
319
  pointer-events: none;
320
  }
321
  .movie-card:hover .use { opacity: 1; transform: translateY(0); }
322
+ .movie-card.sel { border-color: var(--accent); }
323
+ .movie-card.sel .use { opacity: 1; transform: translateY(0); }
324
+
325
+ /* ---------- playground tabs + pair bar ---------- */
326
+ .ref-tabs { display: flex; gap: 8px; margin-bottom: 14px; }
327
+ .tab-btn {
328
+ background: var(--bg-3); border: 1.5px solid var(--line); border-radius: 20px;
329
+ color: var(--text-dim); font-size: 12.5px; font-weight: 600;
330
+ padding: 7px 18px; cursor: pointer; transition: all .15s;
331
+ }
332
+ .tab-btn:hover { color: var(--text); }
333
+ .tab-btn.sel { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
334
+ .pair-bar {
335
+ display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
336
+ background: var(--bg-3); border: 1px solid var(--line); border-radius: 11px;
337
+ padding: 12px 14px; margin-top: 22px;
338
+ }
339
+ .pair-bar img { height: 52px; width: 78px; object-fit: cover; border-radius: 7px; border: 1px solid var(--line); }
340
+ .pair-plus { color: var(--text-dim); font-size: 16px; }
341
+ .pair-info { flex: 1; min-width: 160px; font-size: 12.5px; color: var(--text-dim); line-height: 1.5; }
342
+ .pair-info b { color: var(--text); font-weight: 600; }
343
 
344
  /* ---------- film lab ---------- */
345
  .lab-films { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 16px; }
 
468
  <div class="result-card" id="resultBody"></div>
469
  </div>
470
 
471
+ <section class="demo" id="demoSection">
472
  <h2>See it in action</h2>
473
  <div class="demo-card">
474
  <div class="demo-grid">
 
497
  </div>
498
  </section>
499
 
500
+ <section class="demo" id="playSection">
501
+ <h2>Try it yourself pick a pair</h2>
502
+ <p class="section-sub">Choose a reference look and an input photo, then run the pair through the model.
503
+ Film-stock pairs with the seven test photos also show an instant pre-rendered preview.
 
 
 
 
 
 
504
  All test photographs shot by <b style="color:var(--text)">Eton Mu</b>.</p>
505
  <div class="demo-card">
506
+ <div class="demo-step" style="margin-bottom:10px"><b>1</b>&nbsp; Choose a reference</div>
507
+ <div class="ref-tabs">
508
+ <button class="tab-btn sel" id="tabMovies">🎬&nbsp; Movie looks</button>
509
+ <button class="tab-btn" id="tabFilms">🎞&nbsp; Film lab</button>
510
+ </div>
511
+ <div class="movies-grid" id="moviesGrid"></div>
512
+ <div class="lab-films" id="labFilms" style="display:none"></div>
513
+ <p class="lab-caption" id="filmsHint" style="display:none;margin-top:10px">
514
+ Click a film's name to read the full story of that emulsion.</p>
515
+
516
+ <div class="demo-step" style="margin:24px 0 10px"><b>2</b>&nbsp; Choose an input photo</div>
517
  <div class="lab-inputs" id="labInputs"></div>
518
+
519
+ <div class="pair-bar">
520
+ <img id="pairRef" alt="Selected reference">
521
+ <span class="pair-plus">+</span>
522
+ <img id="pairInput" alt="Selected input">
523
+ <div class="pair-info" id="pairInfo"></div>
524
+ <button class="try-btn" id="runPairBtn">Run this pair in the app →</button>
525
  </div>
526
+
527
+ <div id="instantWrap" style="display:none">
528
+ <div class="demo-step" style="margin:24px 0 10px">Instant preview pre-rendered · drag to compare</div>
529
+ <div class="compare" id="labCompare">
530
+ <img class="base-img" alt="Rendered">
531
+ <img class="top-img" alt="Original">
532
+ <div class="divider"></div>
533
+ <span class="cmp-label" style="left:12px">RENDERED</span>
534
+ <span class="cmp-label" style="right:12px">ORIGINAL</span>
535
+ </div>
536
+ <p class="lab-caption" id="labCaption" style="margin-top:10px"></p>
537
  </div>
538
  </div>
539
  </section>
 
1271
  return wrap;
1272
  }
1273
 
1274
+ // ------------------------------------------------ demo section (Happy Together walkthrough)
 
1275
  (function initDemo() {
1276
  const paths = {
1277
  ref: 'static/demo/ref.jpg',
1278
  target: 'static/demo/target.jpg',
1279
  result: 'static/demo/result.jpg',
1280
  };
1281
+ $('demoRefImg').src = paths.ref;
1282
+ $('demoInputImg').src = paths.target;
1283
+ $('demoTargetImg').src = paths.target;
1284
+ $('demoResultImg').src = paths.result;
1285
+ initCompare($('demoCompare'));
1286
+
1287
+ $('tryDemoBtn').addEventListener('click', async () => {
1288
+ try {
1289
+ $('tryDemoBtn').textContent = 'Loading…';
1290
+ const [rb, tb] = await Promise.all([
1291
+ fetch(paths.ref).then(r => r.blob()),
1292
+ fetch(paths.target).then(r => r.blob()),
1293
+ ]);
1294
+ refFile = new File([rb], 'demo-reference.jpg', { type: 'image/jpeg' });
1295
+ tgtFile = new File([tb], 'demo-photo.jpg', { type: 'image/jpeg' });
1296
+ fillZone('refZone', refFile, () => { refFile = null; clearZone('refZone'); update(); });
1297
+ fillZone('tgtZone', tgtFile, () => { tgtFile = null; clearZone('tgtZone'); update(); });
1298
+ update();
1299
+ window.scrollTo({ top: 0, behavior: 'smooth' });
1300
+ } finally {
1301
+ $('tryDemoBtn').textContent = 'Try this example yourself →';
1302
+ }
1303
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1304
  })();
1305
 
1306
+ // ------------------------------------------------ demo playground
1307
+ // Reference (movie stills OR film stocks) × input photo picker.
1308
+ (function initPlayground() {
 
1309
  const FILMS = [
1310
  { id: 'vision3', name: 'Kodak Vision3 250D 5207', short: '250D',
1311
  label: '#E8B23A', text: '#181205',
 
1323
  label: '#A8503A', text: '#f7e8e2',
1324
  url: 'https://www.pandacamera.com/products/escura-showa-400-iso-30exp-135-35mm-color-negative-film' },
1325
  ];
1326
+ const MOVIES = [
1327
+ { file: 'static/demo/ref.jpg', title: 'Happy Together', meta: '1997 · Wong Kar-wai' },
1328
+ { file: 'static/movies/mov_01.jpg', title: 'In the Mood for Love', meta: '2000 · Wong Kar-wai' },
1329
+ { file: 'static/movies/mov_02.jpg', title: 'Days of Being Wild', meta: '1990 · Wong Kar-wai' },
1330
+ { file: 'static/movies/mov_03.jpg', title: 'Close-Up', meta: '1990 · Abbas Kiarostami' },
1331
+ { file: 'static/movies/mov_04.jpg', title: 'The Grand Budapest Hotel', meta: '2014 · Wes Anderson' },
1332
+ { file: 'static/movies/mov_05.jpg', title: 'Barry Lyndon', meta: '1975 · Stanley Kubrick' },
1333
+ { file: 'static/movies/mov_06.jpg', title: 'The Good, the Bad and the Ugly', meta: '1966 · Sergio Leone' },
1334
+ { file: 'static/movies/mov_07.jpg', title: 'A Touch of Zen', meta: '1971 · King Hu' },
1335
+ { file: 'static/movies/mov_08.jpg', title: 'Kagemusha', meta: '1980 · Akira Kurosawa' },
1336
  ];
1337
+ const INPUTS = [
1338
+ ['01', 'Tokyo alley', true], ['02', 'Bicycle corridor', true],
1339
+ ['03', 'Asakusa & Skytree', true], ['04', 'Rainy night, NYC', true],
1340
+ ['05', 'Window still life', true], ['06', 'Portrait at the bar', true],
1341
+ ['07', 'Pond reflection', true],
1342
+ ['t1', 'Hilltop monastery, Tibet', false],
1343
+ ['t4', 'Hanging garden', false],
1344
+ ['t6', 'Cypress swamp', false],
1345
+ ].map(a => ({
1346
+ id: a[0], name: a[1], lab: a[2],
1347
+ src: a[2] ? 'static/lab/input_' + a[0] + '.jpg'
1348
+ : 'static/lab/tar_' + a[0].slice(1) + '.jpg',
1349
+ }));
1350
+
1351
+ let refType = 'movie', movie = MOVIES[0], film = FILMS[0], input = INPUTS[0];
1352
 
1353
  function canisterSvg(f) {
1354
  return '<svg class="canister" width="34" height="46" viewBox="0 0 34 46" aria-hidden="true">' +
 
1363
  '<rect x="3" y="40" width="28" height="4" rx="2" fill="#333338"/></svg>';
1364
  }
1365
 
1366
+ function refSrc() { return refType === 'movie' ? movie.file : 'static/lab/ref_' + film.id + '.jpg'; }
1367
+ function refName() { return refType === 'movie' ? movie.title : film.name; }
1368
+
1369
+ function refresh() {
1370
+ document.querySelectorAll('#moviesGrid .movie-card').forEach((el, i) => {
1371
+ const sel = refType === 'movie' && MOVIES[i] === movie;
1372
+ el.classList.toggle('sel', sel);
1373
+ el.querySelector('.use').textContent = sel ? '✓ SELECTED' : 'SELECT';
1374
+ });
1375
  document.querySelectorAll('#labFilms .film-card').forEach((el, i) =>
1376
+ el.classList.toggle('sel', refType === 'film' && FILMS[i] === film));
1377
  document.querySelectorAll('#labInputs img').forEach((el, i) =>
1378
  el.classList.toggle('sel', INPUTS[i] === input));
1379
+
1380
+ $('pairRef').src = refSrc();
1381
+ $('pairInput').src = input.src;
1382
+ $('pairInfo').innerHTML = '<b>' + refName() + '</b> &nbsp;→&nbsp; <b>' + input.name + '</b>';
1383
+
1384
+ const instant = refType === 'film' && input.lab;
1385
+ $('instantWrap').style.display = instant ? 'block' : 'none';
1386
+ if (instant) {
1387
+ const cmp = $('labCompare');
1388
+ cmp.querySelector('.base-img').src = 'static/lab/' + input.id + '_' + film.id + '.jpg';
1389
+ cmp.querySelector('.top-img').src = input.src;
1390
+ $('labCaption').innerHTML = '<b>' + input.name + '</b> rendered as <b>' + film.name + '</b>';
1391
+ }
1392
  }
1393
 
1394
+ function setTab(t) {
1395
+ refType = t;
1396
+ $('tabMovies').classList.toggle('sel', t === 'movie');
1397
+ $('tabFilms').classList.toggle('sel', t === 'film');
1398
+ $('moviesGrid').style.display = t === 'movie' ? 'grid' : 'none';
1399
+ $('labFilms').style.display = t === 'film' ? 'grid' : 'none';
1400
+ $('filmsHint').style.display = t === 'film' ? 'block' : 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1401
  refresh();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1402
  }
1403
+ $('tabMovies').addEventListener('click', () => setTab('movie'));
1404
+ $('tabFilms').addEventListener('click', () => setTab('film'));
1405
+
1406
+ MOVIES.forEach(m => {
1407
+ const card = document.createElement('div');
1408
+ card.className = 'movie-card';
1409
+ card.innerHTML =
1410
+ '<img class="still" loading="lazy" alt="">' +
1411
+ '<span class="use">SELECT</span>' +
1412
+ '<div class="cap"><div class="title"></div><div class="meta"></div></div>';
1413
+ card.querySelector('img').src = m.file;
1414
+ card.querySelector('img').alt = m.title;
1415
+ card.querySelector('.title').textContent = m.title;
1416
+ card.querySelector('.meta').textContent = m.meta;
1417
+ card.addEventListener('click', () => { movie = m; refresh(); });
1418
+ $('moviesGrid').appendChild(card);
1419
+ });
1420
+
1421
+ FILMS.forEach(f => {
1422
+ const card = document.createElement('div');
1423
+ card.className = 'film-card';
1424
+ card.innerHTML =
1425
+ '<img class="thumb" loading="lazy" alt="" src="static/lab/ref_' + f.id + '.jpg">' +
1426
+ '<div class="meta">' + canisterSvg(f) +
1427
+ '<a class="stock" target="_blank" rel="noopener">' + f.name +
1428
+ ' <span class="ext">↗</span></a></div>';
1429
+ const a = card.querySelector('a.stock');
1430
+ a.href = f.url;
1431
+ a.title = 'About ' + f.name;
1432
+ a.addEventListener('click', e => e.stopPropagation());
1433
+ card.addEventListener('click', () => { film = f; refresh(); });
1434
+ $('labFilms').appendChild(card);
1435
+ });
1436
 
1437
+ INPUTS.forEach(inp => {
1438
+ const im = document.createElement('img');
1439
+ im.src = inp.src;
1440
+ im.loading = 'lazy';
1441
+ im.alt = inp.name; im.title = inp.name;
1442
+ im.addEventListener('click', () => { input = inp; refresh(); });
1443
+ $('labInputs').appendChild(im);
1444
+ });
1445
+
1446
+ initCompare($('labCompare'));
1447
+ setTab('movie');
1448
+
1449
+ $('runPairBtn').addEventListener('click', async () => {
1450
+ try {
1451
+ $('runPairBtn').textContent = 'Loading…';
1452
+ const [rb, tb] = await Promise.all([
1453
+ fetch(refSrc()).then(r => r.blob()),
1454
+ fetch(input.src).then(r => r.blob()),
1455
+ ]);
1456
+ refFile = new File([rb], 'reference.jpg', { type: 'image/jpeg' });
1457
+ tgtFile = new File([tb], 'photo.jpg', { type: 'image/jpeg' });
1458
+ fillZone('refZone', refFile, () => { refFile = null; clearZone('refZone'); update(); });
1459
+ fillZone('tgtZone', tgtFile, () => { tgtFile = null; clearZone('tgtZone'); update(); });
1460
+ update();
1461
+ window.scrollTo({ top: 0, behavior: 'smooth' });
1462
+ } finally {
1463
+ $('runPairBtn').textContent = 'Run this pair in the app →';
1464
+ }
1465
+ });
1466
  })();
1467
 
1468
  // ------------------------------------------------ privacy modal
static/lab/tar_1.jpg ADDED

Git LFS Details

  • SHA256: 1f325d40add4f285e2894f0484108194f9fd6bd77fe76c672d8abe8d052594e6
  • Pointer size: 131 Bytes
  • Size of remote file: 184 kB
static/lab/tar_4.jpg ADDED

Git LFS Details

  • SHA256: 30aaf7a43db78e23db8b08a7482da3eced0900f266567b6dd60f9a45fe690a30
  • Pointer size: 131 Bytes
  • Size of remote file: 194 kB
static/lab/tar_6.jpg ADDED

Git LFS Details

  • SHA256: 37125db1fe0db31dc0bee5ab75bad19bf8b3e2a827ef57975499d66da515f3ef
  • Pointer size: 131 Bytes
  • Size of remote file: 304 kB