Skip to content

Commit 8edfdf5

Browse files
authored
Merge pull request #55 from happyprime/feat/blink-controls
Make blink mode's speed adjustable and add an optional crossfade
2 parents f150607 + 233c162 commit 8edfdf5

3 files changed

Lines changed: 157 additions & 19 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ and renders three views client-side from the URL hash — a triage overview
168168
view (swipe, side-by-side, onion skin, diff overlay, blink), and a unified HTML
169169
diff. It opens straight from disk with no network access.
170170

171+
### Blink mode
172+
173+
Blink mode alternates between the baseline and the current capture in place, so
174+
a shift shows up as movement. Its dwell time is adjustable — a speed slider, or
175+
`[` / `]` to slow down / speed up — and an optional crossfade (`f`, or the Fade
176+
button) eases between the two images instead of hard-cutting, which can make a
177+
small change easier to catch than an abrupt swap. Both preferences persist
178+
across pages and reloads.
179+
171180
### Oversized captures
172181

173182
Browsers refuse to decode an image taller or wider than 32,767px and show it as

templates/assets/app.js

Lines changed: 141 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,12 @@
11361136
flip: function () {
11371137
blink.flip();
11381138
},
1139+
nudgeDelay: function (d) {
1140+
blink.nudgeDelay(d);
1141+
},
1142+
toggleFade: function () {
1143+
blink.toggleFade();
1144+
},
11391145
});
11401146
}
11411147

@@ -1155,16 +1161,24 @@
11551161
);
11561162
}
11571163
root.appendChild(stage);
1158-
root.appendChild(
1159-
hintbar([
1160-
{ keys: ['1', '5'], label: 'modes' },
1161-
{ keys: ['←', '→'], label: 'adjust' },
1162-
{ keys: ['n', 'p'], label: 'next / prev changed' },
1163-
{ keys: ['v'], label: 'viewport' },
1164-
{ keys: ['h'], label: 'HTML' },
1165-
{ keys: ['esc'], label: 'overview' },
1166-
])
1164+
var hints = [
1165+
{ keys: ['1', '5'], label: 'modes' },
1166+
{ keys: ['←', '→'], label: 'adjust' },
1167+
];
1168+
if (mode === 'blink') {
1169+
hints.push(
1170+
{ keys: ['space'], label: 'play / pause' },
1171+
{ keys: ['[', ']'], label: 'speed' },
1172+
{ keys: ['f'], label: 'fade' }
1173+
);
1174+
}
1175+
hints.push(
1176+
{ keys: ['n', 'p'], label: 'next / prev changed' },
1177+
{ keys: ['v'], label: 'viewport' },
1178+
{ keys: ['h'], label: 'HTML' },
1179+
{ keys: ['esc'], label: 'overview' }
11671180
);
1181+
root.appendChild(hintbar(hints));
11681182
}
11691183

