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
6 changes: 6 additions & 0 deletions screen.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@
<option>1/2</option>
<option selected>1/4</option>
<option>1/8</option>
<option>1/12</option>
<option>1/16</option>
<option>1/24</option>
<option>1/32</option>
<option>1/48</option>
<option>1/64</option>
<option>1/96</option>
<option>Off</option>
</select>

Expand Down
36 changes: 31 additions & 5 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,32 @@ let BEAT_H = 24;
const LABEL_W = 52;
const MIN_NOTE_W = 18;
const NOTE_PAD = 3;
const SNAP_VALUES = [1, 0.5, 0.25, 0.125, 0.0625, 0]; // 1/1 … 1/16, off
/* @pure:snap-options:start */
const SNAP_OPTIONS = Object.freeze([
{ label: '1/1', value: 1, subdivisions: 1 },
{ label: '1/2', value: 1 / 2, subdivisions: 2 },
{ label: '1/4', value: 1 / 4, subdivisions: 4 },
{ label: '1/8', value: 1 / 8, subdivisions: 8 },
{ label: '1/12', value: 1 / 12, subdivisions: 12 },
{ label: '1/16', value: 1 / 16, subdivisions: 16 },
{ label: '1/24', value: 1 / 24, subdivisions: 24 },
{ label: '1/32', value: 1 / 32, subdivisions: 32 },
{ label: '1/48', value: 1 / 48, subdivisions: 48 },
{ label: '1/64', value: 1 / 64, subdivisions: 64 },
{ label: '1/96', value: 1 / 96, subdivisions: 96 },
{ label: 'Off', value: 0, subdivisions: 0 },
]);
const SNAP_VALUES = SNAP_OPTIONS.map(opt => opt.value);

function _editorSnapOptionLabelsPure() {
return SNAP_OPTIONS.map(opt => opt.label);
}

function _editorSnapSubdivisionsPure(snapValue) {
if (!snapValue) return 0;
return Math.max(1, Math.round(1 / snapValue));
}
/* @pure:snap-options:end */
const DPR = window.devicePixelRatio || 1;

// ── Piano roll constants ────────────────────────────────────────────
Expand Down Expand Up @@ -557,7 +582,7 @@ function updatePianoRange(expandOnly = false) {

function snapTime(t) {
const sv = SNAP_VALUES[S.snapIdx];
if (sv === 0 || S.beats.length < 2) return t;
if (!sv || S.beats.length < 2) return t;
// Find surrounding beat
let bi = 0;
for (let i = 0; i < S.beats.length - 1; i++) {
Expand All @@ -566,7 +591,7 @@ function snapTime(t) {
const bt = S.beats[bi].time;
const nt = bi < S.beats.length - 1 ? S.beats[bi + 1].time : bt + 0.5;
const bd = nt - bt;
const subs = 1 / sv;
const subs = _editorSnapSubdivisionsPure(sv);
const sd = bd / subs;
const idx = Math.round((t - bt) / sd);
return bt + idx * sd;
Expand Down Expand Up @@ -3329,8 +3354,9 @@ function _editorSnapStepSeconds() {
const bt = S.beats[bi].time;
const nt = bi < S.beats.length - 1 ? S.beats[bi + 1].time : bt + 0.5;
const sv = SNAP_VALUES[S.snapIdx];
if (!sv) return Math.max(0.001, nt - bt);
return Math.max(0.001, (nt - bt) / (1 / sv));
const subs = _editorSnapSubdivisionsPure(sv);
if (!subs) return Math.max(0.001, nt - bt);
return Math.max(0.001, (nt - bt) / subs);
}

function _editorSeekToTime(t) {
Expand Down
49 changes: 49 additions & 0 deletions tests/snap_options.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';
/*
* Snap option model tests for screen.js.
*
* Run: node tests/snap_options.test.js
*/
const fs = require('fs');
const path = require('path');
const assert = require('assert');

const src = fs.readFileSync(path.join(__dirname, '..', 'screen.js'), 'utf8');
const m = src.match(/\/\* @pure:snap-options:start \*\/[\s\S]*?\/\* @pure:snap-options:end \*\//);
if (!m) {
console.error('FAIL: @pure:snap-options block not found in screen.js');
process.exit(1);
}

const api = new Function(
'"use strict";' + m[0] + '\nreturn { SNAP_OPTIONS, SNAP_VALUES, _editorSnapOptionLabelsPure, _editorSnapSubdivisionsPure };'
)();

let pass = 0;
let fail = 0;
function t(name, fn) {
try { fn(); pass++; console.log(' ok ' + name); }
catch (e) { fail++; console.error(' FAIL ' + name + ': ' + e.message); }
}

t('keeps 1/4 as the default snap index', () => {
assert.strictEqual(api.SNAP_OPTIONS[2].label, '1/4');
assert.strictEqual(api.SNAP_VALUES[2], 0.25);
});

t('includes dense and triplet-friendly snap divisions before Off', () => {
assert.deepStrictEqual(api._editorSnapOptionLabelsPure(), [
'1/1', '1/2', '1/4', '1/8', '1/12', '1/16',
'1/24', '1/32', '1/48', '1/64', '1/96', 'Off',
]);
});

t('maps snap values to beat subdivisions', () => {
assert.strictEqual(api._editorSnapSubdivisionsPure(1 / 24), 24);
assert.strictEqual(api._editorSnapSubdivisionsPure(1 / 32), 32);
assert.strictEqual(api._editorSnapSubdivisionsPure(1 / 96), 96);
assert.strictEqual(api._editorSnapSubdivisionsPure(0), 0);
});

console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);