b420f182b4cc
space the timeline's dots by the capped gap between saves so it loops without long empty stretches (v3.0.103)
- time
- 2026-09-29 18:08 UTC · 2 weeks ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- match kepr 2 repo page
- parent
- 84560fbbe45a
↓ kepr-b420f182.tar.gzatlas at this save
files changed2 files
package.json+1 −1public/js/kepr.js+20 −29package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.102",3+ "version": "3.0.103",44 "private": true,55 "type": "module",66 "scripts": {public/js/kepr.js+20 −29
@@ -19,11 +19,9 @@1919 [/\b(polish|style|format|lint|whitespace|typo|wording|copy)\b/i, "text-secondary", 0.66],2020 [/\b(chore|deps|update|upgrade|build|ci|config)\b/i, "text-secondary", 0.62],2121 ];22-const DAY = 86400;23-24-// Kepr 2's timeline (canvas.js keprThread): the saves on a strip three views wide that drifts25-// slowly and loops, dots pushed apart so none overlap. Hovering pauses it and shows the save,26-// dragging scrubs, a click opens the save. Under reduced motion it moves only when dragged.22+// Kepr 2's timeline (canvas.js keprThread): the saves on a strip that drifts slowly and loops.23+// Hovering pauses it and shows the save, dragging scrubs, a click opens the save. Under reduced24+// motion it moves only when dragged.2725 function timeline() {2826 const svg = document.querySelector("svg.timeline");2927 if (!svg) return;@@ -52,11 +50,11 @@5250 tip.hidden = true;5351 document.body.append(tip);5452 const reduce = matchMedia("(prefers-reduced-motion: reduce)");55- const [first, last] = [saves[0].t, saves[saves.length - 1].t];56- const end = Math.max(Date.now() / 1000, last) + DAY;57- const start = Math.min(first, Date.now() / 1000) - Math.max(0, 7 * DAY - (end - first));5853 let [width, height, strip, radius, offset] = [0, 0, 0, 0, 0];59- const xOf = (t) => (strip * (t - start)) / (end - start);54+ // Laid out by time, a repo's bursts sat between long empty stretches the drift crawled55+ // through. Each dot instead sits after the one before by the gap between them, on a log scale56+ // and capped, so a busy day stays tight and a quiet month reads as a pause, not a wait.57+ const spacing = (seconds) => radius + 8 + Math.min(96, 12 * Math.log2(1 + seconds / 3600));6058 const resize = () => {6159 width = canvas.parentElement.getBoundingClientRect().width;6260 // Kepr 2's band was tall to hold two lanes; with one line, only room for the dots and label.@@ -66,21 +64,24 @@6664 canvas.style.width = `${width}px`;6765 canvas.style.height = `${height}px`;6866 ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);69- strip = Math.max(width * 3, saves.length * 48);7067 radius = 4;71- let previous = Number.NEGATIVE_INFINITY;72- for (const save of saves) {73- save.x = Math.max(xOf(save.t), previous + radius + 8);74- previous = save.x;75- }76- offset = Math.max(0, previous - width * 0.82);68+ let x = 0;69+ saves.forEach((save, i) => {70+ if (i > 0) x += spacing(save.t - saves[i - 1].t);71+ save.x = x;72+ });73+ // The seam is one more capped pause, or the rest of the view when the saves are few.74+ strip = Math.max(x + spacing(Number.POSITIVE_INFINITY), width);75+ offset = x - width * 0.82;7776 };7877 // Every visible copy of each x on the looping strip.7978 const onScreen = (x, margin) => {8079 const shift = ((offset % strip) + strip) % strip;81- return [-1, 0, 1]82- .map((k) => x - shift + k * strip)83- .filter((sx) => sx > -margin && sx < width + margin);80+ const copies = [];81+ for (let sx = x - shift - strip; sx < width + margin; sx += strip) {82+ if (sx > -margin) copies.push(sx);83+ }84+ return copies;8485 };8586 const hit = (event) => {8687 const box = canvas.getBoundingClientRect();@@ -130,16 +131,6 @@130131 const y = height / 2;131132 ctx.clearRect(0, 0, width, height);132133 ctx.lineWidth = 1;133- ctx.strokeStyle = css.getPropertyValue("--border");134- const years = [new Date(start * 1000).getFullYear(), new Date(end * 1000).getFullYear()];135- for (let year = years[0]; year <= years[1]; year++) {136- for (const x of onScreen(xOf(new Date(year, 0, 1) / 1000), 0)) {137- ctx.beginPath();138- ctx.moveTo(x, height * 0.1);139- ctx.lineTo(x, height * 0.9);140- ctx.stroke();141- }142- }143134 ctx.globalAlpha = 0.7;144135 ctx.strokeStyle = css.getPropertyValue("--text-muted");145136 ctx.setLineDash([4, 4]);