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
2 files package.json+1 −1public/js/kepr.js+20 −29
package.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": {

view file

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]);

view file