Draw the Flanger's comb as a thin line with a fading trail (#398)
game-maker / gate (push) Successful in 4m8s
game-maker / suite (push) Failing after 16m2s

The comb was still drawn too heavy. Each frame showed the whole band
the comb had swept over the last 1/20 s, edged by two lines 1.5 wide,
and the faster the delay moved, the thicker that band got.

- flangerLine draws the comb where the light is as one line, 1 wide.
  It runs through the ends of each column and the peaks and notches
  in it, where they are. A comb finer than a column zigzags over it.
- Behind the line a faint trail shows what the comb swept over the
  last 1/20 s, and over the last 1/40 s once more, so it fades with
  age. A Flanger that stands still has none.
- The line fades out where its teeth move more than a quarter of a
  tooth within the trail (clearAt), and is gone from half a tooth on.
  There only the calm band of the trail is left, so nothing flickers.
- responseFrame gives the screen the line, how clear each point is,
  the sharp columns for the notches and the trail. It replaces
  responseColumnsOf, and RESPONSE_BLUR is now RESPONSE_TRAIL.
- The notches are counted on the sharp comb again, not on the band.
- The Phaser's response is drawn as the same thin line, without a
  trail.

Tests:
  Unit modulation-view (stein2): 26 tests, 2 new. The line holds the
    comb's dB at each point and every notch. On a fast swing it fades
    out over 8 kHz, stays clear under 1 kHz, and the trail is the same
    band over 16 kHz in three frames. A still Flanger has no trail.
  Counter-check: with clearAt always 1, the fade test fails.
  e2e 34 (atx): ALL PASSED, 142 checks.
  Gate (atx): CHECK EXIT 0, 291 files, 4471 tests.
  Pictures of the default Flanger, a fast one (2 Hz, 5 ms) and one
    sweeping to 20 ms, and of the Phaser: a thin line, the trail
    behind it, a calm band high up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 21:30:03 +02:00
1 parent f9a953f3d3
commit b145e1f288
3 files changed
+152 -44

No files matched your search

+31 -18
View File
@@ -24,7 +24,7 @@ import {
notchesOf,
RESPONSE_FLOOR,
RESPONSE_TOP,
responseColumnsOf,
responseFrame,
shareOfHz,
type SwingPicture,
swingValueAt,
@@ -481,26 +481,39 @@ class ResponseScope extends ModScope {
const values = this.values;
if (!picture || !values || this.w <= 0) return;
const r = this.response;
const columns = responseColumnsOf(this.device, values, this.at, Math.max(1, Math.round(r.w)));
this.notches = notchesOf(columns);
const frame = responseFrame(this.device, values, this.at, Math.max(1, Math.round(r.w)));
this.notches = notchesOf(frame.columns);
const bottom = r.y + r.h;
const step = r.w / columns.length;
const xs = columns.map((_, i) => r.x + (i + 0.5) * step);
const highs = columns.map((column) => this.yOfDb(column.high));
const lows = columns.map((column) => this.yOfDb(column.low));
// Under the response a faint fill. The response is a band from the highest to the lowest of
// each column, edged by two smooth lines: one line where it is smooth, filled where a comb runs
// too fine for the columns.
c.poly([r.x, bottom, ...xs.flatMap((x, i) => [x, highs[i]!]), r.x + r.w, bottom]).fill({
const step = r.w / frame.columns.length;
const xs = frame.columns.map((_, i) => r.x + (i + 0.5) * step);
// Under the response a faint fill, up to the longest trail where there is one, so it stays calm.
const under = frame.trail[0] ?? frame.columns;
c.poly([r.x, bottom, ...xs.flatMap((x, i) => [x, this.yOfDb(under[i]!.high)]), r.x + r.w, bottom]).fill({
color: this.look.trace,
alpha: 0.12,
alpha: 0.1,
});
const band = xs.flatMap((x, i) => [x, highs[i]!]);
for (let i = xs.length - 1; i >= 0; i--) band.push(xs[i]!, lows[i]!);
c.poly(band).fill(this.look.trace);
for (const edge of [highs, lows]) {
xs.forEach((x, i) => (i === 0 ? c.moveTo(x, edge[i]!) : c.lineTo(x, edge[i]!)));
c.stroke({ color: this.look.trace, width: 1.5, join: 'round' });
// Behind the line the trail of what it swept just before, fading with age: each band reaches
// from longer ago to now, so they stack up towards the line.
for (const columns of frame.trail) {
const band = xs.flatMap((x, i) => [x, this.yOfDb(columns[i]!.high)]);
for (let i = xs.length - 1; i >= 0; i--) band.push(xs[i]!, this.yOfDb(columns[i]!.low));
c.poly(band).fill({ color: this.look.trace, alpha: 0.16 });
}
// The line itself, thin, fading out where its teeth pass faster than an eye follows: in runs
// of the same few steps of alpha, each starting where the last ended.
const { line, clear } = frame;
const steps = 8;
const at = (i: number) => [r.x + line[i]!.share * r.w, this.yOfDb(line[i]!.db)] as const;
for (let i = 0; i < line.length - 1;) {
const alpha = Math.round(clear[i]! * steps) / steps;
let j = i + 1;
while (j < line.length - 1 && Math.round(clear[j]! * steps) / steps === alpha) j++;
if (alpha > 0) {
c.moveTo(...at(i));
for (let k = i + 1; k <= j; k++) c.lineTo(...at(k));
c.stroke({ color: this.look.trace, width: 1, alpha, join: 'round' });
}
i = j;
}
const hz = this.markHz(this.reading);
if (hz !== null && shareOfHz(hz) >= 0 && shareOfHz(hz) <= 1)