From d165d797ba0ddbdf29dbdade878d3dd1c87dcf35 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Sat, 10 Oct 2026 19:45:04 +0530 Subject: [PATCH 01/11] feat(inspector): open on the top suggested block Entering the Block Inspector from the viewer used to land on an empty "click a block" page; it now starts on the first suggested block. Also drops the sidebar's "Transform" group, which held only a note and no control, and the emoji in the suggested-block category headers (each block already carries its own icon). Co-Authored-By: Claude Opus 5.5 --- web/src/InspectorMode.svelte | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/web/src/InspectorMode.svelte b/web/src/InspectorMode.svelte index 0523120..d61212d 100644 --- a/web/src/InspectorMode.svelte +++ b/web/src/InspectorMode.svelte @@ -45,6 +45,15 @@ } }); + // Opened without a block (from the viewer): start on the top suggestion + // rather than an empty page. + $effect(() => { + const first = appState.suggestedBlocks[0]; + if (first && !untrack(() => appState.inspectedBlock)) { + appState.inspectedBlock = { x: first.x, y: first.y }; + } + }); + // Reactive: re-process when quality/CS/transform changes in inspector $effect(() => { const q = appState.quality; @@ -353,7 +362,7 @@ {#each ['edge', 'texture', 'smooth'] as cat} {#if appState.suggestedBlocks.some(b => b.category === cat)}
- {cat === 'edge' ? 'πŸ”· Edges' : cat === 'texture' ? 'πŸ”Ά Textures' : '🟒 Smooth'} + {cat === 'edge' ? 'Edges' : cat === 'texture' ? 'Textures' : 'Smooth'}
{#each appState.suggestedBlocks.filter(b => b.category === cat) as block} {@const isActive = appState.inspectedBlock?.x === block.x && appState.inspectedBlock?.y === block.y} @@ -409,14 +418,6 @@ -
-
Transform
-

- The block inspector follows the 8Γ—8 DCT used by JPEG. Wavelet - transforms work on the whole image β€” switch to Haar or CDF 9/7 - in the viewer and open the Subbands view to explore them. -

-
From 6289cc85ff5c5ea4342a248597e1d699df015f60 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Sat, 10 Oct 2026 19:45:04 +0530 Subject: [PATCH 02/11] feat(header): quieter header "Codec Explorer" instead of "WASM Codec Explorer"; the privacy badge becomes a small "Runs locally" note and the GitHub link an icon button, so the header stays on one row on phones. Co-Authored-By: Claude Opus 5.5 --- .../lib/components/viewer/AppHeader.svelte | 18 ++++++------ web/src/styles/header.css | 28 +++++++------------ 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/web/src/lib/components/viewer/AppHeader.svelte b/web/src/lib/components/viewer/AppHeader.svelte index d27b0a5..4fae33b 100644 --- a/web/src/lib/components/viewer/AppHeader.svelte +++ b/web/src/lib/components/viewer/AppHeader.svelte @@ -17,19 +17,19 @@
-

WASM Codec Explorer

+

Codec Explorer

-
- + + - Processed locally on your device -
- - GitHub Logo - View Source on GitHub + Runs locally + + + - {/if} -
- - {#if !appState.originalImageData} -
Load an image to compare coefficient sparsity.
- {:else if loading} -
Transforming the image with DCT, Haar and CDF 9/7…
- {:else if !stats} -
Click Generate to compare how sparse each transform makes the image.
- {:else} - {@const series = TRANSFORMS.map(t => ({ info: t, data: stats![t.value] }))} - {@const svgW = 700} - {@const svgH = 240} - {@const padL = 52} - {@const padR = 16} - {@const padT = 30} - {@const padB = 48} - {@const chartW = svgW - padL - padR} - {@const chartH = svgH - padT - padB} - {@const maxY = Math.max(...series.flatMap(s => s.data.histogram)) * 1.1 || 1} - {@const groupW = chartW / COEFF_BINS} - {@const barW = (groupW - 6) / series.length} -
- - {#each [0.25, 0.5, 0.75, 1.0] as frac} - {@const gy = padT + chartH * (1 - frac)} - - {(maxY * frac * 100).toFixed(0)}% - {/each} - - - - {#each series as s, k} - {#each s.data.histogram as val, b} - {@const barH = Math.max(0, (val / maxY) * chartH)} - - {s.info.label}: {(val * 100).toFixed(1)}% of coefficients with |c| in bin {binLabel(b)} - - {/each} - {/each} +
+ {#if !appState.originalImageData} +
Load an image to compare coefficient sparsity.
+ {:else if loading} +
Transforming the image with DCT, Haar and CDF 9/7…
+ {:else if stats} + {@const series = TRANSFORMS.map(t => ({ info: t, data: stats![t.value] }))} + {@const svgW = 700} + {@const svgH = 240} + {@const padL = 52} + {@const padR = 16} + {@const padT = 30} + {@const padB = 48} + {@const chartW = svgW - padL - padR} + {@const chartH = svgH - padT - padB} + {@const maxY = Math.max(...series.flatMap(s => s.data.histogram)) * 1.1 || 1} + {@const groupW = chartW / COEFF_BINS} + {@const barW = (groupW - 6) / series.length} +
+ + {#each [0.25, 0.5, 0.75, 1.0] as frac} + {@const gy = padT + chartH * (1 - frac)} + + {(maxY * frac * 100).toFixed(0)}% + {/each} + + - {#each { length: COEFF_BINS } as _, b} - {binLabel(b)} - {/each} + {#each series as s, k} + {#each s.data.histogram as val, b} + {@const barH = Math.max(0, (val / maxY) * chartH)} + + {s.info.label}: {(val * 100).toFixed(1)}% of coefficients with |c| in bin {binLabel(b)} + + {/each} + {/each} - |AC coefficient| (doubling-width bins) - Share + {#each { length: COEFF_BINS } as _, b} + {binLabel(b)} + {/each} - {#each series as s, k} - - {s.info.label} - {/each} - -
+ |AC coefficient| (doubling-width bins) + Share -
{#each series as s, k} - {#if k > 0}
{/if} -
- {s.info.label} zeroed at Q{statsQuality} - {(s.data.zeroFraction * 100).toFixed(1)}% -
+ + {s.info.label} {/each} + +
+ +
+ {#each series as s, k} + {#if k > 0}
{/if} +
+ {s.info.label} zeroed at Q{statsQuality} + {(s.data.zeroFraction * 100).toFixed(1)}%
-

- Current transform: {transformInfo(appState.transformType).label}. - DC / LL coefficients are excluded. The zeroed share follows the quality slider. -

- {/if} - -
+ {/each} +
+

+ DC / LL coefficients are excluded. The zeroed share follows the quality slider. +

+ {/if} + diff --git a/web/src/lib/components/viewer/ImageSourcePanel.svelte b/web/src/lib/components/viewer/ImageSourcePanel.svelte index bbfc5b9..253b663 100644 --- a/web/src/lib/components/viewer/ImageSourcePanel.svelte +++ b/web/src/lib/components/viewer/ImageSourcePanel.svelte @@ -2,13 +2,12 @@ import { appState } from '../../state.svelte.js'; interface Props { - dropZoneVisible: boolean; activeTestImageIndex: number | null; onFile: (file: File) => void; onTestImage: (index: number) => void; } - let { dropZoneVisible = $bindable(), activeTestImageIndex, onFile, onTestImage }: Props = $props(); + let { activeTestImageIndex, onFile, onTestImage }: Props = $props(); const TEST_IMAGE_COUNT = 20; // Kodak dataset let testImagesOpen = $state(false); @@ -19,65 +18,31 @@ if (file) onFile(file); } - function onDrop(e: DragEvent) { - e.preventDefault(); - dropZoneVisible = false; - const file = e.dataTransfer?.files[0]; - if (file) onFile(file); - } - function openFilePicker() { fileInput?.click(); } - - function onDropZoneKeyDown(e: KeyboardEvent) { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - openFilePicker(); - } - } - + {#if !appState.wasmReady}
{appState.status}
{/if} -{#if dropZoneVisible} -
{ e.preventDefault(); (e.currentTarget as HTMLElement).classList.add('drag-over'); }} - ondragleave={(e) => (e.currentTarget as HTMLElement).classList.remove('drag-over')} - ondrop={onDrop}> - - - - - -
Drop an image here
-
or browse to upload
-
-{:else} - -{/if}
diff --git a/web/src/lib/components/viewer/MetricsPanel.svelte b/web/src/lib/components/viewer/MetricsPanel.svelte deleted file mode 100644 index 3aa3dca..0000000 --- a/web/src/lib/components/viewer/MetricsPanel.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - -
- Analysis -
- {#if appState.originalImageData} -
-
-
Channel
-
PSNR (dB)
-
SSIM
-
- {#each [ - { channel: 'Y', psnr: appState.psnr.y, ssim: appState.ssim.y }, - { channel: 'Cr', psnr: appState.psnr.cr, ssim: appState.ssim.cr }, - { channel: 'Cb', psnr: appState.psnr.cb, ssim: appState.ssim.cb } - ] as metric} - {@const psnrState = getPsnrState(metric.psnr)} - {@const ssimState = getSsimState(metric.ssim)} -
-
{metric.channel}
-
- {metric.psnr.toFixed(2)} -
-
- {metric.ssim.toFixed(4)} -
-
- {/each} -
- - {:else} -
Load an image to see quality metrics and compression analysis.
- {/if} - - {#if appState.fileSizeInfo} -
-
- File Size Comparison - -{appState.fileSizeInfo.reduction}% -
-
-
- {appState.fileSizeInfo.original} - β†’ - ~{appState.fileSizeInfo.estimated} -
-
-
-
-
- -
- {/if} -
-
diff --git a/web/src/lib/components/viewer/RdChart.svelte b/web/src/lib/components/viewer/RdChart.svelte index ddc4808..d3dfa5c 100644 --- a/web/src/lib/components/viewer/RdChart.svelte +++ b/web/src/lib/components/viewer/RdChart.svelte @@ -1,5 +1,3 @@ - - -
-
-
-
-

Rate-Distortion Curve

-

Quality tradeoff: higher bitrate usually improves PSNR.

-
- {#if rdLoading} - Sampling… - {:else if allPoints.length > 0} - {allPoints.length} points - {/if} - {#if appState.originalImageData && !rdLoading} - - {/if} -
- - {#if !appState.originalImageData} -
Load an image to generate an RD plot.
- {:else if rdError} -
{rdError}
- {:else if rdLoading && allPoints.length === 0} -
Sampling {sampleCount} quality levels Γ— {TRANSFORMS.length} transforms…
- {:else if allPoints.length === 0} -
Click Generate to compute the rate-distortion curve.
- {:else if plotted.some(s => s.points.length >= 2)} - {@const svgW = 700} - {@const svgH = 280} - {@const padL = 64} - {@const padR = 20} - {@const padT = 18} - {@const padB = 52} - {@const xMinRaw = Math.min(...allPoints.map((p) => p.bitrate))} - {@const xMaxRaw = Math.max(...allPoints.map((p) => p.bitrate))} - {@const yMinRaw = Math.min(...allPoints.map((p) => p.psnr))} - {@const yMaxRaw = Math.max(...allPoints.map((p) => p.psnr))} - {@const xPad = Math.max(0.05, (xMaxRaw - xMinRaw) * 0.08)} - {@const yPad = Math.max(0.5, (yMaxRaw - yMinRaw) * 0.12)} - {@const xMin = xMinRaw - xPad} - {@const xMax = xMaxRaw + xPad} - {@const yMin = yMinRaw - yPad} - {@const yMax = yMaxRaw + yPad} - {@const chartW = svgW - padL - padR} - {@const chartH = svgH - padT - padB} - {@const mapX = (v: number) => padL + ((v - xMin) / (xMax - xMin || 1)) * chartW} - {@const mapY = (v: number) => svgH - padB - ((v - yMin) / (yMax - yMin || 1)) * chartH} - {@const xTicks = Array.from({length: 5}, (_, i) => xMinRaw + i * (xMaxRaw - xMinRaw) / 4)} - {@const yTicks = Array.from({length: 5}, (_, i) => yMinRaw + i * (yMaxRaw - yMinRaw) / 4)} - - {@const curves = plotted.map(({ info, points }) => { - const pts = points.map(p => ({ x: mapX(p.bitrate), y: mapY(p.psnr) })); - const path = smoothSvgPath(pts); - const fill = pts.length > 0 ? path + ` L ${pts[pts.length - 1].x},${svgH - padB} L ${pts[0].x},${svgH - padB} Z` : ''; - return { info, points, path, fill }; - })} +
+ {#if !appState.originalImageData} +
Load an image to generate an RD plot.
+ {:else if rdError} +
{rdError}
+ {:else if rdLoading && allPoints.length === 0} +
Sampling {sampleCount} quality levels Γ— {TRANSFORMS.length} transforms…
+ {:else if allPoints.length === 0} +
Preparing the curve…
+ {:else if plotted.some(s => s.points.length >= 2)} + {@const svgW = 700} + {@const svgH = 280} + {@const padL = 64} + {@const padR = 20} + {@const padT = 18} + {@const padB = 52} + {@const xMinRaw = Math.min(...allPoints.map((p) => p.bitrate))} + {@const xMaxRaw = Math.max(...allPoints.map((p) => p.bitrate))} + {@const yMinRaw = Math.min(...allPoints.map((p) => p.psnr))} + {@const yMaxRaw = Math.max(...allPoints.map((p) => p.psnr))} + {@const xPad = Math.max(0.05, (xMaxRaw - xMinRaw) * 0.08)} + {@const yPad = Math.max(0.5, (yMaxRaw - yMinRaw) * 0.12)} + {@const xMin = xMinRaw - xPad} + {@const xMax = xMaxRaw + xPad} + {@const yMin = yMinRaw - yPad} + {@const yMax = yMaxRaw + yPad} + {@const chartW = svgW - padL - padR} + {@const chartH = svgH - padT - padB} + {@const mapX = (v: number) => padL + ((v - xMin) / (xMax - xMin || 1)) * chartW} + {@const mapY = (v: number) => svgH - padB - ((v - yMin) / (yMax - yMin || 1)) * chartH} + {@const xTicks = Array.from({length: 5}, (_, i) => xMinRaw + i * (xMaxRaw - xMinRaw) / 4)} + {@const yTicks = Array.from({length: 5}, (_, i) => yMinRaw + i * (yMaxRaw - yMinRaw) / 4)} + + {@const curves = plotted.map(({ info, points }) => { + const pts = points.map(p => ({ x: mapX(p.bitrate), y: mapY(p.psnr) })); + const path = smoothSvgPath(pts); + const fill = pts.length > 0 ? path + ` L ${pts[pts.length - 1].x},${svgH - padB} L ${pts[0].x},${svgH - padB} Z` : ''; + return { info, points, path, fill }; + })} - {@const targetPoints = series[appState.transformType] ?? []} - {@const candidates = targetPoints.length > 0 ? targetPoints : allPoints} - {@const currentPoint = candidates.reduce((best, p) => Math.abs(p.quality - appState.quality) < Math.abs(best.quality - appState.quality) ? p : best)} - {@const cpx = mapX(currentPoint.bitrate)} - {@const cpy = mapY(currentPoint.psnr)} -
- - - {#each curves as curve} - - - - - {/each} - - - - - {#each yTicks as tick} - - {/each} - {#each xTicks as tick} - - {/each} - - - + {@const targetPoints = series[appState.transformType] ?? []} + {@const candidates = targetPoints.length > 0 ? targetPoints : allPoints} + {@const currentPoint = candidates.reduce((best, p) => Math.abs(p.quality - appState.quality) < Math.abs(best.quality - appState.quality) ? p : best)} + {@const cpx = mapX(currentPoint.bitrate)} + {@const cpy = mapY(currentPoint.psnr)} +
+ + {#each curves as curve} - - + + + + {/each} + + + + + {#each yTicks as tick} + + {/each} + {#each xTicks as tick} + + {/each} + + + + {#each curves as curve} + + + {/each} - - + + - {#each curves as curve} - {#each curve.points as point} - {@const isCurrent = point.quality === appState.quality && appState.transformType === curve.info.value} - selectPoint(curve.info.value, point.quality)} - onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') selectPoint(curve.info.value, point.quality); }}> - {curve.info.label}: Q{point.quality} Β· {point.psnr.toFixed(2)} dB Β· {point.bitrate.toFixed(2)} bpp Β· ~{formatBytes(point.estimatedBytes)} - - {/each} - {/each} + {#each curves as curve} + {#each curve.points as point} + {@const isCurrent = point.quality === appState.quality && appState.transformType === curve.info.value} + selectPoint(curve.info.value, point.quality)} + onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') selectPoint(curve.info.value, point.quality); }}> + {curve.info.label}: Q{point.quality} Β· {point.psnr.toFixed(2)} dB Β· {point.bitrate.toFixed(2)} bpp Β· ~{formatBytes(point.estimatedBytes)} + + {/each} + {/each} - Q{currentPoint.quality} + Q{currentPoint.quality} - - {#each curves as curve, i} - - {curve.info.label} - {/each} - {#each xTicks as tick} - {tick.toFixed(2)} - {/each} - {#each yTicks as tick} - {tick.toFixed(1)} - {/each} - Bitrate (bits per pixel) - PSNR Y (dB) - -
+ + {#each curves as curve, i} + + {curve.info.label} + {/each} + {#each xTicks as tick} + {tick.toFixed(2)} + {/each} + {#each yTicks as tick} + {tick.toFixed(1)} + {/each} + Bitrate (bits per pixel) + PSNR Y (dB) + +
-
-
- Transform - {transformInfo(appState.transformType).label} -
-
-
- Quality - Q{currentPoint.quality} -
-
-
- Bitrate - {currentPoint.bitrate.toFixed(2)} bpp -
-
-
- PSNR Y - {currentPoint.psnr.toFixed(2)} dB -
-
-
- Est. size - ~{formatBytes(currentPoint.estimatedBytes)} -
-
- {:else} -
Need more samples to render an RD curve.
- {/if} -
-
+ {:else} +
Need more samples to render an RD curve.
+ {/if} + diff --git a/web/src/lib/components/viewer/ToolsPanel.svelte b/web/src/lib/components/viewer/ToolsPanel.svelte deleted file mode 100644 index d7f2df2..0000000 --- a/web/src/lib/components/viewer/ToolsPanel.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - -
- Advanced -
-
-
Tools
- {#if wavelet} - - - {:else} - - {/if} - - - {#if wavelet} -

The block inspector is specific to the 8Γ—8 DCT. Wavelets have no blocks β€” explore their subband pyramid instead.

- {:else} -

Dive deep into compression artifacts, error heatmaps, and edge distortion analysis.

- {/if} -

[ ] quality Β±5 Β· T cycle transform

-
-
-
diff --git a/web/src/styles/base.css b/web/src/styles/base.css index 24c94a0..9cdc341 100644 --- a/web/src/styles/base.css +++ b/web/src/styles/base.css @@ -17,7 +17,6 @@ body { code, .stat-value, -.file-size-values, .stats { font-family: var(--font-mono); } diff --git a/web/src/styles/components.css b/web/src/styles/components.css index 0c88551..6e2df4f 100644 --- a/web/src/styles/components.css +++ b/web/src/styles/components.css @@ -10,63 +10,16 @@ border-radius: 12px; background: var(--bg-secondary); overflow: hidden; - opacity: 0; - transform: translateY(10px); - animation: viewerSectionStaggerIn 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; - transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; -} - -.controls .viewer-section:nth-child(2) { - animation-delay: 0.08s; -} - -.controls .viewer-section:nth-child(3) { - animation-delay: 0.14s; -} - -.controls .viewer-section:nth-child(4) { - animation-delay: 0.2s; -} - -.viewer-section:hover { - transform: translateY(-1px); - box-shadow: 0 10px 24px -18px var(--shadow-color); - border-color: color-mix(in srgb, var(--primary) 25%, var(--border-subtle, var(--border))); } .viewer-section-title { - list-style: none; - cursor: pointer; + margin: 0; padding: 12px 14px; font-size: var(--text-sm); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); - display: flex; - align-items: center; - gap: 8px; - user-select: none; -} - -.viewer-section-title::-webkit-details-marker { - display: none; -} - -.viewer-section-title::after { - content: ''; - margin-left: auto; - width: 6px; - height: 6px; - border-right: 1.5px solid var(--text-muted); - border-bottom: 1.5px solid var(--text-muted); - transform: rotate(-45deg); - transition: transform 0.2s ease; - opacity: 0.7; -} - -.viewer-section[open]>.viewer-section-title::after { - transform: rotate(45deg); } .viewer-section-body { @@ -75,19 +28,6 @@ display: flex; flex-direction: column; gap: 14px; - animation: viewerSectionIn 0.2s ease; -} - -@keyframes viewerSectionIn { - from { - opacity: 0; - transform: translateY(-3px); - } - - to { - opacity: 1; - transform: translateY(0); - } } .control-group { @@ -96,64 +36,6 @@ gap: 8px; } -.viewer-section .control-group { - opacity: 0; - transform: translateY(4px); - animation: controlGroupIn 0.26s ease forwards; -} - -.viewer-section[open] .control-group:nth-child(1) { - animation-delay: 0.04s; -} - -.viewer-section[open] .control-group:nth-child(2) { - animation-delay: 0.09s; -} - -.viewer-section[open] .control-group:nth-child(3) { - animation-delay: 0.14s; -} - -.viewer-section[open] .control-group:nth-child(4) { - animation-delay: 0.19s; -} - -@keyframes viewerSectionStaggerIn { - from { - opacity: 0; - transform: translateY(10px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes controlGroupIn { - from { - opacity: 0; - transform: translateY(4px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -@media (prefers-reduced-motion: reduce) { - - .viewer-section, - .viewer-section .control-group, - .viewer-section-body { - animation: none; - transition: none; - opacity: 1; - transform: none; - } -} - label { font-weight: 600; font-size: var(--text-md); @@ -407,7 +289,6 @@ input[type="checkbox"] { color: var(--color-poor); } - .analysis-placeholder { font-size: var(--text-base); color: var(--text-muted); @@ -417,12 +298,6 @@ input[type="checkbox"] { text-align: center; } -.advanced-hint { - margin: 0; - font-size: var(--text-base); - color: var(--text-muted); -} - .control-hint { margin: 6px 0 0; font-size: var(--text-base-sm); @@ -500,61 +375,6 @@ input[type="checkbox"] { letter-spacing: 0.02em; } -/* File Size Estimation Bar */ -.file-size-bar-container { - margin-top: 6px; - padding: 14px; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-xl); - display: flex; - flex-direction: column; - gap: 10px; - transition: all 0.2s ease; -} - -.file-size-bar-container:hover { - border-color: var(--primary-subtle); - background: color-mix(in srgb, var(--primary) 3%, var(--bg-secondary)); -} - -.file-size-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.file-size-title { - font-size: var(--text-xs); - font-weight: 800; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.12em; -} - -.reduction-badge { - background: var(--color-good-bg); - color: var(--color-good); - border: 1px solid var(--color-good-border); - font-size: var(--text-sm); - font-weight: 800; - padding: 3px 10px; - border-radius: var(--radius-full); -} - -.file-size-main { - display: flex; - flex-direction: column; - padding: 2px 0; -} - -.size-comparison-flow { - display: flex; - align-items: center; - gap: 8px; - font-family: var(--font-mono); -} - .size-val { font-weight: 700; white-space: nowrap; @@ -570,39 +390,10 @@ input[type="checkbox"] { opacity: 0.7; } -.size-arrow { - color: var(--text-muted); - opacity: 0.5; - font-size: 1.1rem; - font-weight: 400; -} - .size-val.highlight { color: var(--primary); } -.file-size-track { - width: 100%; - height: 8px; - background: var(--bg-tertiary); - border-radius: var(--radius-full); - overflow: hidden; - margin: 2px 0; - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.file-size-fill { - height: 100%; - background: linear-gradient(90deg, var(--color-good), var(--primary)); - border-radius: var(--radius-full); - transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); -} - -.file-size-footer { - display: flex; - justify-content: center; -} - .compression-ratio-label { font-size: var(--text-sm); font-weight: 700; @@ -610,15 +401,6 @@ input[type="checkbox"] { opacity: 0.9; } -.tint-toggle-group { - margin-top: 6px; -} - -.tint-toggle-group label { - padding: 5px 4px; - font-size: var(--text-base-sm); -} - /* Beta Badge */ .beta-badge { font-size: 0.6rem; @@ -871,12 +653,4 @@ input[type="checkbox"] { color: var(--text-muted); text-align: center; opacity: 0.7; -} - -/* Specific button for RD/Histogram generation to ensure consistency */ -.rd-generate-btn { - width: 120px !important; - padding-left: 0 !important; - padding-right: 0 !important; - flex-shrink: 0; } \ No newline at end of file diff --git a/web/src/styles/layout.css b/web/src/styles/layout.css index ae7ad4a..009baf1 100644 --- a/web/src/styles/layout.css +++ b/web/src/styles/layout.css @@ -39,70 +39,19 @@ } } -#viewerMode .main-column > .card, -#viewerMode .container > .controls-card { - opacity: 0; - transform: translateY(14px); - animation: viewerCardIn 0.45s cubic-bezier(0.16, 1, 0.3, 1) forwards; -} - -#viewerMode .viewer-card { - animation-delay: 0.05s; -} - -#viewerMode .rd-card { - animation-delay: 0.12s; -} - -#viewerMode .hist-card { - animation-delay: 0.19s; -} - -#viewerMode .controls-card { - animation-delay: 0.15s; -} - -#viewerMode .main-column > .card:hover, -#viewerMode .container > .controls-card:hover { - transform: translateY(-2px); - box-shadow: 0 12px 30px -14px var(--shadow-color), 0 5px 12px -6px var(--shadow-color); - border-color: color-mix(in srgb, var(--primary) 22%, var(--border-subtle, var(--border))); -} - -@keyframes viewerCardIn { - from { - opacity: 0; - transform: translateY(14px) scale(0.995); - } - - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@media (prefers-reduced-motion: reduce) { - #viewerMode .main-column > .card, - #viewerMode .container > .controls-card { - opacity: 1; - transform: none; - animation: none; - } - - #viewerMode .main-column > .card:hover, - #viewerMode .container > .controls-card:hover { - transform: none; - } - - .card { - transition: none; - } -} - @media (max-width: 768px) { .container { grid-template-columns: 1fr; } + + /* Phones: image, then the controls that change it, then the charts. */ + #viewerMode .main-column { + display: contents; + } + + #viewerMode .viewer-card { order: 1; } + #viewerMode .controls-card { order: 2; } + #viewerMode .compare-card { order: 3; } } @media (max-width: 1024px) { diff --git a/web/src/styles/onboarding.css b/web/src/styles/onboarding.css deleted file mode 100644 index b86822e..0000000 --- a/web/src/styles/onboarding.css +++ /dev/null @@ -1,64 +0,0 @@ -/* ===== Onboarding / Drop Zone ===== */ -.drop-zone { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 16px; - padding: 60px 32px; - border: 2px dashed var(--border); - border-radius: 16px; - background: var(--bg-secondary); - cursor: pointer; - transition: all 0.3s ease; - text-align: center; - min-height: 280px; -} - -.drop-zone:hover, -.drop-zone.drag-over { - border-color: var(--primary); - background: var(--primary-subtle); -} - -.drop-zone.drag-over { - transform: scale(1.01); - box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1); -} - -.drop-zone-icon { - width: 64px; - height: 64px; - color: var(--text-muted); - opacity: 0.6; - transition: all 0.3s ease; -} - -.drop-zone:hover .drop-zone-icon, -.drop-zone.drag-over .drop-zone-icon { - color: var(--primary); - opacity: 1; - transform: translateY(-4px); -} - -.drop-zone-text { - font-size: 1rem; - font-weight: 600; - color: var(--text); -} - -.drop-zone-hint { - font-size: 0.8rem; - color: var(--text-muted); -} - -.drop-zone-hint .browse-link { - color: var(--primary); - font-weight: 600; - text-decoration: underline; - text-underline-offset: 2px; -} - -.drop-zone input[type="file"] { - display: none; -} \ No newline at end of file diff --git a/web/src/styles/viewer.css b/web/src/styles/viewer.css index 230c69b..b91a106 100644 --- a/web/src/styles/viewer.css +++ b/web/src/styles/viewer.css @@ -54,7 +54,6 @@ canvas { background: rgba(30, 41, 59, 0.9); } - .comparison-viewer { position: relative; width: 100%; @@ -98,43 +97,6 @@ canvas { box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.35), 0 8px 24px rgba(5, 150, 105, 0.28); } -.inspector-active-banner { - position: absolute; - left: 12px; - top: 12px; - z-index: 4; - display: inline-flex; - align-items: center; - gap: 7px; - background: rgba(15, 23, 42, 0.82); - border: 1px solid rgba(52, 211, 153, 0.45); - color: rgba(241, 245, 249, 0.98); - border-radius: 999px; - font-size: 0.7rem; - padding: 6px 10px; - max-width: calc(100% - 24px); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); -} - -.inspector-active-banner strong { - font-size: 0.72rem; - white-space: nowrap; -} - -.inspector-active-banner span:last-child { - opacity: 0.9; -} - -.banner-dot { - width: 7px; - height: 7px; - border-radius: 999px; - background: #34d399; - box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.25); - flex-shrink: 0; -} - .comparison-divider { position: absolute; top: 0; @@ -182,7 +144,6 @@ canvas { @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { - .inspector-active-banner, .comparison-handle { backdrop-filter: none; -webkit-backdrop-filter: none; @@ -213,33 +174,14 @@ canvas { height: auto; } - @media (max-width: 860px) { .viewer-canvas-header { padding: 0 4px 6px; } - - .comparison-label { - font-size: 0.65rem; - } - - .inspector-active-banner span:last-child { - display: none; - } - - .inspector-quickstart { - flex-direction: column; - align-items: flex-start; - } - - .inspector-quickstart-btn { - width: 100%; - } } @media (max-width: 1024px) { - .inspector-active-banner, .comparison-handle { backdrop-filter: none; -webkit-backdrop-filter: none; @@ -247,57 +189,10 @@ canvas { } .viewer-canvas-header { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - padding: 0 4px 12px; -} - -.canvas-header-left { - display: flex; - align-items: center; -} - -.canvas-header-center { - display: flex; - align-items: center; - justify-content: center; -} - -.canvas-header-right { display: flex; align-items: center; - justify-content: flex-end; -} - -.img-dim-info { - font-size: 0.72rem; - font-weight: 600; - color: var(--text-muted); - background: rgba(255, 255, 255, 0.05); - padding: 5px 10px; - border-radius: 999px; - display: inline-flex; - align-items: center; - gap: 3px; - border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.1)); -} - -.dim-sep { - color: var(--text-muted); - opacity: 0.4; - font-size: 0.65rem; - padding: 0 1px; -} - -.dim-unit { - font-size: 0.65rem; - opacity: 0.6; - margin-left: 1px; -} - -.dim-val { - font-variant-numeric: tabular-nums; + gap: 8px; + padding: 0 4px 12px; } /* View Mode Toggle (Segmented Control) within Viewer Card */ @@ -321,7 +216,7 @@ canvas { cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); white-space: nowrap; - min-width: 90px; + min-width: 56px; } .view-mode-toggle button:hover { @@ -335,16 +230,10 @@ canvas { box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); } -/* Phones: dimensions + zoom share the first row, the view toggle spans the second. */ +/* Phones: the view toggle gets its own row above the zoom controls. */ @media (max-width: 600px) { .viewer-canvas-header { - grid-template-columns: 1fr auto; - row-gap: 8px; - } - - .canvas-header-center { - grid-column: 1 / -1; - grid-row: 2; + flex-wrap: wrap; } .view-mode-toggle { @@ -359,6 +248,7 @@ canvas { } .viewer-zoom-controls { + margin-left: auto; display: flex; align-items: center; gap: 6px; @@ -409,20 +299,6 @@ canvas { letter-spacing: 0.02em; } -/* Old labels and mini info removed β€” moved to global header */ - - -.inspector-quickstart { - border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.2)); - border-radius: 10px; - padding: 10px 12px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - background: rgba(15, 23, 42, 0.4); -} - .rd-chart-panel { padding: 4px; display: flex; @@ -452,13 +328,6 @@ canvas { opacity: 0.85; } -.rd-chart-status { - font-size: 0.72rem; - font-weight: 700; - color: var(--primary); - white-space: nowrap; -} - .rd-chart-svg-wrap { width: 100%; border: 1px solid var(--border-subtle, var(--border)); @@ -603,39 +472,83 @@ canvas { min-width: 40%; } } +.view-mode-toggle button:disabled, +.zoom-ctrl-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Dropping an image file onto the viewer */ +.viewer-card.drag-over { + outline: 2px dashed var(--primary); + outline-offset: -6px; +} -.quickstart-copy { +.split-labels { + display: flex; + justify-content: space-between; + padding: 8px 4px 0; + font-size: 0.72rem; + font-weight: 600; + color: rgba(203, 213, 225, 0.8); +} + +.viewer-note { + margin: 8px 4px 0; + font-size: 0.72rem; + line-height: 1.5; + color: rgba(203, 213, 225, 0.8); +} + +/* Ways into the two deep-dive modes, under the image */ +.go-deeper { + display: flex; + gap: 10px; + margin-top: 12px; +} + +.go-deeper-btn { + flex: 1; display: flex; flex-direction: column; + align-items: flex-start; gap: 2px; - min-width: 0; + text-align: left; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.04); + color: rgba(248, 250, 252, 0.98); + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease; } -.quickstart-copy strong { +.go-deeper-btn strong { font-size: 0.8rem; - color: rgba(248, 250, 252, 0.98); - letter-spacing: 0.02em; } -.quickstart-copy span { +.go-deeper-btn strong::after { + content: ' β†’'; + color: var(--primary); +} + +.go-deeper-btn span { font-size: 0.72rem; - color: rgba(203, 213, 225, 0.92); + color: rgba(203, 213, 225, 0.85); } -.inspector-quickstart-btn { - flex-shrink: 0; - border: none; - background: var(--primary); - color: white; - border-radius: 8px; - padding: 8px 10px; - font-size: 0.74rem; - font-weight: 700; - cursor: pointer; - transition: background-color 0.2s ease, transform 0.15s ease; +.go-deeper-btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.08); + border-color: var(--primary); } -.inspector-quickstart-btn:hover { - background: var(--primary-hover); - transform: translateY(-1px); -} \ No newline at end of file +.go-deeper-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +@media (max-width: 600px) { + .go-deeper { + flex-direction: column; + } +} From 7a3eed87cb104d131c003d74565596ae9ec26fec Mon Sep 17 00:00:00 2001 From: Abhinav Date: Sat, 10 Oct 2026 20:06:53 +0530 Subject: [PATCH 05/11] feat(ui): explain labels on hover Every term a newcomer might not know now explains itself on hover, with a dotted underline marking text labels that do: - Viewer: quality, transform, chroma subsampling, the size estimate, per-channel metrics, sample images, chart axes and zeroed shares. - Block Inspector: each pipeline stage, the colour legend, the coordinate and luma/chroma badges, and the sidebar headings. The quantization arrows' existing tooltips now say it is the lossy step. - Artifact Analyzer: what Blink, Heat Map and Grid actually do. Co-Authored-By: Claude Opus 5.5 --- web/src/ArtifactInspectorMode.svelte | 6 +-- web/src/InspectorMode.svelte | 42 +++++++++---------- web/src/lib/components/CodecSettings.svelte | 4 +- web/src/lib/components/QualityControl.svelte | 2 +- web/src/lib/components/ResultSummary.svelte | 4 +- .../components/viewer/CoeffHistogram.svelte | 7 ++-- .../components/viewer/ImageSourcePanel.svelte | 2 +- web/src/lib/components/viewer/RdChart.svelte | 4 +- web/src/styles/utilities.css | 9 +++- 9 files changed, 44 insertions(+), 36 deletions(-) diff --git a/web/src/ArtifactInspectorMode.svelte b/web/src/ArtifactInspectorMode.svelte index 6a8c5bc..3740339 100644 --- a/web/src/ArtifactInspectorMode.svelte +++ b/web/src/ArtifactInspectorMode.svelte @@ -399,7 +399,7 @@
{/if}
- - -
@@ -338,7 +338,7 @@