From 0a59aa66f655c94d45e1b85ce81675adf8af4742 Mon Sep 17 00:00:00 2001 From: Rhiannon Morris Date: Fri, 13 Dec 2024 03:14:37 +0100 Subject: [PATCH] history, reduced-motion, editable name box, an easter egg --- rainbow-quox/back.svg | 2 + rainbow-quox/close.svg | 16 ++ rainbow-quox/history.svg | 43 +++++ rainbow-quox/index.html | 24 ++- rainbow-quox/script/color.ts | 188 +++++++++++++++---- rainbow-quox/script/history.ts | 128 +++++++++++++ rainbow-quox/script/quox.ts | 132 +++++++++++--- rainbow-quox/script/rand.ts | 132 +++++++++++--- rainbow-quox/showui.svg | 2 +- rainbow-quox/style/3px-tile.png | 3 + rainbow-quox/style/bright-squares.png | 3 - rainbow-quox/style/groovepaper.dark.png | 3 + rainbow-quox/style/groovepaper.png | 3 + rainbow-quox/style/style.scss | 230 ++++++++++++++++++++---- 14 files changed, 782 insertions(+), 127 deletions(-) create mode 100644 rainbow-quox/close.svg create mode 100644 rainbow-quox/history.svg create mode 100644 rainbow-quox/script/history.ts create mode 100644 rainbow-quox/style/3px-tile.png delete mode 100644 rainbow-quox/style/bright-squares.png create mode 100644 rainbow-quox/style/groovepaper.dark.png create mode 100644 rainbow-quox/style/groovepaper.png diff --git a/rainbow-quox/back.svg b/rainbow-quox/back.svg index 5ca488c..6db0442 100644 --- a/rainbow-quox/back.svg +++ b/rainbow-quox/back.svg @@ -2,6 +2,8 @@ + go back + back to the cube. diff --git a/rainbow-quox/close.svg b/rainbow-quox/close.svg new file mode 100644 index 0000000..fd713a6 --- /dev/null +++ b/rainbow-quox/close.svg @@ -0,0 +1,16 @@ + + + close + + + + + + + + + + diff --git a/rainbow-quox/history.svg b/rainbow-quox/history.svg new file mode 100644 index 0000000..174ce3b --- /dev/null +++ b/rainbow-quox/history.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rainbow-quox/index.html b/rainbow-quox/index.html index ff59dd6..443422a 100644 --- a/rainbow-quox/index.html +++ b/rainbow-quox/index.html @@ -1,5 +1,6 @@ + @@ -15,14 +16,31 @@
- show ui +
-
quox #0
+
+ + +
+ hello my name is + anonymous + +
+ +
+
+
diff --git a/rainbow-quox/script/color.ts b/rainbow-quox/script/color.ts index 2fa9fab..1cd1d39 100644 --- a/rainbow-quox/script/color.ts +++ b/rainbow-quox/script/color.ts @@ -52,11 +52,26 @@ function isLight(l: Luma): boolean { return l >= MINL_LIGHT; } export namespace Rand { export type State = R.State; } -export class Rand extends R.Rand { - constructor(seed?: R.State) { super(seed); } +type CloseFar = 'close' | 'far'; - lightFor(baseL: Luma): Luma { return this.float(baseL, MAXL); } - darkFor(baseL: Luma): Luma { return this.float(MINL, baseL); } +export class Rand extends R.Rand { + constructor(); + constructor([a, b, c, d]: Rand.State); + constructor(str: string); + constructor(st?: Rand.State | string) { + if (st === undefined) super(); + else if (typeof st === 'string') super(st); + else super(st); + } + + lightFor(baseL: Luma, d: CloseFar = 'close'): Luma { + let maxl = d == 'close' ? min(MAXL, baseL * 1.25) : MAXL; + return this.float(baseL, maxl); + } + darkFor(baseL: Luma, d: CloseFar = 'close'): Luma { + let minl = d == 'close' ? max(MINL, baseL * 0.8) : MINL + return this.float(minl, baseL); + } brightFor(l: Luma, baseC: Chroma): Chroma { return this.float(baseC, isLight(l) ? MAXC_LIGHT : MAXC_DARK); @@ -140,14 +155,6 @@ export class Oklch { } } - css(alpha: number = 1): string { - const l = (this.l * 100).toFixed(0); - const c = (this.c * 250).toFixed(0); - const h = this.h.toFixed(0); - if (alpha != 1) { return `oklch(${l}% ${c}% ${h} / ${alpha})`; } - else { return `oklch(${l}% ${c}% ${h})`; } - } - with(maps: Oklch.With): Oklch { function call(comp: Oklch.With1, x: number) { switch (typeof comp) { @@ -163,7 +170,23 @@ export class Oklch { }); } + css(alpha: number = 1): string { + const l = (this.l * 100).toFixed(0); + const c = (this.c * 250).toFixed(0); + const h = this.h.toFixed(0); + if (alpha != 1) { return `oklch(${l}% ${c}% ${h} / ${alpha})`; } + else { return `oklch(${l}% ${c}% ${h})`; } + } + rgb(): Rgb { return toRgbViaCanvas(this); } + + static validate(x: unknown): Oklch | undefined { + if (typeof x == 'object' && x != null && 'l' in x && 'c' in x && 'h' in x) { + const { l, c, h } = x; + if (typeof l == 'number' && typeof c == 'number' && typeof h == 'number') + return oklch(l, c, h); + } + } } @@ -196,9 +219,15 @@ export function makeColorInfo(f: (l: Layer) => A): Record { return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record; } +export type BaseCol = 'outer' | 'belly' | 'fins'; +export type OptionalBaseCol = 'eyes' | 'stripes'; -export function colors(r: Rand = new Rand()): Scheme { - const outer = new Oklch(r, 'dark'); +type KnownPalette = + Record & Partial>; + + +export function colors(r: Rand = new Rand(), base?: KnownPalette): Scheme { + const outer = base?.outer ?? new Oklch(r, 'dark'); let outerCols: OuterCols = { outer, spines: mkSpines(r, outer), vitiligo1: mkVitiligo(r, outer) }; @@ -210,18 +239,18 @@ export function colors(r: Rand = new Rand()): Scheme { if (whichBody > 2/3) { type = 'triad'; const [f, b] = r.triad(outer.h); - finCols = mkFins(r, f, outer); bellyCols = mkBelly(r, b); + finCols = mkFins(r, f, outer, base); bellyCols = mkBelly(r, b, base); } else if (whichBody > 1/3) { type = 'fin-belly'; const [f, b] = r.complementary(outer.h, 2); - finCols = mkFins(r, f!, outer); bellyCols = mkBelly(r, b!); + finCols = mkFins(r, f!, outer, base); bellyCols = mkBelly(r, b!, base); } else { type = 'fin-body'; - finCols = mkFins(r, r.analogous1(outer.h), outer); - bellyCols = mkBelly(r, r.complementary1(outer.h)); + finCols = mkFins(r, r.analogous1(outer.h), outer, base); + bellyCols = mkBelly(r, r.complementary1(outer.h), base); } - let miscCols = mkMisc(r, outerCols, finCols, bellyCols); + let miscCols = mkMisc(r, outerCols, finCols, bellyCols, base); return merge(outerCols, sockCols, finCols, bellyCols, miscCols, type); } @@ -229,9 +258,9 @@ export function colors(r: Rand = new Rand()): Scheme { function mkSpines(r: Rand, outer: Oklch): Oklch { return outer.with({ - l: x => x * 0.8, - c: x => x * 1.1, - h: x => r.float(x + 12, x - 12), + l: l => r.darkFor(l), + c: c => r.brightFor(outer.l, c), + h: h => r.float(h + 12, h - 12), }) } @@ -258,8 +287,10 @@ function mkCuffs(r: Rand, sock: Oklch): Oklch { }); } -function mkFins(r: Rand, h: Hue, outer: Oklch): FinCols { - const [fin1Hue, fin2Hue, fin3Hue] = r.analogous(h, 3); +function mkFins(r: Rand, h: Hue, outer: Oklch, base?: KnownPalette): FinCols { + const baseFin1 = base?.fins; + const [fin1Hue, fin2Hue, fin3Hue] = r.analogous(baseFin1?.h ?? h, 3); + const direction: 'lighter' | 'darker' = r.choice(['lighter', 'darker']); function ll(l: Luma): Luma { @@ -269,17 +300,18 @@ function mkFins(r: Rand, h: Hue, outer: Oklch): FinCols { return direction == 'lighter' ? r.dullFor(l, c) : r.brightFor(l, c); } - const fins1 = new Oklch(ll(outer.l), cc(outer.l, outer.c), fin1Hue!); - const fins2 = new Oklch(ll(fins1.l), cc(fins1.l, fins1.c), fin2Hue!); - const fins3 = new Oklch(ll(fins2.l), cc(fins2.l, fins2.c), fin3Hue!); + const fins1 = baseFin1 ?? oklch(ll(outer.l), cc(outer.l, outer.c), fin1Hue!); + const fins2 = oklch(ll(fins1.l), cc(fins1.l, fins1.c), fin2Hue!); + const fins3 = oklch(ll(fins2.l), cc(fins2.l, fins2.c), fin3Hue!); const lighter = fins1.l >= fins3.l ? fins1 : fins3; const vitiligo4 = mkVitiligo(r, lighter); return { fins1, fins2, fins3, vitiligo4 }; } -function mkBelly(r: Rand, h: Hue): BellyCols { - const [belly1Hue, belly2Hue] = r.analogous(h, 2); - const belly1 = new Oklch({ +function mkBelly(r: Rand, h: Hue, base?: KnownPalette): BellyCols { + let baseBelly1 = base?.belly; + const [belly1Hue, belly2Hue] = r.analogous(baseBelly1?.h ?? h, 2); + const belly1 = baseBelly1 ?? new Oklch({ l: r.float(0.7, MAXL), c: r.baseChroma(1), h: belly1Hue! @@ -294,7 +326,8 @@ function mkBelly(r: Rand, h: Hue): BellyCols { return { belly1, belly2, vitiligo2, vitiligo3 }; } -function mkMisc(r: Rand, o: OuterCols, f: FinCols, b: BellyCols): MiscCols { +function mkMisc(r: Rand, o: OuterCols, f: FinCols, b: BellyCols, + base?: KnownPalette): MiscCols { const masks = new Oklch({ l: r.float(0.8, MAXL), c: r.float(0.01, 0.06), @@ -302,7 +335,7 @@ function mkMisc(r: Rand, o: OuterCols, f: FinCols, b: BellyCols): MiscCols { }); return { masks, - eyes: new Oklch({ + eyes: base?.eyes ?? new Oklch({ l: r.baseLuma('light'), c: r.float(0.28, MAXC_LIGHT), h: r.boolean() ? r.analogous1(o.outer.h) : r.complementary1(o.outer.h) @@ -365,6 +398,14 @@ export class Rgb { } return `#${h(this.r)}${h(this.g)}${h(this.b)}` } + + static validate(x: unknown): Rgb | undefined { + if (typeof x == 'object' && x != null && 'r' in x && 'g' in x && 'b' in x) { + const { r, g, b } = x; + if (typeof r == 'number' && typeof g == 'number' && typeof b == 'number') + return rgb(r, g, b); + } + } } export type Rgbs = Record; @@ -375,8 +416,8 @@ export function toRgbViaCanvas(col: Oklch): Rgb { rgbBuf ??= new OffscreenCanvas(1, 1).getContext('2d')!; rgbBuf.fillStyle = col.css(); rgbBuf.fillRect(0, 0, 1, 1); - const rgb = rgbBuf.getImageData(0, 0, 1, 1).data; - return new Rgb(rgb[0]!, rgb[1]!, rgb[2]!); + const pix = rgbBuf.getImageData(0, 0, 1, 1).data; + return rgb(pix[0]!, pix[1]!, pix[2]!); } export function toRgbs(col: Colors): Rgbs { @@ -390,3 +431,82 @@ export function toHex({r, g, b}: Rgb): string { } return `#${chan(r)}${chan(g)}${chan(b)}`; } + + +export function oklch(l: number, c: number, h: number) { + return new Oklch(l, c, h); +} + +export function rgb(r: number, g: number, b: number) { + return new Rgb(r, g, b); +} + + +export const KNOWN: Record = { + niss: { + outer: oklch(0.83, 0.201, 151), + belly: oklch(0.87, 0.082, 99), + fins: oklch(0.68, 0.178, 16), + eyes: oklch(0.73, 0.135, 242), + }, + kesi: { + outer: oklch(0.86, 0.147, 147), + belly: oklch(0.96, 0.04, 108), + fins: oklch(0.94, 0.142, 102), + eyes: oklch(0.76, 0.115, 300), + }, + 60309: { + outer: oklch(0.84, 0.068, 212), + belly: oklch(0.56, 0.035, 233), + fins: oklch(0.55, 0.101, 268), + eyes: oklch(0.86, 0.146, 154), + }, + 'prickly pear': { + outer: oklch(0.64, 0.087, 316), + belly: oklch(0.88, 0.03, 88), + fins: oklch(0.6, 0.071, 142), + eyes: oklch(0.66, 0.091, 134), + }, + 'the goo': { + outer: oklch(0.92, 0.046, 354), + belly: oklch(0.83, 0.099, 354), + fins: oklch(0.74, 0.115, 354), + eyes: oklch(0.73, 0.149, 0), + }, + lambda: { + outer: oklch(0.71, 0.154, 58), + belly: oklch(0.9, 0.05, 80), + fins: oklch(0.76, 0.16, 140), + eyes: oklch(0.82, 0.178, 141), + }, + flussence: { + outer: oklch(0.77, 0.118, 133), + belly: oklch(0.71, 0.086, 253), + fins: oklch(0.58, 0.102, 254), + eyes: oklch(0.37, 0.107, 278), + }, + serena: { + outer: oklch(0.69, 0.176, 349), + belly: oklch(0.92, 0.04, 350), + fins: oklch(0.74, 0.138, 319), + eyes: oklch(0.65, 0.206, 4), + }, + pippin: { + outer: oklch(0.74, 0.08, 61), + belly: oklch(0.82, 0.062, 70), + fins: oklch(0.52, 0.09, 45), + eyes: oklch(0.74, 0.167, 136), + }, + su: { + outer: oklch(0.29, 0.012, 219), + belly: oklch(0.89, 0.01, 256), + fins: oklch(0.53, 0.093, 20), + // eyes: oklch(0.53, 0.109, 254), + }, + trans: { + outer: oklch(0.83, 0.065, 228), + belly: oklch(0.95, 0.021, 137), + fins: oklch(0.86, 0.069, 352), + // eyes: oklch(0.57, 0.158, 273), + }, +}; diff --git a/rainbow-quox/script/history.ts b/rainbow-quox/script/history.ts new file mode 100644 index 0000000..af8f71e --- /dev/null +++ b/rainbow-quox/script/history.ts @@ -0,0 +1,128 @@ +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 = ` + + + + fin colour: ${fins.css()} + + + belly colour: ${belly.css()} + + + outer body colour: ${outer.css()} + + + sample of the palette for ${this.name}. + fin colour: ${fins.css()}. + belly colour: ${belly.css()}. + outer body colour: ${outer.css()}. + + + ${this.name} + `; + + 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 { + let seen = new Set; + 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 { + 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); + } +} diff --git a/rainbow-quox/script/quox.ts b/rainbow-quox/script/quox.ts index 4a2b7d8..d23f667 100644 --- a/rainbow-quox/script/quox.ts +++ b/rainbow-quox/script/quox.ts @@ -1,6 +1,5 @@ import * as Color from './color.js'; - -type State = Color.Rand.State; +import { History } from './history.js'; async function loadBitmap(url: string): Promise { @@ -177,46 +176,55 @@ function message(msg: string, error = false) { } -function urlState(): State | undefined { - const str = document.location.hash; - if (str?.match(/^#\d+$/)) return parseInt(str.substring(1)); +function urlState(): string | undefined { + let hash = document.location.hash?.substring(1); + if (hash != '' && hash !== undefined) return decodeURI(hash); } -function updateUrl(state: State): void { - history.replaceState({}, '', `#${state}`); +function updateUrl(seed: string): void { + history.replaceState({}, '', `#${encodeURI(seed)}`); } -type ApplyStateOpts = - { side: Side, state: State, firstLoad: boolean, buf: Buffer, done: Done }; +type ApplyStateOpts = { + seed: string, + side?: Side, + firstLoad?: boolean, + buf?: Buffer, + history?: History, + done?: Done, +}; async function -applyState(data: LayerData, opts: Partial): Promise { - let { side, state, firstLoad, buf, done } = opts; +applyState(data: LayerData, opts: ApplyStateOpts): Promise { + let { side, seed, firstLoad, buf, history, done } = opts; side ??= 'front'; firstLoad ??= false; buf ??= new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!; + done ??= () => {}; - let r = new Color.Rand(state); - const initState = r.state; + let rand = new Color.Rand(seed); - const oklch = Color.colors(r); + const oklch = Color.colors(rand, Color.KNOWN[seed]); const rgb = Color.toRgbs(oklch); - const newState = r.state; + const newSeed = rand.alphaNum(); await recolorLayers(data, rgb); updateBg(oklch); updateSvgs(oklch, rgb); - updateLabel(initState); - updateUrl(initState); + updateLabel(seed); + updateUrl(seed); if (firstLoad) { await instantUpdateImage(side, await ensureComposed(buf, data)); + done(); } else { - await animateUpdateImage(buf, side, data, done ?? (() => {})); + await animateUpdateImage(buf, side, data, done); } - return newState; + if (history) history.addSave(seed); + + return newSeed; } type CanvasId = 'main' | 'aux'; @@ -233,8 +241,16 @@ instantUpdateImage(side: Side, data: ComposedData) { type Done = () => void; +const noAnim = matchMedia('(prefers-reduced-motion: reduce)'); + async function animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) { + if (noAnim.matches) { + instantUpdateImage(side, await ensureComposed(buf, data)); + done(); + return; + } + const duration = 200; const main = getCanvasCtx('main'); @@ -258,6 +274,12 @@ animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) { async function animateSwapImage(buf: Buffer, newSide: Side, data: ComposedData, done: Done) { + if (noAnim.matches) { + instantUpdateImage(newSide, data); + done(); + return; + } + const duration = 400; const main = getCanvasCtx('main'); @@ -284,9 +306,9 @@ function updateBg(cols: Color.Colors) { document.documentElement.style.setProperty('--hue', `${cols.outer.h}`); } -function updateLabel(st: State) { - const stateLabel = document.getElementById('state'); - if (stateLabel) stateLabel.innerHTML = `${st}`; +function updateLabel(seed: string) { + const stateLabel = document.getElementById('current-name'); + if (stateLabel) stateLabel.innerHTML = seed; } function updateSvgs(oklch: Color.Colors, rgb: Color.Rgbs) { @@ -327,21 +349,56 @@ function updateSvgs(oklch: Color.Colors, rgb: Color.Rgbs) { } +function showHistory(history: History, data: LayerData, + opts: Omit) { + const list = document.getElementById('history-items'); + if (!list) return; + list.innerHTML = ''; + let { side, firstLoad, buf, done } = opts; + + for (const item of history.iterItems()) { + const elem = item.asHtml(); + let allOpts = { side, firstLoad, buf, done, seed: item.name, history }; + elem.addEventListener('click', () => { + applyState(data, allOpts); + }); + list.appendChild(elem); + } + + setTimeout(() => { + document.documentElement.dataset.state = 'history'; + const elem = document.createElement('div'); + elem.id = 'history-close-target'; + elem.addEventListener('click', closeHistory); + document.body.appendChild(elem); + }); +} + +function closeHistory() { + document.getElementById('history-items')?. + scroll({top: 0, left: 0, behavior: 'smooth'}); + let field = document.getElementById('history-close-target'); + if (field) field.parentElement?.removeChild(field); + document.documentElement.dataset.state = 'ready'; +} + async function setup() { message('loading layers…'); let data = await loadData().catch(e => { message(e, true); throw e }); + let history = History.loadOrClear(); let buf = new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!; - let state = urlState(); + let seed = urlState() ?? new Color.Rand().alphaNum(); let side: Side = 'front'; - state = await applyState(data, { state, buf, firstLoad: true }); + seed = await applyState(data, { seed, buf, history, firstLoad: true }); const reroll = document.getElementById('reroll')!; const swap = document.getElementById('swap')!; addListeners(); + // these ones don't need to be toggled document.getElementById('hideui')?.addEventListener('click', () => { document.documentElement.dataset.state = 'fullquox'; @@ -349,6 +406,24 @@ async function setup() { document.getElementById('showui')?.addEventListener('click', () => { document.documentElement.dataset.state = 'ready'; }); + document.getElementById('history-button')?.addEventListener('click', () => { + // does this need the add/remove listeners dance + // actually does anything any more? + showHistory(history, data, { side, buf }); + }); + document.getElementById('close-history')?.addEventListener('click', closeHistory); + document.getElementById('current-name')?.addEventListener('focusout', async e => { + const space = String.raw`(\n|\s|
| )`; + const re = new RegExp(`^${space}+|${space}+$`, 'msgu'); + + let elem = e.target as HTMLElement; + let str = elem.innerText.replaceAll(re, ''); + if (!str) str = new Color.Rand().alphaNum(); + elem.innerText = str; + // todo allow images cos it's funny + + seed = await applyState(data, { side, seed: str, buf, history }); + }); document.documentElement.dataset.state = 'ready'; @@ -359,15 +434,16 @@ async function setup() { function updateFromUrl() { run(async k => { - const newState = urlState(); - if (newState) { - state = await applyState(data, { side, state: newState, buf, done: k }); + const newSeed = urlState(); + if (newSeed) { + const opts = { history, side, seed: newSeed, buf, done: k }; + seed = await applyState(data, opts); } }); } function runReroll() { run(async k => { - state = await applyState(data, { side, state, buf, done: k }); + seed = await applyState(data, { side, seed, buf, history, done: k }); }); } function runSwap() { diff --git a/rainbow-quox/script/rand.ts b/rainbow-quox/script/rand.ts index aae8bdb..efc8a1c 100644 --- a/rainbow-quox/script/rand.ts +++ b/rainbow-quox/script/rand.ts @@ -1,45 +1,123 @@ -// https://stackoverflow.com/a/424445 thanks my dude +// https://stackoverflow.com/questions/521295#47593316 -export type State = number; +export type State = [number, number, number, number]; -const M = 0x80000000; -const A = 1103515245; -const C = 12345; - -export class Rand { - state: number; - - constructor(state?: State) { - this.state = typeof state == 'number' && !isNaN(state) ? - state : Math.floor(Math.random() * (M - 1)); - } - - #next(): number { return this.state = (A * this.state + C) % M; } - - #float(x?: number, y?: number): number { - const [lo, hi] = - x === undefined ? [0, 1] : - y === undefined ? [0, x] : - [Math.min(x, y), Math.max(x, y)]; - - return lo + this.#next() / (M - 1) * (hi - lo); - } +export interface Randy { + state: State; int(): number; // whole int32 range int(x: number): number; // [0, x) int(x: number, y: number): number; // [x, y) + + float(): number; // [0, 1) + float(x: number): number; // [0, x) + float(x: number, y: number): number; // [x, y) + + choice
(array: A[]): A; + boolean(): boolean; +} + + +export function fromHex(s: string): State { + const a = s.substring(0, 8); + const b = s.substring(8, 16); + const c = s.substring(16, 24); + const d = s.substring(24, 32); + return [h(a), h(b), h(c), h(d)]; + + function h(x: string) { return parseInt(x, 16); } +} + +export function toHex([a, b, c, d]: State): string { + return `${h(a)}${h(b)}${h(c)}${h(d)}`; + function h(x: number) { return x.toString(16); } +} + + +const UINT_MAX = 4294967296; + +export class Rand implements Randy { + #a: number; #b: number; #c: number; #d: number; + + constructor(); + constructor([a, b, c, d]: State); + constructor(str: string); + constructor(st?: State | string) { + const [a, b, c, d] = + st === '' ? s4() : + typeof st === 'string' ? Rand.stateFrom(st) : + st ?? s4(); + this.#a = a; this.#b = b; this.#c = c; this.#d = d; + + for (let i = 0; i < 20; ++i) this.#next(); + + function s() { return (Math.random() * 2**32) >>> 0; } + function s4() { return [s(), s(), s(), s()] as const; } + } + + get state(): State { return [this.#a, this.#b, this.#c, this.#d]; } + + get stateString() { return toHex(this.state); } + + static stateFrom(str: string): State { + let h1 = 1779033703, h2 = 3144134277, + h3 = 1013904242, h4 = 2773480762; + for (let i = 0, k; i < str.length; i++) { + k = str.charCodeAt(i); + h1 = h2 ^ Math.imul(h1 ^ k, 597399067); + h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); + h3 = h4 ^ Math.imul(h3 ^ k, 951274213); + h4 = h1 ^ Math.imul(h4 ^ k, 2716044179); + } + h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067); + h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233); + h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213); + h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179); + h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1; + return [h1>>>0, h2>>>0, h3>>>0, h4>>>0]; + } + + int(): number; // whole int32 range + int(to: number): number; // [0, x) + int(from: number, to: number): number; // [x, y) int(x?: number, y?: number): number { - return x === undefined ? this.#next() : Math.floor(this.#float(x, y)); + if (x === undefined) return this.#next(); + return Math.floor(y === undefined ? this.float(x) : this.float(x, y)); } float(): number; // [0, 1) float(x: number): number; // [0, x) float(x: number, y: number): number; // [x, y) - float(x?: number, y?: number): number { return this.#float(x, y); } + float(x?: number, y?: number): number { + const in01 = this.#next() / UINT_MAX; + if (x === undefined) return in01; + const [lo, hi] = y === undefined? [0, x] : [x, y]; + return lo + in01 * (hi - lo); + } choice(array: A[]): A { - return array[this.int(0, array.length)]!; + return array[this.int(array.length)]!; } boolean(): boolean { return this.float() > 0.5; } + + alphaNum(len?: number): string { + let res = ""; + // [todo] is there a better way to make a string in js + if (len === undefined || len <= 0) len = 16; // idk + for (let i = 0; i < len; ++i) + res += this.choice(Array.from("abcdefghijklmnopqrstuvwxyz0123456789")); + return res; + } + + #next(): number { + this.#a |= 0; this.#b |= 0; this.#c |= 0; this.#d |= 0; + let t = (this.#a + this.#b | 0) + this.#d | 0; + this.#d = this.#d + 1 | 0; + this.#a = this.#b ^ this.#b >>> 9; + this.#b = this.#c + (this.#c << 3) | 0; + this.#c = (this.#c << 21 | this.#c >>> 11); + this.#c = this.#c + t | 0; + return (t >>> 0); + } } diff --git a/rainbow-quox/showui.svg b/rainbow-quox/showui.svg index 5fdd6f1..add1e2c 100644 --- a/rainbow-quox/showui.svg +++ b/rainbow-quox/showui.svg @@ -1,6 +1,6 @@ diff --git a/rainbow-quox/style/3px-tile.png b/rainbow-quox/style/3px-tile.png new file mode 100644 index 0000000..8acbe8c --- /dev/null +++ b/rainbow-quox/style/3px-tile.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dc7cd84f755aa0d799d5f9b51383329b699d09a66cd274fa3527cfc398e1bcc +size 5898 diff --git a/rainbow-quox/style/bright-squares.png b/rainbow-quox/style/bright-squares.png deleted file mode 100644 index bacde4f..0000000 --- a/rainbow-quox/style/bright-squares.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6446c779f15729bb9c47103bf4f9a2c831439ce7246943bea3715dfbdfaebb27 -size 41392 diff --git a/rainbow-quox/style/groovepaper.dark.png b/rainbow-quox/style/groovepaper.dark.png new file mode 100644 index 0000000..1e73900 --- /dev/null +++ b/rainbow-quox/style/groovepaper.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5494b4cf3fb6fd035fe82caf38283d1f6a8f932c84e1c9e9ff08635514af6cf0 +size 61201 diff --git a/rainbow-quox/style/groovepaper.png b/rainbow-quox/style/groovepaper.png new file mode 100644 index 0000000..f334520 --- /dev/null +++ b/rainbow-quox/style/groovepaper.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb51825f76616c381250a05993c2124152f1c5af467c2d9c43360cb963d23038 +size 59534 diff --git a/rainbow-quox/style/style.scss b/rainbow-quox/style/style.scss index 5be1ac5..8047321 100644 --- a/rainbow-quox/style/style.scss +++ b/rainbow-quox/style/style.scss @@ -1,3 +1,4 @@ +@use 'sass:math'; @import url(../../fonts/muller/muller.css); @keyframes swap1 { @@ -19,26 +20,59 @@ } -@mixin transy { - transition: transform 0.25s cubic-bezier(.47,.74,.61,1.2); +// https://oakreef.ie/transy :) +$transy-default: 250ms; +@mixin transy($prop: transform, $duration: $transy-default) { + transition: $prop $duration cubic-bezier(.47,.74,.61,1.2); } @mixin shadow { filter: drop-shadow(6px 6px 0 oklch(0.4 0.2 var(--hue) / 0.45)); @media (prefers-color-scheme: dark) { - filter: drop-shadow(6px 6px 0 oklch(0.1 0.15 var(--hue) / 0.45)); + filter: drop-shadow(6px 6px 0 oklch(0.1 0.125 var(--hue) / 0.45)); } } -@mixin box { +$box-texture: url(3px-tile.png); +$box-bg: oklch(0.3 0.2 var(--hue)); +$box-fg: oklch(0.95 0.075 var(--c-hue)); + +$button-bg: oklch(0.5 0.25 var(--hue)); +$button-fg: oklch(0.98 0.1 var(--c-hue)); + +@mixin box($font-scale: 1, $button: false) { @include shadow; - font: 700 20pt var(--font); // respecify font family for