export function lastOf(ts: T[]): T | null { return ts[ts.length - 1] || null } export function sortBy(ts: T[], key: (t: T) => number | string): void { function comparator(a: T, b: T) { return key(a) < key(b) ? -1 : 1 } ts.sort(comparator) } export function getOrInsert(map: Map, k: K, fallback: (k: K) => V): V { if (!map.has(k)) map.set(k, fallback(k)) return map.get(k)! } export function getOrElse(map: Map, k: K, fallback: (k: K) => V): V { if (!map.has(k)) return fallback(k) return map.get(k)! } export function getOrThrow(map: Map, k: K): V { if (!map.has(k)) { throw new Error(`Expected key ${k}`) } return map.get(k)! } export function* itMap(it: Iterable, f: (t: T) => U): Iterable { for (let t of it) { yield f(t) } } export function itForEach(it: Iterable, f: (t: T) => void): void { for (let t of it) { f(t) } } export function itReduce(it: Iterable, f: (a: U, b: T) => U, init: U): U { let accum: U = init for (let t of it) { accum = f(accum, t) } return accum } export function zeroPad(s: string, width: number) { return new Array(Math.max(width - s.length, 0) + 1).join('0') + s } // NOTE: This blindly assumes the same result across contexts. const measureTextCache = new Map() let lastDevicePixelRatio = -1 export function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: string): number { if (window.devicePixelRatio !== lastDevicePixelRatio) { // This cache is no longer valid! measureTextCache.clear() lastDevicePixelRatio = window.devicePixelRatio } if (!measureTextCache.has(text)) { measureTextCache.set(text, ctx.measureText(text).width) } return measureTextCache.get(text)! }