|
1095 | 1095 | flip: function () { |
1096 | 1096 | blink.flip(); |
1097 | 1097 | }, |
| 1098 | + nudgeDelay: function (d) { |
| 1099 | + blink.nudgeDelay(d); |
| 1100 | + }, |
| 1101 | + toggleFade: function () { |
| 1102 | + blink.toggleFade(); |
| 1103 | + }, |
1098 | 1104 | }); |
1099 | 1105 | } |
1100 | 1106 |
|
|
1111 | 1117 | ); |
1112 | 1118 | } |
1113 | 1119 | root.appendChild(stage); |
1114 | | - root.appendChild( |
1115 | | - hintbar([ |
1116 | | - { keys: ['1', '5'], label: 'modes' }, |
1117 | | - { keys: ['←', '→'], label: 'adjust' }, |
1118 | | - { keys: ['n', 'p'], label: 'next / prev changed' }, |
1119 | | - { keys: ['v'], label: 'viewport' }, |
1120 | | - { keys: ['h'], label: 'HTML' }, |
1121 | | - { keys: ['esc'], label: 'overview' }, |
1122 | | - ]) |
| 1120 | + var hints = [ |
| 1121 | + { keys: ['1', '5'], label: 'modes' }, |
| 1122 | + { keys: ['←', '→'], label: 'adjust' }, |
| 1123 | + ]; |
| 1124 | + if (mode === 'blink') { |
| 1125 | + hints.push( |
| 1126 | + { keys: ['space'], label: 'play / pause' }, |
| 1127 | + { keys: ['[', ']'], label: 'speed' }, |
| 1128 | + { keys: ['f'], label: 'fade' } |
| 1129 | + ); |
| 1130 | + } |
| 1131 | + hints.push( |
| 1132 | + { keys: ['n', 'p'], label: 'next / prev changed' }, |
| 1133 | + { keys: ['v'], label: 'viewport' }, |
| 1134 | + { keys: ['h'], label: 'HTML' }, |
| 1135 | + { keys: ['esc'], label: 'overview' } |
1123 | 1136 | ); |
| 1137 | + root.appendChild(hintbar(hints)); |
1124 | 1138 | } |
1125 | 1139 |
|
1126 | 1140 | /** |
|
1328 | 1342 | }; |
1329 | 1343 | } |
1330 | 1344 |
|
| 1345 | + // Blink dwell time (ms each image is held) and crossfade preference persist |
| 1346 | + // across slugs and reloads so a chosen rhythm sticks while triaging. |
| 1347 | + var BLINK_MIN = 100; |
| 1348 | + var BLINK_MAX = 3000; |
| 1349 | + var BLINK_STEP = 50; |
| 1350 | + |
1331 | 1351 | /** |
1332 | | - * Build the blink stage and its play/pause control. |
| 1352 | + * Read the saved blink dwell, clamped to the supported range. |
| 1353 | + * |
| 1354 | + * @returns {number} The dwell in milliseconds. |
| 1355 | + */ |
| 1356 | + function blinkDelay() { |
| 1357 | + var saved = parseInt(sessionStorage.getItem('rg-blink-delay'), 10); |
| 1358 | + var ms = Number.isFinite(saved) ? saved : 650; |
| 1359 | + return Math.max(BLINK_MIN, Math.min(BLINK_MAX, ms)); |
| 1360 | + } |
| 1361 | + |
| 1362 | + /** |
| 1363 | + * Build the blink stage and its play/pause, speed, and fade controls. |
| 1364 | + * |
| 1365 | + * Stacks the baseline over the current image and alternates which is |
| 1366 | + * opaque. The dwell time is adjustable (slider or `[` / `]`); an optional |
| 1367 | + * crossfade (`f`) eases between the two rather than hard-cutting, which can |
| 1368 | + * make a small shift easier to spot than an abrupt swap. |
1333 | 1369 | * |
1334 | 1370 | * @param {HTMLElement} stage The stage container to fill. |
1335 | 1371 | * @param {HTMLElement} bar2 The mode-control bar. |
1336 | 1372 | * @param {object} img The result's image paths. |
1337 | | - * @returns {object} `{ setPlaying, toggle, flip }` controls. |
| 1373 | + * @returns {object} `{ setPlaying, toggle, flip, nudgeDelay, toggleFade }`. |
1338 | 1374 | */ |
1339 | 1375 | function buildBlink(stage, bar2, img) { |
1340 | 1376 | var showBase = false; |
1341 | 1377 | var playing = true; |
1342 | | - var layer = h( |
1343 | | - 'div', |
1344 | | - { class: 'layer', style: { visibility: 'hidden' } }, |
1345 | | - [h('img', { src: img.control, alt: 'Baseline screenshot' })] |
1346 | | - ); |
| 1378 | + var delay = blinkDelay(); |
| 1379 | + var fade = sessionStorage.getItem('rg-blink-fade') === '1'; |
| 1380 | + |
| 1381 | + var layer = h('div', { class: 'layer blink-layer' }, [ |
| 1382 | + h('img', { src: img.control, alt: 'Baseline screenshot' }), |
| 1383 | + ]); |
1347 | 1384 | var label = h('span', { class: 'taglabel l', text: 'current' }); |
1348 | 1385 | var readout = h('b', { |
1349 | 1386 | style: { color: 'var(--text)' }, |
1350 | 1387 | text: 'current', |
1351 | 1388 | }); |
1352 | 1389 | var playBtn = h('button', { class: 'ghost-btn', text: 'Pause' }); |
| 1390 | + var slider = h('input', { |
| 1391 | + type: 'range', |
| 1392 | + min: String(BLINK_MIN), |
| 1393 | + max: String(BLINK_MAX), |
| 1394 | + step: String(BLINK_STEP), |
| 1395 | + value: String(delay), |
| 1396 | + 'aria-label': 'Blink dwell time in milliseconds', |
| 1397 | + }); |
| 1398 | + var delayOut = h('span', { |
| 1399 | + class: 'blink-delay', |
| 1400 | + text: delay + 'ms', |
| 1401 | + }); |
| 1402 | + var fadeBtn = h('button', { |
| 1403 | + class: 'ghost-btn', |
| 1404 | + text: 'Fade', |
| 1405 | + 'aria-pressed': fade ? 'true' : 'false', |
| 1406 | + title: 'Crossfade between images (f)', |
| 1407 | + }); |
| 1408 | + |
| 1409 | + // Ease no longer than half the dwell so each image still reaches its |
| 1410 | + // own full opacity before the next swap starts. |
| 1411 | + var applyFade = function () { |
| 1412 | + layer.style.transition = fade |
| 1413 | + ? 'opacity ' + Math.min(delay / 2, 400) + 'ms linear' |
| 1414 | + : 'none'; |
| 1415 | + }; |
1353 | 1416 | var setShowBase = function (b) { |
1354 | 1417 | showBase = b; |
1355 | | - layer.style.visibility = b ? 'visible' : 'hidden'; |
| 1418 | + layer.style.opacity = b ? '1' : '0'; |
1356 | 1419 | label.textContent = b ? 'baseline' : 'current'; |
1357 | 1420 | readout.textContent = b ? 'baseline' : 'current'; |
1358 | 1421 | }; |
|
1366 | 1429 | if (p) { |
1367 | 1430 | blinkTimer = setInterval(function () { |
1368 | 1431 | setShowBase(!showBase); |
1369 | | - }, 650); |
| 1432 | + }, delay); |
1370 | 1433 | } |
1371 | 1434 | }; |
| 1435 | + var setDelay = function (ms) { |
| 1436 | + delay = Math.max(BLINK_MIN, Math.min(BLINK_MAX, ms)); |
| 1437 | + sessionStorage.setItem('rg-blink-delay', String(delay)); |
| 1438 | + delayOut.textContent = delay + 'ms'; |
| 1439 | + slider.value = String(delay); |
| 1440 | + applyFade(); |
| 1441 | + // Restart the interval so a change takes effect immediately. |
| 1442 | + if (playing) { |
| 1443 | + setPlaying(true); |
| 1444 | + } |
| 1445 | + }; |
| 1446 | + var setFade = function (on) { |
| 1447 | + fade = on; |
| 1448 | + sessionStorage.setItem('rg-blink-fade', on ? '1' : '0'); |
| 1449 | + fadeBtn.setAttribute('aria-pressed', on ? 'true' : 'false'); |
| 1450 | + applyFade(); |
| 1451 | + }; |
| 1452 | + |
1372 | 1453 | playBtn.addEventListener('click', function () { |
1373 | 1454 | setPlaying(!playing); |
1374 | 1455 | }); |
| 1456 | + slider.addEventListener('input', function () { |
| 1457 | + setDelay(parseInt(slider.value, 10)); |
| 1458 | + }); |
| 1459 | + fadeBtn.addEventListener('click', function () { |
| 1460 | + setFade(!fade); |
| 1461 | + }); |
| 1462 | + |
| 1463 | + applyFade(); |
| 1464 | + setShowBase(false); |
| 1465 | + |
1375 | 1466 | bar2.appendChild( |
1376 | | - h('span', { class: 'modectl' }, [playBtn, 'showing: ', readout]) |
| 1467 | + h('span', { class: 'modectl' }, [ |
| 1468 | + playBtn, |
| 1469 | + 'showing: ', |
| 1470 | + readout, |
| 1471 | + h('span', { class: 'modectl-div' }), |
| 1472 | + 'speed', |
| 1473 | + slider, |
| 1474 | + delayOut, |
| 1475 | + fadeBtn, |
| 1476 | + ]) |
1377 | 1477 | ); |
1378 | 1478 | stage.appendChild( |
1379 | 1479 | h('div', { class: 'shot' }, [ |
|
1391 | 1491 | setPlaying(false); |
1392 | 1492 | setShowBase(!showBase); |
1393 | 1493 | }, |
| 1494 | + nudgeDelay: function (d) { |
| 1495 | + setDelay(delay + d * BLINK_STEP); |
| 1496 | + }, |
| 1497 | + toggleFade: function () { |
| 1498 | + setFade(!fade); |
| 1499 | + }, |
1394 | 1500 | }; |
1395 | 1501 | } |
1396 | 1502 |
|
|
1439 | 1545 | if (ctx.togglePlay) { |
1440 | 1546 | ctx.togglePlay(); |
1441 | 1547 | } |
| 1548 | + } else if ( |
| 1549 | + (e.key === '[' || e.key === ']') && |
| 1550 | + ctx.mode === 'blink' |
| 1551 | + ) { |
| 1552 | + e.preventDefault(); |
| 1553 | + if (ctx.nudgeDelay) { |
| 1554 | + ctx.nudgeDelay(e.key === ']' ? 1 : -1); |
| 1555 | + } |
| 1556 | + } else if (e.key === 'f' && ctx.mode === 'blink') { |
| 1557 | + e.preventDefault(); |
| 1558 | + if (ctx.toggleFade) { |
| 1559 | + ctx.toggleFade(); |
| 1560 | + } |
1442 | 1561 | } |
1443 | 1562 | }; |
1444 | 1563 | } |
|
1709 | 1828 | cmp: [ |
1710 | 1829 | ['Swipe / Side / Onion / Diff / Blink', ['1', '5']], |
1711 | 1830 | ['Adjust slider / flip blink', ['←', '→']], |
| 1831 | + ['Play / pause blink', ['space']], |
| 1832 | + ['Blink speed (slower / faster)', ['[', ']']], |
| 1833 | + ['Toggle blink crossfade', ['f']], |
1712 | 1834 | ['Next / prev changed result', ['n', 'p']], |
1713 | 1835 | ['Cycle viewport', ['v']], |
1714 | 1836 | ['HTML diff', ['h']], |
|
0 commit comments