import * as Color from './color.js'; import { History } from './history.js'; import * as Layer from './layer.js'; import * as Palette from './palette.js'; function message(msg: string, size = 100) { const ctx = getCanvasCtx('main'); ctx.save(); ctx.clearRect(0, 0, Layer.WIDTH, Layer.HEIGHT); ctx.font = `bold ${size}px Muller, sans-serif`; ctx.textAlign = 'center'; ctx.fillText(msg, Layer.WIDTH/2, Layer.HEIGHT/2, Layer.WIDTH-10); ctx.restore(); } function urlState(): string | undefined { const hash = document.location.hash?.substring(1); if (hash != '' && hash !== undefined) return decodeURI(hash); } function updateUrl(seed: string): void { history.replaceState({}, '', `#${encodeURI(seed)}`); } type ApplyStateOpts = { seed: string, side?: Layer.Side, firstLoad?: boolean, history?: History, done?: Done, }; async function applyState(data: Layer.Data, opts: ApplyStateOpts): Promise { const { seed, history } = opts; let { side, firstLoad, done } = opts; side ??= 'front'; firstLoad ??= false; done ??= () => {}; const rand = new Color.Rand(seed); const cols = Color.colors(rand, Color.KNOWN[seed]); const newSeed = rand.alphaNum(); await Layer.recolorAll(data, cols); updateBg(cols); updateSvgs(cols); updateLabel(seed); updateUrl(seed); if (firstLoad) { await instantUpdateImage(side, data); done(); } else { await animateUpdateImage(side, data, done); } if (history) history.addSave(seed); return newSeed; } type CanvasId = 'main' | 'aux'; function getCanvasCtx(id: CanvasId) { const canvas = document.getElementById(id) as HTMLCanvasElement; return canvas.getContext('2d')!; } async function instantUpdateImage(side: Layer.Side, data: Layer.Data) { const cdata = await Layer.ensureComposed(getCanvasCtx('aux'), data); getCanvasCtx('main').putImageData(cdata[`${side}Image`], 0, 0); } type Done = () => void; const noAnim = matchMedia('(prefers-reduced-motion: reduce)'); async function animateUpdateImage(side: Layer.Side, data: Layer.Data, done: Done) { if (noAnim.matches) { await instantUpdateImage(side, data); done(); return; } const duration = 200; const main = getCanvasCtx('main'); const aux = getCanvasCtx('aux'); document.documentElement.dataset.running = 'reroll'; const cdata = await Layer.ensureComposed(aux, data); Layer.redraw(aux, cdata, side); aux.canvas.addEventListener('animationend', () => { Layer.redraw(main, cdata, side); aux.canvas.style.removeProperty('animation'); delete document.documentElement.dataset.running; done(); }); setTimeout(() => aux.canvas.style.animation = `${duration}ms ease forwards fade-in` ); } async function animateSwapImage(newSide: Layer.Side, data: Layer.ComposedData, done: Done) { if (noAnim.matches) { await instantUpdateImage(newSide, data); done(); return; } const duration = 400; const main = getCanvasCtx('main'); const aux = getCanvasCtx('aux'); document.documentElement.dataset.running = 'swap'; Layer.redraw(aux, data, newSide); aux.canvas.addEventListener('animationend', () => { const image = aux.getImageData(0, 0, Layer.WIDTH, Layer.HEIGHT); main.putImageData(image, 0, 0); main.canvas.style.removeProperty('animation'); aux.canvas.style.removeProperty('animation'); delete document.documentElement.dataset.running; done(); }); main.canvas.style.animation = `${duration}ms ease swap1`; aux.canvas.style.animation = `${duration}ms ease swap2`; } function updateBg(cols: Color.Colors) { document.documentElement.style.setProperty('--hue', `${cols.outer.hue}`); } function updateLabel(seed: string) { const stateLabel = document.getElementById('current-name'); if (stateLabel) stateLabel.innerHTML = seed; } function updateSvgs(cols: Color.Colors) { const paletteObj = document.getElementById('palette') as HTMLObjectElement; const palette = paletteObj.contentDocument as XMLDocument | null; if (palette) { palette.documentElement.style.setProperty('--hue', `${cols.outer.hue}`); const get = (id: string) => palette.getElementById(id); for (const layer of Color.allLayers) { const col = cols[layer].css(); let elem; // main group if ((elem = get(`i-${layer}`))) { if (cols[layer].luma < 0.6) { elem.classList.add('light'); elem.classList.remove('dark'); } else { elem.classList.add('dark'); elem.classList.remove('light'); } elem.style.setProperty('--col', col); } // label if ((elem = get(`c-${layer}`))) elem.innerHTML = col; // minor swatch, if applicable if ((elem = get(`s-${layer}`))) elem.style.setProperty('--col', col); } } } function showHistory(history: History, data: Layer.Data, opts: Omit) { const list = document.getElementById('history-items'); if (!list) return; list.innerHTML = ''; const { side, firstLoad, done } = opts; for (const item of history.iterItems()) { const elem = item.asHtml(); const allOpts = { side, firstLoad, done, seed: item.name, history }; elem.addEventListener('click', () => void 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' }); const field = document.getElementById('history-close-target'); if (field) field.parentElement?.removeChild(field); document.documentElement.dataset.state = 'ready'; } function download(seed: string) { const colors = Color.colors(new Color.Rand(seed)); const blob = Palette.make(seed, colors); // there must be a better way to push out a file than // this autohotkey-ass nonsense const elem = document.createElement('a'); elem.download = `quox-${seed}.gpl`; const url = URL.createObjectURL(blob); elem.href = url; elem.click(); URL.revokeObjectURL(url); } async function setup() { message('loading layers…'); const aux = getCanvasCtx('aux'); const data = await Layer.loadData(aux) .catch(e => { message(`${e}`, 30); throw e }); const history = History.load(); let prevSeed = urlState() ?? new Color.Rand().alphaNum(); let seed = await applyState(data, { seed: prevSeed, history, firstLoad: true }); let side: Layer.Side = 'front'; const reroll = document.getElementById('reroll')!; const swap = document.getElementById('swap')!; addListeners(); function asyncHandler(h: (e: Event) => Promise): (e: Event) => void { return (e: Event) => void h(e); } // these ones don't need to be toggled document.getElementById('hideui')?.addEventListener('click', () => { document.documentElement.dataset.state = 'fullquox'; }); 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 }); }); document.getElementById('close-history')?.addEventListener('click', closeHistory); document.getElementById('current-name')?.addEventListener('focusout', asyncHandler(async e => { const space = String.raw`(\n|\s|
| )`; const re = new RegExp(`^${space}+|${space}+$`, 'msgu'); const 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 prevSeed = seed; seed = await applyState(data, { side, seed: str, history }); })); document.getElementById('download-button')?.addEventListener('click', () => { download(prevSeed); }); document.documentElement.dataset.state = 'ready'; function run(task: (k: Done) => Promise): void { removeListeners(); void task(addListeners); } function updateFromUrl() { run(async k => { const newSeed = urlState(); if (newSeed) { const opts = { history, side, seed: newSeed, done: k }; prevSeed = seed; seed = await applyState(data, opts); } }); } function runReroll() { run(async k => { prevSeed = seed; seed = await applyState(data, { side, seed, history, done: k }); }); } function runSwap() { run(async k => { side = Layer.swapSide(side); const cdata = await Layer.ensureComposed(aux, data); await animateSwapImage(side, cdata, k); }); } function addListeners() { window.addEventListener('hashchange', updateFromUrl); reroll.addEventListener('click', runReroll); swap.addEventListener('click', runSwap); } function removeListeners() { window.removeEventListener('hashchange', updateFromUrl); reroll.removeEventListener('click', runReroll); swap.removeEventListener('click', runSwap); } } document.addEventListener('DOMContentLoaded', () => void setup());