yummy.cricket/rainbow-quox/script/history.ts

128 lines
3.5 KiB
TypeScript

import { Colors as Oklchs, Rgbs } from './color.js';
import * as Color from './color.js';
export class HistoryItem {
name: string;
oklch: Oklchs;
rgb: Rgbs;
constructor(name: string, oklch: Oklchs, rgb: Rgbs) {
this.oklch = oklch;
this.rgb = rgb;
this.name = name;
}
asHtml(): HTMLButtonElement {
const { lines: bg, outer, belly1: belly, fins1: fins } = this.rgb;
const content = `
<svg class=history-colors width=30 height=25 viewBox="-10 -10 140 120">
<rect x=-10 y=-10 width=140 height=120 fill="${bg.css()}" />
<path fill="${fins.css()}" d="M 60,0 h -57.73 v 100 z">
<title>fin colour: ${fins.css()}</title>
</path>
<path fill="${belly.css()}" d="M 70,0 h 40 l -57.73,100 h -40 z">
<title>belly colour: ${belly.css()}</title>
</path>
<path fill="${outer.css()}" d="M 120,0 v 100 h -57.73 z">
<title>outer body colour: ${outer.css()}</title>
</path>
<desc>
sample of the palette for ${this.name}.
fin colour: ${fins.css()}.
belly colour: ${belly.css()}.
outer body colour: ${outer.css()}.
</desc>
</svg>
<span class=history-name>${this.name}</span>
`;
let button = document.createElement('button');
button.className = 'history-item';
button.dataset.name = this.name;
button.innerHTML = content;
return button;
}
}
export class History {
items: string[];
constructor(items: string[] = []) { this.items = items; }
add(name: string): void { this.items.push(name); }
*iterNames(maxLength?: number | null): Iterable<string> {
let seen = new Set<string>;
let done = 0;
if (maxLength === undefined) maxLength = 100;
for (let i = this.items.length - 1; i >= 0; i--) {
if (maxLength !== null && done > maxLength) break;
const name = this.items[i]!;
if (!name || seen.has(name)) continue;
seen.add(name); done++;
yield name;
}
}
*iterItems(maxLength?: number | null): Iterable<HistoryItem> {
for (const name of this.iterNames(maxLength)) {
const oklch = Color.colors(new Color.Rand(name), Color.KNOWN[name]);
const rgbs = Color.toRgbs(oklch);
yield new HistoryItem(name, oklch, rgbs);
}
}
static validate(x: unknown): History | undefined {
if (!Array.isArray(x)) return;
if (!x.every(i => typeof i === 'string')) return;
return new History(x);
}
toJSON() { return this.items; }
save(persist = true) {
const storage = persist ? localStorage : sessionStorage;
storage.setItem('history', JSON.stringify(this));
}
// if the json was invalid, return it
// if no history exists just start a new one
static load(): History | string {
const json =
sessionStorage.getItem('history') ??
localStorage.getItem('history');
if (json != null) {
let h = History.validate(JSON.parse(json));
if (h) { h.prune(); return h; }
else return json;
} else {
return new History;
}
}
// if the json is invalid, discard it
static loadOrClear(): History {
const h = History.load();
return h instanceof History ? h : new History;
}
addSave(name: string, persist = true): void {
this.add(name);
this.save(persist);
}
prune(maxLength?: number | null) {
let keep = [];
for (let name of this.iterNames(maxLength)) keep.push(name);
this.items = keep.reverse();
}
pruneSave(maxLength?: number | null, persist = true) {
this.prune(maxLength);
this.save(persist);
}
}