|
1136 | 1136 | flip: function () { |
1137 | 1137 | blink.flip(); |
1138 | 1138 | }, |
| 1139 | + nudgeDelay: function (d) { |
| 1140 | + blink.nudgeDelay(d); |
| 1141 | + }, |
| 1142 | + toggleFade: function () { |
| 1143 | + blink.toggleFade(); |
| 1144 | + }, |
1139 | 1145 | }); |
1140 | 1146 | } |
1141 | 1147 |
|
|
1155 | 1161 | ); |
1156 | 1162 | } |
1157 | 1163 | 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' } |
1167 | 1180 | ); |
| 1181 | + root.appendChild(hintbar(hints)); |
1168 | 1182 | } |
1169 | 1183 |
|
1170 | 1184 | /** |
|
1372 | 1386 | }; |
1373 | 1387 | } |
1374 | 1388 |
|
| 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 | + |
1375 | 1395 | /** |
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. |
1377 | 1413 | * |
1378 | 1414 | * @param {HTMLElement} stage The stage container to fill. |
1379 | 1415 | * @param {HTMLElement} bar2 The mode-control bar. |
1380 | 1416 | * @param {object} img The result's image paths. |
1381 | | - * @returns {object} `{ setPlaying, toggle, flip }` controls. |
| 1417 | + * @returns {object} `{ setPlaying, toggle, flip, nudgeDelay, toggleFade }`. |
1382 | 1418 | */ |
1383 | 1419 | function buildBlink(stage, bar2, img) { |
1384 | 1420 | var showBase = false; |
1385 | 1421 | 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 | + ]); |
1391 | 1428 | var label = h('span', { class: 'taglabel l', text: 'current' }); |
1392 | 1429 | var readout = h('b', { |
1393 | 1430 | style: { color: 'var(--text)' }, |
1394 | 1431 | text: 'current', |
1395 | 1432 | }); |
1396 | 1433 | 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 | + }; |
1397 | 1460 | var setShowBase = function (b) { |
1398 | 1461 | showBase = b; |
1399 | | - layer.style.visibility = b ? 'visible' : 'hidden'; |
| 1462 | + layer.style.opacity = b ? '1' : '0'; |
1400 | 1463 | label.textContent = b ? 'baseline' : 'current'; |
1401 | 1464 | readout.textContent = b ? 'baseline' : 'current'; |
1402 | 1465 | }; |
|
1410 | 1473 | if (p) { |
1411 | 1474 | blinkTimer = setInterval(function () { |
1412 | 1475 | setShowBase(!showBase); |
1413 | | - }, 650); |
| 1476 | + }, delay); |
1414 | 1477 | } |
1415 | 1478 | }; |
| 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 | + |
1416 | 1497 | playBtn.addEventListener('click', function () { |
1417 | 1498 | setPlaying(!playing); |
1418 | 1499 | }); |
| 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 | + |
1419 | 1510 | 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 | + ]) |
1421 | 1521 | ); |
1422 | 1522 | stage.appendChild( |
1423 | 1523 | h('div', { class: 'shot' }, [ |
|
1435 | 1535 | setPlaying(false); |
1436 | 1536 | setShowBase(!showBase); |
1437 | 1537 | }, |
| 1538 | + nudgeDelay: function (d) { |
| 1539 | + setDelay(delay + d * BLINK_STEP); |
| 1540 | + }, |
| 1541 | + toggleFade: function () { |
| 1542 | + setFade(!fade); |
| 1543 | + }, |
1438 | 1544 | }; |
1439 | 1545 | } |
1440 | 1546 |
|
|
1483 | 1589 | if (ctx.togglePlay) { |
1484 | 1590 | ctx.togglePlay(); |
1485 | 1591 | } |
| 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 | + } |
1486 | 1605 | } |
1487 | 1606 | }; |
1488 | 1607 | } |
|
1753 | 1872 | cmp: [ |
1754 | 1873 | ['Swipe / Side / Onion / Diff / Blink', ['1', '5']], |
1755 | 1874 | ['Adjust slider / flip blink', ['←', '→']], |
| 1875 | + ['Play / pause blink', ['space']], |
| 1876 | + ['Blink speed (slower / faster)', ['[', ']']], |
| 1877 | + ['Toggle blink crossfade', ['f']], |
1756 | 1878 | ['Next / prev changed result', ['n', 'p']], |
1757 | 1879 | ['Cycle viewport', ['v']], |
1758 | 1880 | ['HTML diff', ['h']], |
|
0 commit comments