11701184
/**
@@ -1372,31 +1386,80 @@
13721386
};
13731387
}
13741388

1389+
// Blink dwell time (ms each image is held) and crossfade preference persist
1390+
// across slugs and reloads so a chosen rhythm sticks while triaging.
1391+
var BLINK_MIN = 100;
1392+
var BLINK_MAX = 3000;
1393+
var BLINK_STEP = 50;
1394+
13751395
/**
1376-
* Build the blink stage and its play/pause control.
1396+
* Read the saved blink dwell, clamped to the supported range.
1397+
*
1398+
* @returns {number} The dwell in milliseconds.
1399+
*/
1400+
function blinkDelay() {
1401+
var saved = parseInt(sessionStorage.getItem('rg-blink-delay'), 10);
1402+
var ms = Number.isFinite(saved) ? saved : 650;
1403+
return Math.max(BLINK_MIN, Math.min(BLINK_MAX, ms));
1404+
}
1405+
1406+
/**
1407+
* Build the blink stage and its play/pause, speed, and fade controls.
1408+
*
1409+
* Stacks the baseline over the current image and alternates which is
1410+
* opaque. The dwell time is adjustable (slider or `[` / `]`); an optional
1411+
* crossfade (`f`) eases between the two rather than hard-cutting, which can
1412+
* make a small shift easier to spot than an abrupt swap.
13771413
*
13781414
* @param {HTMLElement} stage The stage container to fill.
13791415
* @param {HTMLElement} bar2 The mode-control bar.
13801416
* @param {object} img The result's image paths.
1381-
* @returns {object} `{ setPlaying, toggle, flip }` controls.
1417+
* @returns {object} `{ setPlaying, toggle, flip, nudgeDelay, toggleFade }`.
13821418
*/
13831419
function buildBlink(stage, bar2, img) {
13841420
var showBase = false;
13851421
var playing = true;
1386-
var layer = h(
1387-
'div',
1388-
{ class: 'layer', style: { visibility: 'hidden' } },
1389-
[h('img', { src: img.control, alt: 'Baseline screenshot' })]
1390-
);
1422+
var delay = blinkDelay();
1423+
var fade = sessionStorage.getItem('rg-blink-fade') === '1';
1424+
1425+
var layer = h('div', { class: 'layer blink-layer' }, [
1426+
h('img', { src: img.control, alt: 'Baseline screenshot' }),
1427+
]);
13911428
var label = h('span', { class: 'taglabel l', text: 'current' });
13921429
var readout = h('b', {
13931430
style: { color: 'var(--text)' },
13941431
text: 'current',
13951432
});
13961433
var playBtn = h('button', { class: 'ghost-btn', text: 'Pause' });
1434+
var slider = h('input', {
1435+
type: 'range',
1436+
min: String(BLINK_MIN),
1437+
max: String(BLINK_MAX),
1438+
step: String(BLINK_STEP),
1439+
value: String(delay),
1440+
'aria-label': 'Blink dwell time in milliseconds',
1441+
});
1442+
var delayOut = h('span', {
1443+
class: 'blink-delay',
1444+
text: delay + 'ms',
1445+
});
1446+
var fadeBtn = h('button', {
1447+
class: 'ghost-btn',
1448+
text: 'Fade',
1449+
'aria-pressed': fade ? 'true' : 'false',
1450+
title: 'Crossfade between images (f)',
1451+
});
1452+
1453+
// Ease no longer than half the dwell so each image still reaches its
1454+
// own full opacity before the next swap starts.
1455+
var applyFade = function () {
1456+
layer.style.transition = fade
1457+
? 'opacity ' + Math.min(delay / 2, 400) + 'ms linear'
1458+
: 'none';
1459+
};
13971460
var setShowBase = function (b) {
13981461
showBase = b;
1399-
layer.style.visibility = b ? 'visible' : 'hidden';
1462+
layer.style.opacity = b ? '1' : '0';
14001463
label.textContent = b ? 'baseline' : 'current';
14011464
readout.textContent = b ? 'baseline' : 'current';
14021465
};
@@ -1410,14 +1473,51 @@
14101473
if (p) {
14111474
blinkTimer = setInterval(function () {
14121475
setShowBase(!showBase);
1413-
}, 650);
1476+
}, delay);
14141477
}
14151478
};
1479+
var setDelay = function (ms) {
1480+
delay = Math.max(BLINK_MIN, Math.min(BLINK_MAX, ms));
1481+
sessionStorage.setItem('rg-blink-delay', String(delay));
1482+
delayOut.textContent = delay + 'ms';
1483+
slider.value = String(delay);
1484+
applyFade();
1485+
// Restart the interval so a change takes effect immediately.
1486+
if (playing) {
1487+
setPlaying(true);
1488+
}
1489+
};
1490+
var setFade = function (on) {
1491+
fade = on;
1492+
sessionStorage.setItem('rg-blink-fade', on ? '1' : '0');
1493+
fadeBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
1494+
applyFade();
1495+
};
1496+
14161497
playBtn.addEventListener('click', function () {
14171498
setPlaying(!playing);
14181499
});
1500+
slider.addEventListener('input', function () {
1501+
setDelay(parseInt(slider.value, 10));
1502+
});
1503+
fadeBtn.addEventListener('click', function () {
1504+
setFade(!fade);
1505+
});
1506+
1507+
applyFade();
1508+
setShowBase(false);
1509+
14191510
bar2.appendChild(
1420-
h('span', { class: 'modectl' }, [playBtn, 'showing: ', readout])
1511+
h('span', { class: 'modectl' }, [
1512+
playBtn,
1513+
'showing: ',
1514+
readout,
1515+
h('span', { class: 'modectl-div' }),
1516+
'speed',
1517+
slider,
1518+
delayOut,
1519+
fadeBtn,
1520+
])
14211521
);
14221522
stage.appendChild(
14231523
h('div', { class: 'shot' }, [
@@ -1435,6 +1535,12 @@
14351535
setPlaying(false);
14361536
setShowBase(!showBase);
14371537
},
1538+
nudgeDelay: function (d) {
1539+
setDelay(delay + d * BLINK_STEP);
1540+
},
1541+
toggleFade: function () {
1542+
setFade(!fade);
1543+
},
14381544
};
14391545
}
14401546

@@ -1483,6 +1589,19 @@
14831589
if (ctx.togglePlay) {
14841590
ctx.togglePlay();
14851591
}
1592+
} else if (
1593+
(e.key === '[' || e.key === ']') &&
1594+
ctx.mode === 'blink'
1595+
) {
1596+
e.preventDefault();
1597+
if (ctx.nudgeDelay) {
1598+
ctx.nudgeDelay(e.key === ']' ? 1 : -1);
1599+
}
1600+
} else if (e.key === 'f' && ctx.mode === 'blink') {
1601+
e.preventDefault();
1602+
if (ctx.toggleFade) {
1603+
ctx.toggleFade();
1604+
}
14861605
}
14871606
};
14881607
}
@@ -1753,6 +1872,9 @@
17531872
cmp: [
17541873
['Swipe / Side / Onion / Diff / Blink', ['1', '5']],
17551874
['Adjust slider / flip blink', ['←', '→']],
1875+
['Play / pause blink', ['space']],
1876+
['Blink speed (slower / faster)', ['[', ']']],
1877+
['Toggle blink crossfade', ['f']],
17561878
['Next / prev changed result', ['n', 'p']],
17571879
['Cycle viewport', ['v']],
17581880
['HTML diff', ['h']],

templates/assets/reglance.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,13 @@ kbd {
349349

350350
.modectl { display: flex; align-items: center; gap: 10px; color: var(--text3); font-size: 12.5px; white-space: nowrap; }
351351
.modectl input[type="range"] { accent-color: var(--accent); width: 150px; }
352+
.modectl-div { width: 1px; align-self: stretch; background: var(--line); margin: 2px 2px; }
353+
.modectl .blink-delay { font-family: var(--font-mono); color: var(--text2); min-width: 52px; }
354+
.modectl button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); color: var(--text); }
355+
356+
/* Blink crossfade: the easing is set inline (tied to the dwell time); this
357+
just establishes opacity as the animated property. */
358+
.blink-layer { opacity: 0; }
352359

353360
.stage { max-width: 1100px; margin: 6px auto 80px; padding: 0 22px; }
354361
.stage.wide { max-width: 1500px; }

0 commit comments

Comments
 (0)