feat(demo): click any tile to view image at target display size#7
Conversation
Adds a modal that pops up when you click a tile. The modal renders the encoded image at its actual target width (capped to viewport when needed) — letting you see what the user would actually see at that display size, separate from how the tile shrinks the image to fit the grid. The modal makes the canon mechanism visible: - For target-binding tiles: the proxy encoded at target × 1.5; the browser downscales from there to target for display. That downscale IS the artifact filter — same mechanism that yields soft blur instead of blocking artifacts. - For source-binding tiles: explains that source × 1.5 capped to prevent manufacturing pixels from no signal; the browser upscales modestly from the encode to the target display. - For equal-binding tiles: no scaling work at the encoder. UX: - Click tile or Enter/Space when tile is focused → open modal - Click backdrop, X button, or Esc → close - Tiles get role=button + tabindex for accessibility - Hover state on tiles signals they're clickable - Hint added to legend: 'click any tile to view at target size' No behavioral change to the proxy or smoke test — just the demo page.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
transcode-mcp | 44ff166 | Commit Preview URL Branch Preview URL |
May 27 2026, 09:54 PM |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Legend hint span gets unwanted ■ bullet prefix
- Scoped the bullet rule to only
.target,.source, and.equalchildren so the unclassed hint span no longer receives the ■ prefix.
- Scoped the bullet rule to only
- ✅ Fixed: Source-binding explanation incorrectly claims browser is upscaling
- The explanation now picks
upscaling,downscaling, orrendering 1:1based on comparingencodeWtodisplayWidthinstead of always saying upscaling.
- The explanation now picks
Preview (6ff3c609ea)
diff --git a/src/demo-page.ts b/src/demo-page.ts
--- a/src/demo-page.ts
+++ b/src/demo-page.ts
@@ -65,7 +65,11 @@
.tile {
background: var(--panel); border: 1px solid var(--border);
border-radius: 8px; overflow: hidden; display: flex; flex-direction: column;
+ cursor: pointer; transition: border-color 0.15s, transform 0.15s;
}
+ .tile:hover { border-color: var(--accent); transform: translateY(-1px); }
+ .tile.loading, .tile.error { cursor: default; }
+ .tile.loading:hover, .tile.error:hover { border-color: var(--border); transform: none; }
.tile-header {
padding: 10px 12px; border-bottom: 1px solid var(--border);
background: var(--panel-2); font-size: 13px; font-weight: 600;
@@ -89,6 +93,74 @@
}
.file-size.ok { color: var(--accent-2); }
.file-size.high { color: var(--warn); }
+
+ /* Modal */
+ .modal-backdrop {
+ position: fixed; inset: 0;
+ background: rgba(0,0,0,0.85);
+ display: none;
+ z-index: 100;
+ overflow-y: auto;
+ padding: 24px;
+ }
+ .modal-backdrop.open { display: flex; align-items: flex-start; justify-content: center; }
+ .modal {
+ background: var(--panel); border: 1px solid var(--border);
+ border-radius: 12px;
+ max-width: calc(100vw - 48px);
+ width: auto;
+ margin: auto;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.5);
+ display: flex; flex-direction: column;
+ }
+ .modal-header {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 14px 18px;
+ border-bottom: 1px solid var(--border);
+ gap: 16px;
+ }
+ .modal-title { font-size: 15px; font-weight: 600; }
+ .modal-title .target { color: var(--accent); }
+ .modal-title .display-info { color: var(--text-dim); font-weight: 400; font-size: 12px; margin-left: 8px; }
+ .modal-close {
+ background: var(--panel-2); border: 1px solid var(--border);
+ color: var(--text); width: 32px; height: 32px;
+ border-radius: 6px; cursor: pointer; font-size: 18px;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+ }
+ .modal-close:hover { background: var(--border); }
+ .modal-image-wrap {
+ background: #000; display: flex; align-items: center; justify-content: center;
+ padding: 0; max-height: calc(100vh - 240px);
+ overflow: auto;
+ }
+ .modal-image-wrap img {
+ display: block; height: auto;
+ /* width is set inline by JS to match target (or viewport cap) */
+ }
+ .modal-meta {
+ padding: 14px 18px;
+ font-family: var(--mono); font-size: 12px;
+ color: var(--text-dim);
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 8px 24px;
+ border-top: 1px solid var(--border);
+ }
+ .modal-meta .row { display: flex; justify-content: space-between; gap: 8px; }
+ .modal-meta .row strong { color: var(--text); font-weight: 500; }
+ .modal-explanation {
+ padding: 14px 18px;
+ font-size: 12px;
+ color: var(--text-dim);
+ border-top: 1px solid var(--border);
+ line-height: 1.6;
+ }
+ .modal-explanation code {
+ background: var(--panel-2); padding: 1px 5px;
+ border-radius: 3px; font-size: 11px;
+ color: var(--text);
+ }
details { margin-top: 24px; }
summary { cursor: pointer; color: var(--text-dim); font-size: 13px; padding: 8px 0; }
details[open] summary { color: var(--text); }
@@ -102,7 +174,9 @@
display: flex; gap: 16px; flex-wrap: wrap; font-size: 11px;
color: var(--text-dim); margin-top: 12px;
}
- .legend span::before {
+ .legend .target::before,
+ .legend .source::before,
+ .legend .equal::before {
content: "■"; margin-right: 4px;
}
.legend .target::before { color: var(--accent); }
@@ -165,10 +239,26 @@
<div id="grid" class="grid"></div>
+<div id="modal-backdrop" class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-title">
+ <div class="modal" id="modal">
+ <div class="modal-header">
+ <div class="modal-title" id="modal-title">
+ <span class="target">target ?px</span>
+ <span class="display-info"></span>
+ </div>
+ <button class="modal-close" id="modal-close" aria-label="Close">×</button>
+ </div>
+ <div class="modal-image-wrap" id="modal-image-wrap"></div>
+ <div class="modal-meta" id="modal-meta"></div>
+ <div class="modal-explanation" id="modal-explanation"></div>
+ </div>
+</div>
+
<div class="legend">
<span class="target">target × 1.5 binds (normal case)</span>
<span class="source">source × 1.5 binds (tiny-source cap)</span>
<span class="equal">source equals target</span>
+ <span style="color: var(--accent); margin-left: auto;">→ click any tile to view at target size</span>
</div>
<details>
@@ -289,6 +379,22 @@
tile.classList.remove('loading');
tile.classList.add(bindingClass(binding));
+ // Store metadata for the modal
+ tile.dataset.target = String(target);
+ tile.dataset.path = path;
+ tile.dataset.sourceW = sourceW || '';
+ tile.dataset.sourceH = sourceH || '';
+ tile.dataset.encodeW = encodeW || '';
+ tile.dataset.encodeH = encodeH || '';
+ tile.dataset.binding = binding;
+ tile.dataset.quality = quality || '';
+ tile.dataset.format = format;
+ tile.dataset.cache = cache;
+ tile.dataset.size = String(result.size);
+ tile.setAttribute('role', 'button');
+ tile.setAttribute('tabindex', '0');
+ tile.setAttribute('aria-label', 'Open ' + target + 'px preview');
+
const img = document.createElement('img');
img.src = path;
img.alt = 'target ' + target;
@@ -322,6 +428,121 @@
}[c]));
}
+// Modal
+const modalBackdrop = document.getElementById('modal-backdrop');
+const modalTitle = document.getElementById('modal-title');
+const modalImageWrap = document.getElementById('modal-image-wrap');
+const modalMeta = document.getElementById('modal-meta');
+const modalExplanation = document.getElementById('modal-explanation');
+const modalCloseBtn = document.getElementById('modal-close');
+
+function openModalForTile(tile) {
+ if (tile.classList.contains('loading') || tile.classList.contains('error')) return;
+ const d = tile.dataset;
+ const target = parseInt(d.target, 10);
+ const encodeW = parseInt(d.encodeW || '0', 10);
+ const encodeH = parseInt(d.encodeH || '0', 10);
+ const sourceW = d.sourceW;
+ const sourceH = d.sourceH;
+ const binding = d.binding;
+ const quality = d.quality;
+ const format = d.format;
+ const cache = d.cache;
+ const size = parseInt(d.size || '0', 10);
+ const path = d.path;
+
+ // Cap display width to viewport (with a margin for modal padding/scrollbars)
+ const viewportCap = window.innerWidth - 80;
+ const displayWidth = Math.min(target, viewportCap);
+ const isClamped = displayWidth < target;
+
+ modalTitle.innerHTML =
+ '<span class="target">target ' + target + 'px</span>' +
+ '<span class="display-info">' +
+ (isClamped
+ ? 'displayed at ' + displayWidth + 'px (your viewport is narrower than target)'
+ : 'displayed at ' + target + 'px (1:1 with target)') +
+ '</span>';
+
+ // Render the image at target width — the browser does the final downscale
+ // from the encode dimensions (encodeW × encodeH) to this display size.
+ // That downscale IS the artifact-filter canon describes.
+ const img = document.createElement('img');
+ img.src = path;
+ img.alt = 'target ' + target + ' preview';
+ img.style.width = displayWidth + 'px';
+ modalImageWrap.innerHTML = '';
+ modalImageWrap.appendChild(img);
+
+ modalMeta.innerHTML =
+ '<div class="row"><span>source</span><strong>' + (sourceW || '?') + ' × ' + (sourceH || '?') + '</strong></div>' +
+ '<div class="row"><span>encode</span><strong>' + (encodeW || '?') + ' × ' + (encodeH || '?') + '</strong></div>' +
+ '<div class="row"><span>display</span><strong>' + displayWidth + 'px wide</strong></div>' +
+ '<div class="row"><span>binds</span><strong>' + (binding || '—') + '</strong></div>' +
+ '<div class="row"><span>quality</span><strong>q=' + (quality || '?') + '</strong></div>' +
+ '<div class="row"><span>format</span><strong>' + (format || '?') + '</strong></div>' +
+ '<div class="row"><span>size</span><strong>' + formatBytes(size) + '</strong></div>' +
+ '<div class="row"><span>cache</span><strong>' + (cache || '—') + '</strong></div>';
+
+ // Explanation tailored to which term bound
+ let explain = '';
+ if (binding === 'target') {
+ explain =
+ 'The proxy encoded this image at <code>' + encodeW + '×' + encodeH + '</code> ' +
+ '(target × 1.5, mod-16 aligned). Your browser is downscaling that to ' + displayWidth + 'px ' +
+ 'for display — that downscale is the artifact filter, the same mechanism canon describes for ' +
+ '"control the character of the loss."';
+ } else if (binding === 'source') {
+ const scaleVerb = encodeW < displayWidth ? 'upscaling' : encodeW > displayWidth ? 'downscaling' : 'rendering 1:1';
+ const scaleClause = encodeW === displayWidth
+ ? 'and your browser is rendering 1:1 at ' + displayWidth + 'px'
+ : 'and your browser is ' + scaleVerb + ' from ' + encodeW + 'px to ' + displayWidth + 'px for display';
+ explain =
+ 'Source is small enough that <code>source × 1.5 = ' + encodeW + 'px</code> binds instead of ' +
+ 'target × 1.5. The proxy encoded at the modest overshoot, ' + scaleClause + '. ' +
+ 'Without the <code>source × 1.5</code> cap, this would have manufactured pixels from no signal.';
+ } else if (binding === 'equal') {
+ explain =
+ 'Source dimensions already match the target. No scaling at the encoder; the only work is ' +
+ 'format conversion and quality adjustment.';
+ }
+ modalExplanation.innerHTML = explain;
+
+ modalBackdrop.classList.add('open');
+ // Focus close button for keyboard accessibility
+ modalCloseBtn.focus();
+}
+
+function closeModal() {
+ modalBackdrop.classList.remove('open');
+ // Free the image so it doesn't stay in memory
+ modalImageWrap.innerHTML = '';
+}
+
+// Tile click → open modal
+grid.addEventListener('click', (e) => {
+ const tile = e.target.closest('.tile');
+ if (tile) openModalForTile(tile);
+});
+grid.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ const tile = e.target.closest('.tile');
+ if (tile) {
+ e.preventDefault();
+ openModalForTile(tile);
+ }
+ }
+});
+
+// Close handlers
+modalCloseBtn.addEventListener('click', closeModal);
+modalBackdrop.addEventListener('click', (e) => {
+ if (e.target === modalBackdrop) closeModal();
+});
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && modalBackdrop.classList.contains('open')) closeModal();
+});
+
reloadBtn.addEventListener('click', loadGrid);
sourceSelect.addEventListener('change', () => { customUrl.value = ''; loadGrid(); });
customUrl.addEventListener('change', loadGrid);You can send follow-ups to the cloud agent here.
Three changes that work together to make the demo honest: 1. **Curated, verified test images.** Replaced random picsum.photos sources with Unsplash images I visually verified contain what the labels claim — a person reading a newspaper (real text on paper), a wall of open books (dense small text), tax forms with calculator (mixed text/objects), a portrait of a person (face, fabric pattern), a field of poppies (canon's 'confetti' case), a library (portrait detail). Plus Unsplash's ?w=N parameter is used for the source-near-target case (850×600) to exercise canon Example 2 precisely, and the small-thumbnail case (400×300) to exercise the source × 1.5 binding cleanly. 2. **Baseline tile.** A distinct tile above the grid shows the unmodified source through the proxy's passthrough path (no options applied means the worker streams origin bytes through unchanged). Displays source dimensions, original file size, and format. Click it to open in the modal at native size. Gives an honest comparison point — every transcoded tile below is what the proxy delivers relative to this. 3. **Query-string fix in URL parser.** The previous parser stripped query strings from the source URL because new URL(req.url).pathname doesn't include search. With Unsplash URLs like /image/w=800/https://images.unsplash.com/photo-X?w=2000, the ?w=2000 was being lost — the worker fetched the natural 5472×3648 instead of the requested 2000-wide version. Fixed parseProxyPath to accept a second 'search' argument and reattach it to the source URL. Added a test for the case. Worker now passes both pathname and search. Tests: 29 pass / 0 fail (was 28; added 1 for the search-reattach case). Typecheck clean.
Updated scope — additional changes pushedPushed two more commits to this branch to address feedback. The PR is now broader than just the modal feature, but the changes are coherent: What is now in the PR
Why these landed togetherThe new test image URLs include Unsplash Tests29 pass / 0 fail (was 28; added 1 for the search-reattach case). Typecheck clean. Head SHA: c516ecf. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Baseline tile clickable before dimensions are known
- Moved tile.classList.remove('loading') into the img.onload callback so the tile only becomes clickable after sourceW/sourceH dataset values are populated.
Preview (44ff1664ec)
diff --git a/src/demo-page.ts b/src/demo-page.ts
--- a/src/demo-page.ts
+++ b/src/demo-page.ts
@@ -65,7 +65,11 @@
.tile {
background: var(--panel); border: 1px solid var(--border);
border-radius: 8px; overflow: hidden; display: flex; flex-direction: column;
+ cursor: pointer; transition: border-color 0.15s, transform 0.15s;
}
+ .tile:hover { border-color: var(--accent); transform: translateY(-1px); }
+ .tile.loading, .tile.error { cursor: default; }
+ .tile.loading:hover, .tile.error:hover { border-color: var(--border); transform: none; }
.tile-header {
padding: 10px 12px; border-bottom: 1px solid var(--border);
background: var(--panel-2); font-size: 13px; font-weight: 600;
@@ -89,6 +93,125 @@
}
.file-size.ok { color: var(--accent-2); }
.file-size.high { color: var(--warn); }
+
+ /* Baseline tile — the unmodified source for comparison */
+ .baseline-wrap { margin-bottom: 20px; }
+ .baseline-tile {
+ background: var(--panel); border: 1px solid var(--border);
+ border-radius: 8px; overflow: hidden;
+ display: grid; grid-template-columns: 1fr 1fr;
+ cursor: pointer; transition: border-color 0.15s;
+ }
+ .baseline-tile:hover { border-color: var(--accent-2); }
+ .baseline-tile.loading, .baseline-tile.error { cursor: default; }
+ .baseline-tile.loading:hover, .baseline-tile.error:hover { border-color: var(--border); }
+ .baseline-tile .baseline-image {
+ background: #000;
+ display: flex; align-items: center; justify-content: center;
+ min-height: 200px; max-height: 360px; overflow: hidden;
+ }
+ .baseline-tile .baseline-image img {
+ max-width: 100%; max-height: 360px; display: block; object-fit: contain;
+ }
+ .baseline-tile .baseline-info {
+ padding: 18px 20px;
+ display: flex; flex-direction: column; gap: 10px;
+ }
+ .baseline-tile .baseline-label {
+ color: var(--accent-2); font-size: 11px; font-weight: 600;
+ text-transform: uppercase; letter-spacing: 0.08em;
+ }
+ .baseline-tile h2 {
+ margin: 0; font-size: 16px; font-weight: 600;
+ }
+ .baseline-tile .baseline-meta {
+ margin-top: 6px;
+ font-family: var(--mono); font-size: 11px;
+ color: var(--text-dim);
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ gap: 6px 16px;
+ }
+ .baseline-tile .baseline-meta .row { display: flex; justify-content: space-between; gap: 8px; }
+ .baseline-tile .baseline-meta strong { color: var(--text); font-weight: 500; }
+ .baseline-tile .baseline-note {
+ font-size: 11px; color: var(--text-dim); line-height: 1.5;
+ border-top: 1px solid var(--border); padding-top: 10px; margin-top: 4px;
+ }
+ .baseline-tile.error .baseline-image { background: #2a1a1a; color: #ff8b8b; font-size: 12px; padding: 16px; text-align: center; }
+ .baseline-tile.loading .baseline-image::before {
+ content: "loading…"; color: var(--text-dim); font-size: 12px;
+ }
+ @media (max-width: 720px) {
+ .baseline-tile { grid-template-columns: 1fr; }
+ }
+
+ /* Modal */
+ .modal-backdrop {
+ position: fixed; inset: 0;
+ background: rgba(0,0,0,0.85);
+ display: none;
+ z-index: 100;
+ overflow-y: auto;
+ padding: 24px;
+ }
+ .modal-backdrop.open { display: flex; align-items: flex-start; justify-content: center; }
+ .modal {
+ background: var(--panel); border: 1px solid var(--border);
+ border-radius: 12px;
+ max-width: calc(100vw - 48px);
+ width: auto;
+ margin: auto;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.5);
+ display: flex; flex-direction: column;
+ }
+ .modal-header {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 14px 18px;
+ border-bottom: 1px solid var(--border);
+ gap: 16px;
+ }
+ .modal-title { font-size: 15px; font-weight: 600; }
+ .modal-title .target { color: var(--accent); }
+ .modal-title .display-info { color: var(--text-dim); font-weight: 400; font-size: 12px; margin-left: 8px; }
+ .modal-close {
+ background: var(--panel-2); border: 1px solid var(--border);
+ color: var(--text); width: 32px; height: 32px;
+ border-radius: 6px; cursor: pointer; font-size: 18px;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+ }
+ .modal-close:hover { background: var(--border); }
+ .modal-image-wrap {
+ background: #000; display: flex; align-items: center; justify-content: center;
+ padding: 0; max-height: calc(100vh - 240px);
+ overflow: auto;
+ }
+ .modal-image-wrap img {
+ display: block; height: auto;
+ /* width is set inline by JS to match target (or viewport cap) */
+ }
+ .modal-meta {
+ padding: 14px 18px;
+ font-family: var(--mono); font-size: 12px;
+ color: var(--text-dim);
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 8px 24px;
+ border-top: 1px solid var(--border);
+ }
+ .modal-meta .row { display: flex; justify-content: space-between; gap: 8px; }
+ .modal-meta .row strong { color: var(--text); font-weight: 500; }
+ .modal-explanation {
+ padding: 14px 18px;
+ font-size: 12px;
+ color: var(--text-dim);
+ border-top: 1px solid var(--border);
+ line-height: 1.6;
+ }
+ .modal-explanation code {
+ background: var(--panel-2); padding: 1px 5px;
+ border-radius: 3px; font-size: 11px;
+ color: var(--text);
+ }
details { margin-top: 24px; }
summary { cursor: pointer; color: var(--text-dim); font-size: 13px; padding: 8px 0; }
details[open] summary { color: var(--text); }
@@ -102,7 +225,9 @@
display: flex; gap: 16px; flex-wrap: wrap; font-size: 11px;
color: var(--text-dim); margin-top: 12px;
}
- .legend span::before {
+ .legend .target::before,
+ .legend .source::before,
+ .legend .equal::before {
content: "■"; margin-right: 4px;
}
.legend .target::before { color: var(--accent); }
@@ -126,13 +251,16 @@
<div class="control-group">
<label for="source-select">Source image</label>
<select id="source-select">
- <option value="https://picsum.photos/id/1015/4000/3000">Phone photo, 4000×3000 (dominant case)</option>
- <option value="https://picsum.photos/id/1043/1920/1080">Scripture screenshot, 1920×1080 (near-target)</option>
- <option value="https://picsum.photos/id/237/2400/1600">Photograph with text, 2400×1600</option>
- <option value="https://picsum.photos/id/1062/400/300">Pericope thumbnail, 400×300 (small source)</option>
- <option value="https://picsum.photos/id/1084/200/300">Portrait thumbnail, 200×300 (tall aspect)</option>
- <option value="https://picsum.photos/id/1059/64/64">Icon, 64×64 (tiny source)</option>
- <option value="https://picsum.photos/id/237/16/16">Pixel art, 16×16 (cap kicks in)</option>
+ <option value="https://images.unsplash.com/photo-1495020689067-958852a7765e">Person reading newspaper, 6016×4016 (text on paper, real-world test)</option>
+ <option value="https://images.unsplash.com/photo-1457369804613-52c61a468e7d">Wall of open books, 5472×3648 (dense small text)</option>
+ <option value="https://images.unsplash.com/photo-1554224155-6726b3ff858f">Tax forms with calculator, 5563×3192 (forms, text, mixed objects)</option>
+ <option value="https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e">Portrait of person, 3840×5760 (face detail, fabric pattern)</option>
+ <option value="https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07">Field of poppies, 4928×3264 (the "confetti" case from canon)</option>
+ <option value="https://images.unsplash.com/photo-1568667256549-094345857637">Library bookshelves, 4000×5600 (portrait, fine detail)</option>
+ <option value="https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=850&h=600&fit=crop">Source near target, 850×600 (canon Example 2)</option>
+ <option value="https://images.unsplash.com/photo-1495020689067-958852a7765e?w=400&h=300&fit=crop">Small thumbnail, 400×300 (source × 1.5 binds)</option>
+ <option value="https://picsum.photos/id/1059/64/64">Tiny icon, 64×64 (cap kicks in)</option>
+ <option value="https://picsum.photos/id/237/16/16">Pixel art, 16×16 (extreme cap case)</option>
</select>
</div>
<div class="control-group">
@@ -163,12 +291,30 @@
<div id="source-info" class="source-info">Loading source…</div>
+<div id="baseline-wrap" class="baseline-wrap"></div>
+
<div id="grid" class="grid"></div>
+<div id="modal-backdrop" class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-title">
+ <div class="modal" id="modal">
+ <div class="modal-header">
+ <div class="modal-title" id="modal-title">
+ <span class="target">target ?px</span>
+ <span class="display-info"></span>
+ </div>
+ <button class="modal-close" id="modal-close" aria-label="Close">×</button>
+ </div>
+ <div class="modal-image-wrap" id="modal-image-wrap"></div>
+ <div class="modal-meta" id="modal-meta"></div>
+ <div class="modal-explanation" id="modal-explanation"></div>
+ </div>
+</div>
+
<div class="legend">
<span class="target">target × 1.5 binds (normal case)</span>
<span class="source">source × 1.5 binds (tiny-source cap)</span>
<span class="equal">source equals target</span>
+ <span style="color: var(--accent); margin-left: auto;">→ click any tile to view at target size</span>
</div>
<details>
@@ -247,6 +393,89 @@
return 'ok';
}
+const baselineWrap = document.getElementById('baseline-wrap');
+
+async function loadBaseline(source) {
+ // Passthrough URL: no options, worker just streams the source bytes through.
+ // This gives us source dimensions and original file size as a baseline for
+ // comparing what the transcoded tiles deliver.
+ const path = '/image/' + source;
+ const fullUrl = window.location.origin + path;
+
+ // Skeleton
+ baselineWrap.innerHTML = \`
+ <div class="baseline-tile loading">
+ <div class="baseline-image"></div>
+ <div class="baseline-info">
+ <span class="baseline-label">Baseline — Original Source</span>
+ <h2>Loading source…</h2>
+ <div class="baseline-meta"></div>
+ </div>
+ </div>
+ \`;
+
+ const tile = baselineWrap.querySelector('.baseline-tile');
+
+ try {
+ const result = await fetchHead(fullUrl);
+ const h = result.headers;
+ const contentType = (h['content-type'] || 'image/?').replace('image/', '');
+ const cache = h['x-transcode-cache'] || '—';
+ const encodeMarker = h['x-transcode-encode'] || '';
+
+ // For the baseline we need the natural dimensions of the source. The
+ // passthrough path doesn't set X-Transcode-Source-* headers (because no
+ // transform was attempted), so we measure from the loaded image instead.
+ // We'll fill these in once the image loads.
+ tile.dataset.path = path;
+ tile.dataset.size = String(result.size);
+ tile.dataset.cache = cache;
+ tile.dataset.format = contentType;
+ tile.setAttribute('role', 'button');
+ tile.setAttribute('tabindex', '0');
+ tile.setAttribute('aria-label', 'Open original source preview');
+
+ const img = document.createElement('img');
+ img.src = path;
+ img.alt = 'original source';
+ img.onload = () => {
+ tile.dataset.sourceW = String(img.naturalWidth);
+ tile.dataset.sourceH = String(img.naturalHeight);
+ tile.classList.remove('loading');
+ const heading = tile.querySelector('h2');
+ heading.innerHTML = \`Original source <span style="color: var(--text-dim); font-weight: 400; font-size: 13px;">\${img.naturalWidth} × \${img.naturalHeight}</span>\`;
+ const meta = tile.querySelector('.baseline-meta');
+ meta.innerHTML = \`
+ <div class="row"><span>dimensions</span><strong>\${img.naturalWidth} × \${img.naturalHeight}</strong></div>
+ <div class="row"><span>format</span><strong>\${contentType}</strong></div>
+ <div class="row"><span>size</span><strong>\${formatBytes(result.size)}</strong></div>
+ <div class="row"><span>delivery</span><strong>\${encodeMarker || 'passthrough'}</strong></div>
+ <div class="row"><span>cache</span><strong>\${cache}</strong></div>
+ \`;
+ };
+ img.onerror = () => {
+ tile.classList.add('error');
+ tile.querySelector('.baseline-image').textContent = 'failed to load source';
+ };
+ tile.querySelector('.baseline-image').appendChild(img);
+
+ // Add a small note explaining the comparison
+ const info = tile.querySelector('.baseline-info');
+ const note = document.createElement('div');
+ note.className = 'baseline-note';
+ note.textContent =
+ 'Served through the proxy without any options applied — the worker ' +
+ 'streams the origin bytes through unchanged. This is the comparison ' +
+ 'baseline: every transcoded tile below is what the proxy delivers when ' +
+ 'asked to transform this source for the corresponding target width.';
+ info.appendChild(note);
+ } catch (err) {
+ tile.classList.remove('loading');
+ tile.classList.add('error');
+ tile.querySelector('.baseline-image').textContent = 'failed: ' + err.message;
+ }
+}
+
async function loadGrid() {
const source = currentSource();
const q = qualitySelect.value;
@@ -255,6 +484,10 @@
sourceInfo.innerHTML = 'Source: <strong>' + escapeHtml(source) + '</strong>';
grid.innerHTML = '';
+ // Build and load the baseline tile (source through proxy passthrough — no
+ // options applied means the worker streams the source unmodified).
+ loadBaseline(source);
+
// Build tiles up front so they appear immediately
const tiles = TARGETS.map(target => {
const tile = document.createElement('div');
@@ -289,6 +522,22 @@
tile.classList.remove('loading');
tile.classList.add(bindingClass(binding));
+ // Store metadata for the modal
+ tile.dataset.target = String(target);
+ tile.dataset.path = path;
+ tile.dataset.sourceW = sourceW || '';
+ tile.dataset.sourceH = sourceH || '';
+ tile.dataset.encodeW = encodeW || '';
+ tile.dataset.encodeH = encodeH || '';
+ tile.dataset.binding = binding;
+ tile.dataset.quality = quality || '';
+ tile.dataset.format = format;
+ tile.dataset.cache = cache;
+ tile.dataset.size = String(result.size);
+ tile.setAttribute('role', 'button');
+ tile.setAttribute('tabindex', '0');
+ tile.setAttribute('aria-label', 'Open ' + target + 'px preview');
+
const img = document.createElement('img');
img.src = path;
img.alt = 'target ' + target;
@@ -322,6 +571,185 @@
}[c]));
}
+// Modal
+const modalBackdrop = document.getElementById('modal-backdrop');
+const modalTitle = document.getElementById('modal-title');
+const modalImageWrap = document.getElementById('modal-image-wrap');
+const modalMeta = document.getElementById('modal-meta');
+const modalExplanation = document.getElementById('modal-explanation');
+const modalCloseBtn = document.getElementById('modal-close');
+
+function openModalForTile(tile) {
+ if (tile.classList.contains('loading') || tile.classList.contains('error')) return;
+ const d = tile.dataset;
+ const target = parseInt(d.target, 10);
+ const encodeW = parseInt(d.encodeW || '0', 10);
+ const encodeH = parseInt(d.encodeH || '0', 10);
+ const sourceW = d.sourceW;
+ const sourceH = d.sourceH;
+ const binding = d.binding;
+ const quality = d.quality;
+ const format = d.format;
+ const cache = d.cache;
+ const size = parseInt(d.size || '0', 10);
+ const path = d.path;
+
+ // Cap display width to viewport (with a margin for modal padding/scrollbars)
+ const viewportCap = window.innerWidth - 80;
+ const displayWidth = Math.min(target, viewportCap);
+ const isClamped = displayWidth < target;
+
+ modalTitle.innerHTML =
+ '<span class="target">target ' + target + 'px</span>' +
+ '<span class="display-info">' +
+ (isClamped
+ ? 'displayed at ' + displayWidth + 'px (your viewport is narrower than target)'
+ : 'displayed at ' + target + 'px (1:1 with target)') +
+ '</span>';
+
+ // Render the image at target width — the browser does the final downscale
+ // from the encode dimensions (encodeW × encodeH) to this display size.
+ // That downscale IS the artifact-filter canon describes.
+ const img = document.createElement('img');
+ img.src = path;
+ img.alt = 'target ' + target + ' preview';
+ img.style.width = displayWidth + 'px';
+ modalImageWrap.innerHTML = '';
+ modalImageWrap.appendChild(img);
+
+ modalMeta.innerHTML =
+ '<div class="row"><span>source</span><strong>' + (sourceW || '?') + ' × ' + (sourceH || '?') + '</strong></div>' +
+ '<div class="row"><span>encode</span><strong>' + (encodeW || '?') + ' × ' + (encodeH || '?') + '</strong></div>' +
+ '<div class="row"><span>display</span><strong>' + displayWidth + 'px wide</strong></div>' +
+ '<div class="row"><span>binds</span><strong>' + (binding || '—') + '</strong></div>' +
+ '<div class="row"><span>quality</span><strong>q=' + (quality || '?') + '</strong></div>' +
+ '<div class="row"><span>format</span><strong>' + (format || '?') + '</strong></div>' +
+ '<div class="row"><span>size</span><strong>' + formatBytes(size) + '</strong></div>' +
+ '<div class="row"><span>cache</span><strong>' + (cache || '—') + '</strong></div>';
+
+ // Explanation tailored to which term bound
+ let explain = '';
+ if (binding === 'target') {
+ explain =
+ 'The proxy encoded this image at <code>' + encodeW + '×' + encodeH + '</code> ' +
+ '(target × 1.5, mod-16 aligned). Your browser is downscaling that to ' + displayWidth + 'px ' +
+ 'for display — that downscale is the artifact filter, the same mechanism canon describes for ' +
+ '"control the character of the loss."';
+ } else if (binding === 'source') {
+ const scaleVerb = encodeW < displayWidth ? 'upscaling' : encodeW > displayWidth ? 'downscaling' : 'rendering 1:1';
+ const scaleClause = encodeW === displayWidth
+ ? 'and your browser is rendering 1:1 at ' + displayWidth + 'px'
+ : 'and your browser is ' + scaleVerb + ' from ' + encodeW + 'px to ' + displayWidth + 'px for display';
+ explain =
+ 'Source is small enough that <code>source × 1.5 = ' + encodeW + 'px</code> binds instead of ' +
+ 'target × 1.5. The proxy encoded at the modest overshoot, ' + scaleClause + '. ' +
+ 'Without the <code>source × 1.5</code> cap, this would have manufactured pixels from no signal.';
+ } else if (binding === 'equal') {
+ explain =
+ 'Source dimensions already match the target. No scaling at the encoder; the only work is ' +
+ 'format conversion and quality adjustment.';
+ }
+ modalExplanation.innerHTML = explain;
+
+ modalBackdrop.classList.add('open');
+ // Focus close button for keyboard accessibility
+ modalCloseBtn.focus();
+}
+
+function closeModal() {
+ modalBackdrop.classList.remove('open');
+ // Free the image so it doesn't stay in memory
+ modalImageWrap.innerHTML = '';
+}
+
+function openModalForBaseline(tile) {
+ if (tile.classList.contains('loading') || tile.classList.contains('error')) return;
+ const d = tile.dataset;
+ const sourceW = parseInt(d.sourceW || '0', 10);
+ const sourceH = parseInt(d.sourceH || '0', 10);
+ const format = d.format;
+ const cache = d.cache;
+ const size = parseInt(d.size || '0', 10);
+ const path = d.path;
+
+ // For the baseline, show the source at its native width, capped to viewport
+ const viewportCap = window.innerWidth - 80;
+ const displayWidth = sourceW ? Math.min(sourceW, viewportCap) : viewportCap;
+ const isClamped = sourceW && displayWidth < sourceW;
+
+ modalTitle.innerHTML =
+ '<span class="target" style="color: var(--accent-2);">original source</span>' +
+ '<span class="display-info">' +
+ (sourceW
+ ? (isClamped
+ ? 'displayed at ' + displayWidth + 'px (your viewport is narrower than source ' + sourceW + 'px)'
+ : 'displayed at ' + sourceW + 'px (1:1 with source)')
+ : '') +
+ '</span>';
+
+ const img = document.createElement('img');
+ img.src = path;
+ img.alt = 'original source preview';
+ img.style.width = displayWidth + 'px';
+ modalImageWrap.innerHTML = '';
+ modalImageWrap.appendChild(img);
+
+ modalMeta.innerHTML =
+ '<div class="row"><span>dimensions</span><strong>' + (sourceW || '?') + ' × ' + (sourceH || '?') + '</strong></div>' +
+ '<div class="row"><span>format</span><strong>' + (format || '?') + '</strong></div>' +
+ '<div class="row"><span>size</span><strong>' + formatBytes(size) + '</strong></div>' +
+ '<div class="row"><span>delivery</span><strong>passthrough</strong></div>' +
+ '<div class="row"><span>cache</span><strong>' + (cache || '—') + '</strong></div>';
+
+ modalExplanation.innerHTML =
+ 'This is the unmodified source served through the proxy. The worker is in ' +
+ 'passthrough mode — no <code>w</code>, <code>q</code>, or <code>f</code> options applied, so the bytes ' +
+ 'are streamed from the origin unchanged. Use these dimensions and file size as ' +
+ 'the comparison baseline for the transcoded tiles below.';
+
+ modalBackdrop.classList.add('open');
+ modalCloseBtn.focus();
+}
+
+// Tile click → open modal (regular tiles)
+grid.addEventListener('click', (e) => {
+ const tile = e.target.closest('.tile');
+ if (tile) openModalForTile(tile);
+});
+grid.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ const tile = e.target.closest('.tile');
+ if (tile) {
+ e.preventDefault();
+ openModalForTile(tile);
+ }
+ }
+});
+
+// Baseline tile click → open baseline modal
+baselineWrap.addEventListener('click', (e) => {
+ const tile = e.target.closest('.baseline-tile');
+ if (tile) openModalForBaseline(tile);
+});
+baselineWrap.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ const tile = e.target.closest('.baseline-tile');
+ if (tile) {
+ e.preventDefault();
+ openModalForBaseline(tile);
+ }
+ }
+});
+
+// Close handlers
+modalCloseBtn.addEventListener('click', closeModal);
+modalBackdrop.addEventListener('click', (e) => {
+ if (e.target === modalBackdrop) closeModal();
+});
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && modalBackdrop.classList.contains('open')) closeModal();
+});
+
reloadBtn.addEventListener('click', loadGrid);
sourceSelect.addEventListener('change', () => { customUrl.value = ''; loadGrid(); });
customUrl.addEventListener('change', loadGrid);
diff --git a/src/lib/parse-proxy-path.test.ts b/src/lib/parse-proxy-path.test.ts
--- a/src/lib/parse-proxy-path.test.ts
+++ b/src/lib/parse-proxy-path.test.ts
@@ -41,6 +41,18 @@
);
});
+ test("reattaches search portion when source URL has query string", () => {
+ // When the browser receives /image/w=800/https://cdn.com/img.jpg?w=2000,
+ // the URL parser splits pathname (/image/w=800/https://cdn.com/img.jpg)
+ // from search (?w=2000). The proxy parser must reattach the search to the
+ // source URL, not lose it.
+ const result = parseProxyPath(
+ "/image/w=800/https://cdn.com/img.jpg",
+ "?w=2000",
+ );
+ expect(result.sourceUrl).toBe("https://cdn.com/img.jpg?w=2000");
+ });
+
test("preserves source URL with path segments", () => {
const result = parseProxyPath(
"/image/w=800/https://example.com/path/to/image.jpg",
diff --git a/src/lib/parse-proxy-path.ts b/src/lib/parse-proxy-path.ts
--- a/src/lib/parse-proxy-path.ts
+++ b/src/lib/parse-proxy-path.ts
@@ -36,7 +36,10 @@
}
}
-export function parseProxyPath(pathname: string): ParsedRequest {
+export function parseProxyPath(
+ pathname: string,
+ search: string = "",
+): ParsedRequest {
// Strip leading slash
const trimmed = pathname.startsWith("/") ? pathname.slice(1) : pathname;
@@ -65,7 +68,12 @@
throw new ProxyPathError("No source URL found in path");
}
- const sourceUrl = rest.slice(urlStart);
+ // Reattach search/query string to the source URL. The browser splits the
+ // request URL into pathname+search, but the source URL may legitimately
+ // contain a query string (e.g. ?w=2000 for Unsplash, signed URLs, etc.).
+ // The search portion of the request URL is appended to whatever URL we
+ // extract from the path.
+ const sourceUrl = rest.slice(urlStart) + search;
const optionsSegment = rest.slice(0, urlStart).replace(/\/$/, "");
const options = optionsSegment ? parseOptions(optionsSegment) : {};
diff --git a/src/worker.ts b/src/worker.ts
--- a/src/worker.ts
+++ b/src/worker.ts
@@ -135,7 +135,8 @@
): Promise<Response> {
let parsed;
try {
- parsed = parseProxyPath(new URL(request.url).pathname);
+ const requestUrl = new URL(request.url);
+ parsed = parseProxyPath(requestUrl.pathname, requestUrl.search);
} catch (err) {
if (err instanceof ProxyPathError) {
return new Response(err.message, { status: err.status });
@@ -287,7 +288,8 @@
): Promise<Response> {
let parsed;
try {
- parsed = parseProxyPath(new URL(request.url).pathname);
+ const requestUrl = new URL(request.url);
+ parsed = parseProxyPath(requestUrl.pathname, requestUrl.search);
} catch (err) {
if (err instanceof ProxyPathError) {
return new Response(err.message, { status: err.status });You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit c516ecf. Configure here.

What This Adds
Clicking any tile pops a modal showing the image at its actual target display width (capped to viewport on narrow screens). The tile grid shrinks every image to ~280px so you can compare them; the modal lets you see what the user would actually see at 320px, 1080px, 1920px, etc.
Why This Matters For The Demo
The tiles already show the encode dimensions, file size, and which
× 1.5term bound — but they all squeeze the image to fit a grid cell. You can not actually see the half-class overshoot working until you view the image at the target display size, because that final downscale (encode → display) is the artifact filter canon describes.The modal shows it. For target-binding tiles, the browser downscales from
encode × 1.5to target for display — the artifact filter happens in front of your eyes. For source-binding tiles, you see what a modest upscale of a small source looks like at a large display size.The modal also includes a short canon-aligned explanation tailored to the binding:
UX Details
role=buttonandtabindex=0for keyboard accessibility→ click any tile to view at target sizehint added to the legend so users discover itdisplayed at Xpx (your viewport is narrower than target)so users know when theyre seeing a clamped viewNo Behavioral Changes
This is purely additive to the demo page. The proxy, the canon arithmetic, the smoke test, the tests — none of those changed. 28/28 tests still pass, typecheck still clean.
The modal consumes the same
/image/...URL the tile already loaded, so opening the modal reuses the cached response — no extra proxy work.Note
Low Risk
Proxy URL parsing change is narrow (reattaches request search to source URL) and is covered by a test; demo HTML/JS is additive with no encoder logic changes.
Overview
The demo page gains click-to-preview: grid tiles and a new baseline passthrough tile open a modal that shows the image at target display width (viewport-capped), with binding-specific canon copy and keyboard/backdrop/Esc close. Presets shift to Unsplash URLs (including
?w=/ crop query params), which drove a small proxy parsing fix.parseProxyPathnow takes an optionalsearchsegment and appends it to the extracted source URL so requests like/image/w=800/https://cdn.com/img.jpg?w=2000still fetch the origin with its query string.workerpassespathname+searchfor image and audio routes; a unit test covers the reattach behavior.Reviewed by Cursor Bugbot for commit 44ff166. Bugbot is set up for automated code reviews on this repo. Configure here.