Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,13 @@ Every path below exists; keep this list in step with `src/` when adding modules.
config table. Nothing else — its five other responsibilities were split out
(ADR-018), and it is imported by exactly one module, `DOMSetup.js`
- `spectrogramImage.js` - Spectrogram image load and scaling
- `imageSizing.js` - How big an image gram is drawn (issue #345), by the
optional `image-sizing` row: `screen` (the default: one image pixel per
screen pixel, shrunk to the room), `native` (one per CSS pixel, shrunk to
the room) or `legacy` (one per CSS pixel, capped at 1200). Measures the room
and refits on resize; the arithmetic is `utils/imageFit.js`. Under `screen`
and `native` every image gram gets the expand toggle, portrait snippets
included (`expandsAnyShape`)
- `svgLayout.js` - SVG layout, viewBox and zoom-transform application
- `src/rendering/` - Rendering system. These modules draw; they do not dispatch:
- `regionOverlay.js` - The region-zoom rubber band, the dashed outline of the
Expand Down Expand Up @@ -291,6 +298,7 @@ Every path below exists; keep this list in step with `src/` when adding modules.
under the mouse (the wheel), hold the middle of the view (the `+`/`−`
buttons). Pure, so "the gram under the pointer does not move" is pinned as a
property across levels rather than by one browser drag
- `imageFit.js` - The size an image-backed gram is drawn at, per sizing
- `doppler.js` - Doppler-specific calculations
- `harmonicSampling.js` - Pin sampling for dense harmonic sets
- `markerLabel.js` - Marker label normalisation and table abbreviation
Expand Down
5 changes: 5 additions & 0 deletions debug-multiple.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ <h2>GramFrame Instance 2a</h2>
<td>freq-end</td>
<td>5000</td>
</tr>
<!-- Pinned to the legacy 1200 px cap, which tests/image-scaling.spec.js covers (issue #345). -->
<tr>
<td>image-sizing</td>
<td>legacy</td>
</tr>
</table>
</div>

Expand Down
121 changes: 121 additions & 0 deletions demo/image-sizing.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GramFrame Image Sizing</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.test-container { margin: 20px 0; }
.note { background: #f3e5f5; padding: 15px; border-left: 4px solid #6a1b9a; margin: 10px 0; }
.nav { margin-bottom: 15px; font-size: 14px; }
.nav a { color: #1a73e8; }
h2 { margin-top: 40px; border-bottom: 2px solid #6a1b9a; padding-bottom: 4px; }
h2 .tag { font-size: 13px; color: #fff; background: #6a1b9a; padding: 2px 8px; border-radius: 4px; vertical-align: middle; }
h2.before .tag { background: #9e9e9e; }
.drawn { font-size: 13px; color: #555; font-family: monospace; }
code { background: #eee; padding: 1px 4px; }
</style>
<script src="./gramframe.bundle.js"></script>
</head>
<body>
<h1>Image Sizing (issue #345)</h1>

<div class="nav">
<a href="../">&larr; All demos</a>
</div>

<div class="note">
<p><strong>The problem:</strong> a 357&nbsp;&times;&nbsp;366 snippet cut from a 1677&nbsp;&times;&nbsp;380
gram is drawn larger than the image itself, axis labels and all.</p>
<p><strong>The change:</strong> an optional config row, <code>image-sizing</code>, chooses how big a gram is
drawn, and its default is now <code>screen</code>:</p>
<ul>
<li><code>legacy</code> (<em>before</em>, now opt-in) &mdash; one image pixel per CSS pixel; anything
wider than 1200 CSS pixels is cut down to 1200, whatever room the page has.</li>
<li><code>screen</code> (the default, <em>after</em>: the last two grams have no row at all) &mdash; one image pixel per <em>screen</em> pixel,
shrunk (aspect kept) only if it would not fit the width available. On a display scaled to 125% or
150% a CSS pixel is 1.25 or 1.5 screen pixels, which is what makes <code>legacy</code> draw a small
image bigger than it is.</li>
<li><code>native</code> &mdash; one image pixel per CSS pixel, with the same fit to the available width.
Identical to <code>screen</code> on an unscaled display.</li>
</ul>
<p>Under <code>screen</code> and <code>native</code> every gram gets the expand button, the snippet included; under
<code>legacy</code> only landscape grams do.</p>
<p><strong>Why <em>before</em> may look different here than on the target network:</strong> the legacy
large gram is 1200 CSS pixels wide. On a 1920-pixel screen scaled to 125% the window is about 1536 CSS
pixels, so 1200 nearly fills it; on a wider or unscaled screen it leaves a gap. Compare the scale factor
below with the target network's.</p>
<p>This display: <strong id="dpr">&hellip;</strong> screen pixels per CSS pixel, window
<strong id="win">&hellip;</strong> CSS pixels wide.
Under each gram is the size it is drawn at, in CSS pixels.</p>
</div>

<h2 class="before">Large gram, 1677 &times; 380 <span class="tag">before &mdash; legacy</span></h2>
<div class="test-container">
<table class="gram-config">
<tr><td colspan="2"><img src="../sample/issue-345/gram-large.png" alt="Large spectrogram"></td></tr>
<tr><td>time-start</td><td>0</td></tr>
<tr><td>time-end</td><td>60</td></tr>
<tr><td>freq-start</td><td>0</td></tr>
<tr><td>freq-end</td><td>500</td></tr>
<tr><td>image-sizing</td><td>legacy</td></tr>
</table>
</div>

<h2 class="before">Snippet, 357 &times; 366 <span class="tag">before &mdash; legacy</span></h2>
<div class="test-container">
<table class="gram-config">
<tr><td colspan="2"><img src="../sample/issue-345/gram-snippet.png" alt="Snippet spectrogram"></td></tr>
<tr><td>time-start</td><td>0</td></tr>
<tr><td>time-end</td><td>58</td></tr>
<tr><td>freq-start</td><td>185</td></tr>
<tr><td>freq-end</td><td>291</td></tr>
<tr><td>image-sizing</td><td>legacy</td></tr>
</table>
</div>

<h2>Large gram, 1677 &times; 380 <span class="tag">after &mdash; default</span></h2>
<div class="test-container">
<table class="gram-config">
<tr><td colspan="2"><img src="../sample/issue-345/gram-large.png" alt="Large spectrogram"></td></tr>
<tr><td>time-start</td><td>0</td></tr>
<tr><td>time-end</td><td>60</td></tr>
<tr><td>freq-start</td><td>0</td></tr>
<tr><td>freq-end</td><td>500</td></tr>
</table>
</div>

<h2>Snippet, 357 &times; 366 <span class="tag">after &mdash; default</span></h2>
<div class="test-container">
<table class="gram-config">
<tr><td colspan="2"><img src="../sample/issue-345/gram-snippet.png" alt="Snippet spectrogram"></td></tr>
<tr><td>time-start</td><td>0</td></tr>
<tr><td>time-end</td><td>58</td></tr>
<tr><td>freq-start</td><td>185</td></tr>
<tr><td>freq-end</td><td>291</td></tr>
</table>
</div>

<script>
// Page diagnostics only: report the display scaling and each gram's drawn size.
function report() {
document.getElementById('dpr').textContent = String(window.devicePixelRatio)
document.getElementById('win').textContent = String(window.innerWidth)
document.querySelectorAll('.gram-frame-container').forEach(container => {
const image = container.querySelector('.gram-frame-spectrogram-image')
if (!image || !image.getAttribute('width')) return
let line = container.nextElementSibling
if (!line || !line.classList.contains('drawn')) {
line = document.createElement('div')
line.className = 'drawn'
container.after(line)
}
line.textContent = `Drawn at ${image.getAttribute('width')} × ${image.getAttribute('height')} CSS px`
})
}
window.addEventListener('load', () => setTimeout(report, 500))
window.addEventListener('resize', () => setTimeout(report, 200))
</script>
</body>
</html>
6 changes: 4 additions & 2 deletions docs/HTML-Integration-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The configuration table uses a 2-column format: `parameter | value`.
| `time-end` | number | Yes | End time value (top of Y-axis). Must be > `time-start` |
| `freq-start` | number | Yes | Start frequency value (left of X-axis) |
| `freq-end` | number | Yes | End frequency value (right of X-axis). Must be > `freq-start` |
| `image-sizing` | `screen` / `native` / `legacy` | No (default `screen`) | How big the image is drawn. `screen`: one image pixel per screen pixel, so a display scaled to 125% or 150% does not enlarge it. `native`: one image pixel per CSS pixel. Both shrink the image, keeping its shape, only when it would not fit the page's width, and give every gram the expand button. `legacy`: one image pixel per CSS pixel, capped at 1200 wide whatever the room, with the expand button on landscape grams only |

The first row must contain an `<img>` element with the spectrogram image (using `colspan="2"`).

Expand Down Expand Up @@ -309,15 +310,16 @@ State is deep-copied before being passed to listeners, so you cannot accidentall

## Expand API

A landscape gram can be expanded to fill the space around it. The toggle is a
A gram can be expanded to fill the space around it — any gram under the
default `image-sizing`, landscape grams only under `legacy`. The toggle is a
button on the component; these two methods drive the same state from a host
page:

```javascript
// Is the first instance currently expanded?
const expanded = GramFrame.getExpandState()

// Expand (or collapse) every landscape instance on the page
// Expand (or collapse) every instance on the page that has the toggle
GramFrame.setExpandState(true)
```

Expand Down
5 changes: 5 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ <h3>Spectrograph player <span class="badge badge-player">audio</span></h3>
<h3>Player trial harness <span class="badge badge-trial">pick a file</span></h3>
<p>The package sent to stakeholders: choose one of the four samples, or any WAV on your own machine, then tune the analysis and the painting against it &mdash; FFT size, overlap, frame averaging, background normalisation and the display percentiles. Nothing is uploaded.</p>
</a>

<a class="card" href="demo/image-sizing.html">
<h3>Image sizing <span class="badge badge-trial">before / after</span></h3>
<p>A wide gram and a small snippet cut from it, drawn the old way (<code>image-sizing: legacy</code>) and the new default: one image pixel per screen pixel, shrunk only to fit the page (issue #345).</p>
</a>
</div>

<h2>Developer pages</h2>
Expand Down
Binary file added sample/issue-345/gram-large.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added sample/issue-345/gram-snippet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions sample/pub10-vernier.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pub-10 — Vernier (portrait, no expand toggle)</title>
<title>Pub-10 — Vernier (portrait)</title>
<link rel="stylesheet" href="../src/gramframe.css" />
<!-- Development/test page: expose GramFrame's __test__* API (spec 165, GF-23).
Published training pages omit this, so those methods never ship. -->
Expand Down Expand Up @@ -40,7 +40,8 @@
<h1 class="page-title">Vernier — Portrait Gram</h1>

<div class="content">
<!-- Portrait (taller than wide) gram: vernier, must NOT be expandable -->
<!-- Portrait (taller than wide) gram: vernier. Expandable under the default
`screen` sizing (issue #345); not under `legacy`. -->
<table class="gram-config">
<tr>
<td colspan="2"><img src="pub10-assets/vernier-1.png" alt="Vernier spectrogram"></td>
Expand Down
8 changes: 4 additions & 4 deletions src/api/GramFrameAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
dispatch,
flushDispatch
} from '../core/state.js'
import { setImageExpanded, isLandscape } from '../components/ExpandToggle.js'
import { setImageExpanded, canExpand } from '../components/ExpandToggle.js'
import { isBrowserSupported, showCompatibilityWarning, looksLikeMissingApiError } from '../core/browserCompatibility.js'
import { createErrorIndicator } from '../components/ErrorIndicator.js'

Expand Down Expand Up @@ -332,13 +332,13 @@ export function createGramFrameAPI(GramFrame) {
},

/**
* Programmatically expand or collapse all landscape GramFrame instances.
* No-op for portrait/square images (mirrors the toggle's landscape gate).
* Programmatically expand or collapse every GramFrame instance that has the
* expand toggle; a no-op for those that do not (mirrors the toggle's gate).
* @param {boolean} expanded - Desired expand state
*/
setExpandState(expanded) {
this._getInstances().forEach(instance => {
if (isLandscape(instance)) {
if (canExpand(instance)) {
setImageExpanded(instance, expanded)
}
})
Expand Down
36 changes: 25 additions & 11 deletions src/components/ExpandToggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
* Expand/collapse toggle control for the spectrogram image.
*
* A small floating button at the top-left of the image region that expands a
* landscape gram to fill the available space and restores it. Portrait/square
* images (verniers) receive no toggle. Expand state is in-memory only.
* landscape gram to fill the available space and restores it. Under legacy
* sizing, portrait/square images (verniers) receive no toggle; the
* default `screen` sizing gives every shape one (issue #345). Expand state is
* in-memory only.
*
* An audio-sourced gram (spec 168) always gets the toggle: its natural size is
* bins × frames — portrait, and thousands of rows tall — but it is drawn at a
Expand All @@ -17,6 +19,7 @@ import { updateSVGLayout } from './svgLayout.js'
import { renderAxes } from '../rendering/axes.js'
import { dispatch } from '../core/state.js'
import { baseRenderSize, isPlayerActive } from '../player/playerView.js'
import { expandsAnyShape } from './imageSizing.js'

// Small gap left between the expanded image and the viewport bottom (px).
const BOTTOM_GAP = 16
Expand All @@ -27,11 +30,22 @@ const BOTTOM_GAP = 16
* @param {GramFrame} instance - GramFrame instance
* @returns {boolean} True if the image is landscape
*/
export function isLandscape(instance) {
function isLandscape(instance) {
const { width, height } = baseRenderSize(instance)
return width > 0 && height > 0 && width > height
}

/**
* Whether the instance gets the expand toggle: a landscape gram always does;
* under the `native` and `screen` sizings (issue #345) every image gram does, portrait
* snippets included, and expands the way a landscape one does.
* @param {GramFrame} instance - GramFrame instance
* @returns {boolean} True if the image can be expanded
*/
export function canExpand(instance) {
return isLandscape(instance) || expandsAnyShape(instance)
}

/**
* Compute the render dimensions that fill the available space.
*
Expand Down Expand Up @@ -135,12 +149,12 @@ function updateToggleButton(button, expanded) {
}

/**
* Programmatically set the expand state (no-op for non-landscape images).
* Programmatically set the expand state (no-op where `canExpand` says no).
* @param {GramFrame} instance - GramFrame instance
* @param {boolean} expanded - Desired expand state
*/
export function setImageExpanded(instance, expanded) {
if (!isLandscape(instance)) {
if (!canExpand(instance)) {
return
}
instance.state.imageExpanded = !!expanded
Expand All @@ -157,23 +171,23 @@ export function setImageExpanded(instance, expanded) {
* @param {GramFrame} instance - GramFrame instance
*/
export function refreshExpandedLayout(instance) {
if (!instance.state.imageExpanded) {
const { imageExpanded, imageDetails } = instance.state
if (!imageExpanded) {
return
}
const { width, height } = computeAvailableRenderSize(instance)
const imageDetails = instance.state.imageDetails
imageDetails.renderWidth = width
imageDetails.renderHeight = height
}

/**
* Create and mount the expand toggle for a landscape instance. Does nothing for
* portrait/square images, so no toggle exists in the DOM for verniers.
* Create and mount the expand toggle where `canExpand` allows it. Under legacy
* sizing that is landscape grams only, so no toggle exists in the DOM for verniers.
* @param {GramFrame} instance - GramFrame instance
* @returns {HTMLButtonElement|null} The created button, or null if not landscape
* @returns {HTMLButtonElement|null} The created button, or null if it cannot expand
*/
export function createExpandToggle(instance) {
if (!isLandscape(instance)) {
if (!canExpand(instance)) {
return null
}

Expand Down
Loading
Loading