1// A small canvas that three people have been painting for the last hour, and what each 2// part of the tour does to it. Everything shown is worked out from the paints, so the 3// canvas, the leaderboard and a pixel's history always agree. Times are minutes past 4// 09:00, and it's 09:45 now. 5 6const COLORS = {pink: "#FF99AA", yellow: "#FFD635", green: "#00A368", blue: "#2450A4"} 7const NOW = 45 8const HOUR = 60 9const SIZE = 6 10const PAINTERS = {alice: "Alice", bob: "Bob", carol: "Carol"} 11 12const paint = (id, x, y, color, painter, at) => ({id, cell: `${x},${y}`, color, painter, at}) 13 14const PAINTS = [ 15 paint(1, 1, 1, "blue", "alice", 5), 16 paint(2, 2, 1, "blue", "bob", 10), 17 paint(3, 1, 2, "yellow", "carol", 15), 18 paint(4, 2, 2, "blue", "alice", 20), 19 paint(5, 1, 1, "pink", "bob", 30), 20 paint(6, 3, 3, "green", "alice", 40), 21 paint(7, 4, 4, "blue", "carol", 42), 22] 23 24const NOW_PAINT = paint(8, 4, 1, "blue", "alice", NOW) 25const PAST_PAINT = paint(9, 0, 4, "yellow", "carol", 12) 26const FUTURE_PAINT = paint(10, 4, 1, "pink", "bob", 50) 27const ALL = [...PAINTS, NOW_PAINT, PAST_PAINT, FUTURE_PAINT] 28 29// What each step shows. A step of the article moves through a few of these in turn. 30export const STATES = { 31 start: {paints: PAINTS, marker: NOW}, 32 written: {paints: [...PAINTS, NOW_PAINT], marker: NOW}, 33 past: {paints: [...PAINTS, NOW_PAINT, PAST_PAINT], marker: NOW, ripple: [12, NOW]}, 34 future: {paints: ALL, marker: NOW}, 35 read: {paints: ALL, marker: 25}, 36 count: {paints: ALL, marker: 25, count: "blue"}, 37 painters: {paints: ALL, marker: 25, painters: true}, 38 leadersPast: {paints: ALL, marker: 25, leaderboard: true}, 39 leadersNow: {paints: ALL, marker: NOW, leaderboard: true}, 40 activePast: {paints: ALL, marker: 25, leaderboard: true, active: 10}, 41 activeNow: {paints: ALL, marker: NOW, leaderboard: true, active: 10}, 42 historyNow: {paints: ALL, marker: NOW, history: "1,1"}, 43 historyPast: {paints: ALL, marker: 25, history: "1,1"}, 44} 45 46const clock = (minutes) => 47 `${String(9 + Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}` 48 49const position = (minutes) => `${(minutes * 100) / HOUR}%` 50const fill = (color) => (color ? COLORS[color] : "rgb(255 255 255 / 0.08)") 51 52// Each cell's latest paint up to a moment. 53function canvasAt(paints, moment) { 54 const canvas = new Map() 55 ;[...paints].filter((p) => p.at <= moment).sort((a, b) => a.at - b.at).forEach((p) => canvas.set(p.cell, p)) 56 return canvas 57} 58 59function leaderboard(canvas, state) { 60 const owned = {} 61 for (const p of canvas.values()) owned[p.painter] = (owned[p.painter] || 0) + 1 62 const most = Math.max(1, ...Object.values(owned)) 63 64 return Object.entries(PAINTERS).map(([painter, name]) => ({ 65 painter, 66 name, 67 count: owned[painter] || 0, 68 width: ((owned[painter] || 0) * 100) / most, 69 active: 70 state.active !== undefined && 71 [...canvas.values()].some((p) => p.painter === painter && p.at > state.marker - state.active), 72 })) 73} 74 75function versions(state) { 76 const painted = state.paints 77 .filter((p) => p.cell === state.history && p.at <= state.marker) 78 .sort((a, b) => a.at - b.at) 79 .map((p) => ({color: p.color, by: PAINTERS[p.painter], at: p.at})) 80 81 return [{color: null, by: "", at: 0}, ...painted] 82} 83 84const element = (tag, className, parent) => { 85 const el = document.createElement(tag) 86 if (className) el.className = className 87 if (parent) parent.appendChild(el) 88 return el 89} 90 91// Restarts a CSS animation on an element. 92function replay(el, className) { 93 el.classList.remove(className) 94 void el.offsetWidth 95 el.classList.add(className) 96} 97 98export class TourView { 99 constructor(root) { 100 this.root = root 101 root.classList.add("tour") 102 103 this.caption = element("p", "visual-caption", root) 104 105 const stage = element("div", "tour-stage", root) 106 const canvas = element("div", "tour-canvas", stage) 107 this.cells = new Map() 108 109 for (let y = 0; y < SIZE; y++) { 110 for (let x = 0; x < SIZE; x++) { 111 const cell = element("div", "tour-cell", canvas) 112 const swatch = element("span", "tour-swatch", cell) 113 const letter = element("span", "tour-letter", cell) 114 this.cells.set(`${x},${y}`, {cell, swatch, letter, color: undefined, painter: undefined}) 115 } 116 } 117 118 const side = element("div", "tour-side", stage) 119 120 this.count = element("div", "tour-count", side) 121 this.countNumber = element("span", "tour-count-number", this.count)
122 element("span", "tour-count-label", this.count).textContent = "blue pixels at 09:25" 123 124 this.leaders = element("ol", "tour-leaders", side) 125 this.leaderRows = Object.entries(PAINTERS).map(([painter, name]) => { 126 const row = element("li", "", this.leaders) 127 element("span", "tour-leader-name", row).textContent = name 128 const track = element("span", "tour-leader-track", row) 129 const bar = element("span", "tour-leader-bar", track) 130 const count = element("span", "tour-leader-count", row) 131 const active = element("span", "tour-leader-active", row) 132 return {painter, bar, count, active} 133 }) 134 135 this.history = element("ol", "tour-history", side) 136 137 this.relations = element("div", "tour-relations", side) 138 const header = element("div", "tour-relations-header", this.relations) 139 element("span", "", header).textContent = "pixel" 140 element("span", "", header).textContent = "painted_by" 141 this.relationRows = element("ol", "", this.relations) 142 143 const timeline = element("div", "timeline", root) 144 this.track = element("div", "timeline-track", timeline)
145 element("span", "timeline-future", this.track).style.left = position(NOW) 146 this.activeBand = element("span", "tour-active-band", this.track) 147 this.ripple = element("span", "tour-ripple", this.track) 148 this.dots = new Map() 149 this.marker = element("span", "timeline-marker", this.track) 150 151 const labels = element("div", "timeline-labels", timeline) 152 for (const [label, minutes] of [["09:00", 0], ["09:15", 15], ["09:30", 30], ["now", NOW], ["10:00", 60]]) { 153 const span = element("span", minutes === NOW ? "is-now" : "", labels) 154 span.textContent = label 155 span.style.left = position(minutes) 156 } 157 158 this.state = null 159 } 160 161 show(state, {animate = true} = {}) { 162 const previous = this.state 163 this.state = state 164 this.root.classList.toggle("is-instant", !animate) 165 166 const canvas = canvasAt(state.paints, state.marker) 167 168 this.caption.innerHTML = 169 state.marker === NOW 170 ? `The canvas <strong>now</strong>, at 09:45` 171 : `The canvas <strong>as of ${clock(state.marker)}</strong>` 172 173 // Cells change color in place. A cell that's just been painted pops, while one that 174 // changes because we're looking at another moment fades from one color to the other. 175 const picked = new Set() 176 for (const [key, p] of canvas) if (state.count && p.color === state.count) picked.add(key) 177 if (state.history) picked.add(state.history) 178 179 const painted = new Set(state.paints.filter((p) => previous && !previous.paints.includes(p)).map((p) => p.id)) 180 181 for (const [key, view] of this.cells) { 182 const p = canvas.get(key) 183 const color = p ? p.color : null 184 185 if (view.color !== color) { 186 view.swatch.style.backgroundColor = fill(color) 187 if (animate && p && painted.has(p.id)) replay(view.swatch, "pop") 188 view.color = color 189 } 190 191 const painter = state.painters && p ? p.painter : null 192 if (view.painter !== painter) { 193 view.letter.textContent = painter ? PAINTERS[painter][0] : "" 194 if (animate && painter) replay(view.letter, "pop") 195 view.painter = painter 196 } 197 198 view.cell.classList.toggle("is-picked", picked.has(key)) 199 } 200 201 // Alongside the canvas: a count, the leaderboard, or a pixel's history. 202 this.count.hidden = !state.count 203 if (state.count) { 204 this.countNumber.textContent = [...canvas.values()].filter((p) => p.color === state.count).length 205 } 206 207 this.leaders.hidden = !state.leaderboard 208 if (state.leaderboard) { 209 const rows = leaderboard(canvas, state) 210 this.leaderRows.forEach((view, index) => { 211 view.bar.style.width = `${rows[index].width}%` 212 view.count.textContent = rows[index].count 213 view.active.classList.toggle("is-active", rows[index].active) 214 }) 215 } 216 217 this.history.hidden = !state.history 218 const historyKey = state.history ? `${state.history}@${state.marker}` : null 219 if (state.history && (!previous || historyKey !== this.historyKey)) { 220 this.history.replaceChildren( 221 ...versions(state).map((version, index) => { 222 const row = element("li", animate ? "rise" : "") 223 row.style.animationDelay = `${index * 100}ms` 224 element("span", "tour-history-swatch", row).style.backgroundColor = fill(version.color) 225 element("span", "tour-history-color", row).textContent = version.color || "white" 226 element("span", "tour-history-by", row).textContent = version.by 227 element("span", "tour-history-at", row).textContent = clock(version.at) 228 return row 229 }) 230 ) 231 } 232 this.historyKey = historyKey 233 234 // The relationship each pixel loaded, as of the moment: who had painted it then. 235 this.relations.hidden = !state.painters 236 const relationsKey = state.painters ? `${state.marker}` : null 237 if (state.painters && relationsKey !== this.relationsKey) { 238 const pixels = [...canvas.entries()].sort(([a], [b]) => { 239 const [ax, ay] = a.split(",").map(Number) 240 const [bx, by] = b.split(",").map(Number) 241 return ay - by || ax - bx 242 }) 243 244 this.relationRows.replaceChildren( 245 ...pixels.map(([key, p], index) => {
246 const row = element("li", animate ? "rise" : "") 247 row.style.animationDelay = `${index * 80}ms` 248 const pixel = element("span", "tour-relation-pixel", row) 249 element("span", "tour-history-swatch", pixel).style.backgroundColor = fill(p.color) 250 element("span", "", pixel).textContent = `(${key.replace(",", ", ")})` 251 const painter = element("span", "tour-relation-painter", row) 252 element("span", "tour-relation-initial", painter).textContent = PAINTERS[p.painter][0] 253 element("span", "", painter).textContent = PAINTERS[p.painter] 254 return row 255 }) 256 ) 257 } 258 this.relationsKey = relationsKey 259 260 // The timeline: every paint as a dot, and the moment being looked at. 261 const ids = new Set(state.paints.map((p) => p.id)) 262 for (const [id, dot] of this.dots) { 263 if (!ids.has(id)) { 264 dot.remove() 265 this.dots.delete(id) 266 } 267 } 268 269 for (const p of state.paints) { 270 let dot = this.dots.get(p.id) 271 if (!dot) { 272 dot = element("span", "tour-dot") 273 dot.style.left = position(p.at) 274 dot.style.backgroundColor = COLORS[p.color] 275 this.track.insertBefore(dot, this.marker) 276 this.dots.set(p.id, dot) 277 if (animate && previous) replay(dot, "pop") 278 } 279 dot.classList.toggle("is-dim", Boolean(state.history) && p.cell !== state.history) 280 dot.classList.toggle("is-focus", state.history === p.cell) 281 } 282 283 this.marker.style.left = position(state.marker) 284 285 this.activeBand.hidden = state.active === undefined 286 if (state.active !== undefined) { 287 this.activeBand.style.left = position(state.marker - state.active) 288 this.activeBand.style.width = position(state.active) 289 } 290 291 this.ripple.hidden = !state.ripple 292 if (state.ripple) { 293 this.ripple.style.left = position(state.ripple[0]) 294 this.ripple.style.width = position(state.ripple[1] - state.ripple[0]) 295 if (animate && !(previous && previous.ripple)) replay(this.ripple, "sweep") 296 } 297 } 298}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.