history, reduced-motion, editable name box, an easter egg
This commit is contained in:
parent
d52151e787
commit
0a59aa66f6
14 changed files with 782 additions and 127 deletions
|
@ -2,6 +2,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="65" height="65" viewBox="-5 -2.5 65 65"
|
||||
stroke="#123" stroke-width="2">
|
||||
<title>go back</title>
|
||||
<desc>back to the cube.</desc>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="g-arrow-mask">
|
||||
|
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2 KiB |
16
rainbow-quox/close.svg
Normal file
16
rainbow-quox/close.svg
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="25" height="25" viewBox="0 0 65 65">
|
||||
<title>close</title>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="cross-gradient" y1="100%" y2="0%">
|
||||
<stop offset="20%" stop-color="hsl(60 90% 95%)" />
|
||||
<stop offset="100%" stop-color="hsl(60 80% 90%)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<path fill="url(#cross-gradient)" transform='rotate(45 32.5 32.5)'
|
||||
d="M 27.5,0 v 27.5 h -27.5 v 10 h 27.5 v 27.5 h 10
|
||||
v -27.5 h 27.5 v -10 h -27.5 v -27.5 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 549 B |
43
rainbow-quox/history.svg
Normal file
43
rainbow-quox/history.svg
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="30" height="30" viewBox="0 0 100 100">
|
||||
|
||||
<defs>
|
||||
<linearGradient id="arrow-color-fade" y1="100%" y2="0%">
|
||||
<stop offset="20%" stop-color="hsl(60 90% 95%)" />
|
||||
<stop offset="100%" stop-color="hsl(60 80% 90%)" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="arrow-alpha-fade" y1="100%" y2="0%">
|
||||
<stop offset="25%" stop-color="white" />
|
||||
<stop offset="75%" stop-color="black" />
|
||||
</linearGradient>
|
||||
|
||||
<path id="arrow-path"
|
||||
d="M 50,10 a 40,40 0 1 0 28.28,68.28
|
||||
v15 h10 v-32.07 h-32.07 v10 h15
|
||||
a 30,30 0 1 1 -21.21,-51.21 z" />
|
||||
|
||||
<mask id="arrow-mask">
|
||||
<rect width="100" height="100" fill="black" />
|
||||
<use href="#arrow-path" fill="white" />
|
||||
</mask>
|
||||
|
||||
<mask id="fade-mask">
|
||||
<rect width="100" height="100" fill="white" />
|
||||
<rect width="55" height="55" fill="url(#arrow-alpha-fade)" />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<g id="arrow" mask="url(#fade-mask)">
|
||||
<use href="#arrow-path" fill="none" />
|
||||
|
||||
<g id="arrow-fill" mask="url(#arrow-mask)">
|
||||
<rect width="100" height="100" fill="hsl(60 90% 95%)" />
|
||||
<rect x="40" y="40" width="60" height="60" fill="url(#arrow-color-fade)" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<path id="hands" fill="url(#arrow-color-fade)" stroke="#123"
|
||||
d="M 46,28 v 28 h 28 v -8 h -20 v -20 h -8 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,5 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang=en data-state=loading>
|
||||
<meta charset=utf-8>
|
||||
|
||||
<meta name=viewport content='width=device-width, initial-scale=0.5'>
|
||||
|
||||
|
@ -15,14 +16,31 @@
|
|||
</div>
|
||||
|
||||
<div id=showui-holder>
|
||||
<img id=showui src=showui.svg alt='show ui' title='show ui'>
|
||||
<button id=showui>
|
||||
<img src=showui.svg alt='show ui' title='show ui'>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav id=back>
|
||||
<a href=/><img src=back.svg alt=back title=back></a>
|
||||
<a href=/><img src=back.svg alt=back title='back to the cube.'></a>
|
||||
</nav>
|
||||
|
||||
<div id=state-message class=empty> quox #<span id=state>0</span> </div>
|
||||
<div id=history>
|
||||
<button id=close-history>
|
||||
<img src=close.svg alt=close title=close>
|
||||
</button>
|
||||
|
||||
<div id=current>
|
||||
<span id=current-hello>hello my name is</span>
|
||||
<span id=current-name contenteditable>anonymous</span>
|
||||
<button id=history-button>
|
||||
<img src=history.svg alt=history title=history>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id=history-items>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id=pic-holder>
|
||||
<canvas id=main width=1000 height=673>
|
||||
|
|
|
@ -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<A>(f: (l: Layer) => A): Record<Layer, A> {
|
|||
return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record<Layer, A>;
|
||||
}
|
||||
|
||||
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<BaseCol, Oklch> & Partial<Record<OptionalBaseCol, Oklch>>;
|
||||
|
||||
|
||||
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<Layer, Rgb>;
|
||||
|
@ -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<string, KnownPalette> = {
|
||||
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),
|
||||
},
|
||||
};
|
||||
|
|
128
rainbow-quox/script/history.ts
Normal file
128
rainbow-quox/script/history.ts
Normal file
|
@ -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 = `
|
||||
<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);
|
||||
}
|
||||
}
|
|
@ -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<ImageBitmap> {
|
||||
|
@ -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<ApplyStateOpts>): Promise<State> {
|
||||
let { side, state, firstLoad, buf, done } = opts;
|
||||
applyState(data: LayerData, opts: ApplyStateOpts): Promise<string> {
|
||||
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<ApplyStateOpts, 'seed' | 'history'>) {
|
||||
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|<br>| )`;
|
||||
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() {
|
||||
|
|
|
@ -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<A>(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<A>(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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="65" height="55" viewBox="-2.5 -2.5 65 55"
|
||||
width="40" height="34" viewBox="-2.5 -2.5 65 55"
|
||||
stroke="#123" stroke-width="2">
|
||||
|
||||
<linearGradient id="g-bg" x2="0" y2="100%">
|
||||
|
|
Before Width: | Height: | Size: 876 B After Width: | Height: | Size: 876 B |
BIN
rainbow-quox/style/3px-tile.png
(Stored with Git LFS)
Normal file
BIN
rainbow-quox/style/3px-tile.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
rainbow-quox/style/bright-squares.png
(Stored with Git LFS)
BIN
rainbow-quox/style/bright-squares.png
(Stored with Git LFS)
Binary file not shown.
BIN
rainbow-quox/style/groovepaper.dark.png
(Stored with Git LFS)
Normal file
BIN
rainbow-quox/style/groovepaper.dark.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
rainbow-quox/style/groovepaper.png
(Stored with Git LFS)
Normal file
BIN
rainbow-quox/style/groovepaper.png
(Stored with Git LFS)
Normal file
Binary file not shown.
|
@ -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 <button>
|
||||
color: oklch(0.95 0.075 var(--c-hue));
|
||||
background: oklch(0.5 0.2 var(--hue));
|
||||
border: 3px solid oklch(0.2 0.05 var(--hue));
|
||||
// respecify font family for <button>
|
||||
font: 700 (20pt * $font-scale) var(--font);
|
||||
padding: 0.2rem 0.5rem;
|
||||
@if $button {
|
||||
background: $box-texture, $button-bg;
|
||||
color: $button-fg;
|
||||
} @else {
|
||||
background: $box-texture, $box-bg;
|
||||
color: $box-fg;
|
||||
}
|
||||
background-blend-mode: hard-light;
|
||||
border: 3px solid oklch(0.2 0.05 var(--hue));
|
||||
}
|
||||
|
||||
@mixin image-button {
|
||||
@include box($button: true);
|
||||
padding: 5px;
|
||||
border: 2px solid $button-fg;
|
||||
> * { display: block; }
|
||||
}
|
||||
|
||||
@mixin button-arrow {
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1.25ex; aspect-ratio: 2/3;
|
||||
background:
|
||||
conic-gradient(from -124deg at 100% 50%,
|
||||
currentcolor, currentcolor 68deg, transparent 68deg);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base, layering, come-in;
|
||||
|
||||
|
@ -52,19 +86,20 @@
|
|||
--font: Muller, sans-serif;
|
||||
font-family: var(--font);
|
||||
|
||||
@function bg($lcs) {
|
||||
@function bg($lcs, $img) {
|
||||
$lg: to bottom in oklch;
|
||||
@each $l, $c in $lcs { $lg: $lg, oklch($l $c var(--hue)); }
|
||||
@return url(bright-squares.png), linear-gradient($lg);
|
||||
@return url(#{$img}), linear-gradient($lg);
|
||||
}
|
||||
|
||||
background: bg([0.9 0.08, 0.7 0.1]);
|
||||
background: bg([0.9 0.08, 0.7 0.1], 'groovepaper.png');
|
||||
background-blend-mode: screen;
|
||||
|
||||
color-scheme: light dark;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background: bg([0.3 0.08, 0.2 0.09]);
|
||||
background: bg([0.3 0.08, 0.2 0.09], 'groovepaper.dark.png');
|
||||
background-blend-mode: multiply;
|
||||
}
|
||||
background-blend-mode: screen;
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -78,6 +113,8 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
button { cursor: pointer; }
|
||||
|
||||
#pic-holder, #main, #aux {
|
||||
width: 1000px;
|
||||
// margin: auto;
|
||||
|
@ -101,34 +138,133 @@
|
|||
#hideui { position: absolute; bottom: 0; left: 5em; }
|
||||
|
||||
#buttons button {
|
||||
@include box;
|
||||
width: calc(6em + 100px);
|
||||
@include box($button: true);
|
||||
@include button-arrow;
|
||||
&::before { margin-right: 0.5ex; }
|
||||
width: calc(6em + 1.5ex + 100px);
|
||||
text-align: left;
|
||||
|
||||
$rotate: rotate(-60deg);
|
||||
transform: $rotate;
|
||||
transform-origin: center right;
|
||||
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
:root:not([data-running]) &:active,
|
||||
[data-running=swap] &#swap,
|
||||
[data-running=reroll] &#reroll {
|
||||
transform: $rotate translateX(-1.5em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#back, #showui { position: fixed; top: 22px; left: 22px; }
|
||||
#showui { @include image-button; }
|
||||
|
||||
#back, #showui {
|
||||
@include shadow;
|
||||
position: fixed; top: 22px; left: 22px;
|
||||
}
|
||||
#showui { transform: translateX(-200%); }
|
||||
|
||||
#palette-holder {
|
||||
margin: -60px auto 0;
|
||||
padding-top: 25px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
#palette, #showui { @include shadow; }
|
||||
#palette { @include shadow; }
|
||||
|
||||
#state-message {
|
||||
@include box;
|
||||
position: fixed; top: 1em; right: -1em;
|
||||
padding-right: 2em;
|
||||
|
||||
@mixin history-flex {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: end;
|
||||
gap: 3px;
|
||||
}
|
||||
@mixin history-box($font-scale: 1, $button: false) {
|
||||
@include box($font-scale, $button);
|
||||
border-right: none;
|
||||
width: 100%;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
#history {
|
||||
$width: 24em;
|
||||
width: $width;
|
||||
position: fixed; top: 3rem; right: -1rem;
|
||||
@include history-flex;
|
||||
}
|
||||
#current {
|
||||
@include history-box;
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
display: grid;
|
||||
grid-template: "a c" "b c" / auto min-content;
|
||||
gap: 0 15px;
|
||||
align-items: start;
|
||||
}
|
||||
#current-hello {
|
||||
grid-area: a;
|
||||
font-size: 75%;
|
||||
}
|
||||
#current-name {
|
||||
grid-area: b;
|
||||
max-width: 16em;
|
||||
word-break: break-word;
|
||||
&::spelling-error { text-decoration: none; }
|
||||
}
|
||||
#history-button {
|
||||
@include image-button;
|
||||
grid-area: c;
|
||||
}
|
||||
:is(#current-name, .history-item) img { max-width: 50%; }
|
||||
|
||||
#close-history {
|
||||
@include box(0.8, $button: true);
|
||||
margin: 0; height: 1lh; width: 1lh;
|
||||
position: absolute; top: 0; left: calc(-1lh);
|
||||
img {
|
||||
position: absolute;
|
||||
inset: 0; width: 100%; height: 100%;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
#history-items {
|
||||
$bottom-fade-size: 3lh;
|
||||
$top-fade-size: math.div($bottom-fade-size, 2);
|
||||
@include history-flex;
|
||||
position: absolute;
|
||||
top: -$top-fade-size; left: 0;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
height: 65vh;
|
||||
overflow: hidden auto;
|
||||
padding: $top-fade-size 0 $bottom-fade-size;
|
||||
mask: linear-gradient(to bottom,
|
||||
transparent, black $top-fade-size,
|
||||
black calc(100% - $bottom-fade-size), transparent);
|
||||
}
|
||||
.history-item {
|
||||
@include history-box(0.8, $button: true);
|
||||
@include button-arrow;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
.history-colors {
|
||||
height: 0.8em;
|
||||
flex: none;
|
||||
}
|
||||
.history-name {
|
||||
width: 100%;
|
||||
padding-right: 2rem;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.history-item, #close-history { transform: translateX(25em); }
|
||||
#history-close-target {
|
||||
position: absolute;
|
||||
inset: 0 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,22 +272,54 @@
|
|||
@layer layering {
|
||||
#main { z-index: 0; }
|
||||
#aux { z-index: 1; }
|
||||
|
||||
#buttons, #state-message, #palette-holder, #back, #showui { z-index: 2; }
|
||||
#buttons, #palette-holder, #back, #showui { z-index: 2; }
|
||||
#history-close-target { z-index: 3; }
|
||||
#history { z-index: 4; }
|
||||
}
|
||||
|
||||
|
||||
@layer come-in {
|
||||
#buttons button, #state-message, #palette, #back, #showui {
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
#buttons button, #current, #palette, #back, #showui, .history-item {
|
||||
@include transy;
|
||||
}
|
||||
|
||||
#current { @include transy(all); }
|
||||
#close-history { @include transy; }
|
||||
}
|
||||
|
||||
[data-state=fullquox], [data-state=loading] {
|
||||
#buttons button { transform: none; }
|
||||
#state-message { transform: translateX(100%); }
|
||||
#current { transform: translateX(100%); }
|
||||
#palette { transform: translateY(125%); }
|
||||
#back { transform: translateX(-200%); }
|
||||
}
|
||||
|
||||
[data-state=fullquox] { #showui { transform: none; } }
|
||||
[data-state=fullquox] {
|
||||
#showui { transform: none; }
|
||||
}
|
||||
|
||||
[data-state=history] {
|
||||
#history-items { pointer-events: auto; }
|
||||
#close-history { pointer-events: auto; transform: none; }
|
||||
|
||||
#current { opacity: 0; }
|
||||
|
||||
@for $i from 0 through 6 {
|
||||
.history-item:nth-child(#{$i}) {
|
||||
transform: translateX(3% * $i); }
|
||||
}
|
||||
.history-item:nth-child(n+7) { transform: translateX(3% * 7); }
|
||||
}
|
||||
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
@for $i from 2 through 6 {
|
||||
.history-item:nth-child(#{$i}) {
|
||||
transition-delay: ($i - 1) * 40ms;
|
||||
}
|
||||
}
|
||||
.history-item:nth-child(n+7) {
|
||||
transition-delay: 240ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue