Compare commits

..

23 commits

Author SHA1 Message Date
0301744fde border radius 2025-02-16 22:41:06 +01:00
5e83230768 randomise cube, and separate out setup function 2025-02-16 22:41:06 +01:00
717bae78f5 tweak links face 2025-02-16 22:41:06 +01:00
2b95165aff add cooper button 2025-02-16 22:41:06 +01:00
03b571205f fix abyss's and FaeAlchemist's buttons 2025-02-16 22:41:06 +01:00
38f3ee0a64 quick linx on front face 2025-02-16 22:41:06 +01:00
da410a74f2 let known quox colour schemes specify any colours 2025-02-16 22:41:06 +01:00
355ea44003 quat button 2025-02-16 22:41:06 +01:00
3ff1fb1aae refactor cube styles using @layer 2025-02-16 22:41:06 +01:00
732e5a3d47 fix breathing animation 2025-02-16 16:23:46 +01:00
186b02a132 fix cube in webkit 2025-02-16 16:23:29 +01:00
2514562a86 simplify markup on id face 2025-02-16 00:25:15 +01:00
65db9cdd5a remove d&d stuff from site till i work on it more 2025-02-16 00:14:25 +01:00
f6a4270d13 remove the noscript message. who cares 2025-02-16 00:13:20 +01:00
6a48d013b2 make yellow cube face more yellow 2025-02-16 00:10:50 +01:00
994dd3eed3 cc button 2025-02-16 00:09:08 +01:00
1ef8c3ef91 minor tweaks to quox color stuff 2025-02-16 00:08:42 +01:00
0d1a749c3d add clocktower button 2025-02-15 23:55:11 +01:00
8dbb45953e fix faealchemist button 2025-02-15 23:54:58 +01:00
9bd5edeeb3 remove svg, eot, woff(1) versions of muller. who needs those 2025-02-15 23:54:25 +01:00
15e6c50641 let → const 2025-01-01 00:41:33 +01:00
a4f3877b78 fix "my name is" font size and download z index 2025-01-01 00:41:33 +01:00
ddc36a9eea refactor rainbow-quox scripting 2025-01-01 00:41:33 +01:00
27 changed files with 1587 additions and 1446 deletions

View file

@ -1,17 +1,20 @@
PAGES = index.html pubkey.txt rainbow-quox/index.html \ PAGES = index.html pubkey.txt rainbow-quox/index.html \
dnd/index.html $(wildcard dnd/*/index.html) # dnd/index.html $(wildcard dnd/*/index.html)
MEDIA = \ MEDIA = \
$(wildcard media/*.png) $(wildcard media/*.gif) $(wildcard media/*.webp) \ $(wildcard media/*.png) $(wildcard media/*.gif) $(wildcard media/*.webp) \
$(wildcard media/*.svg) \
$(wildcard media/flags/*) $(wildcard media/buttons/*) \ $(wildcard media/flags/*) $(wildcard media/buttons/*) \
$(wildcard media/icons/*) $(wildcard media/bg/*) 8831.png 8831-quox.png \ $(wildcard media/icons/*) $(wildcard media/bg/*) 8831.png 8831-quox.png \
$(wildcard rainbow-quox/front/*) $(wildcard rainbow-quox/back/*) \ $(wildcard rainbow-quox/front/*) $(wildcard rainbow-quox/back/*) \
$(wildcard rainbow-quox/*.svg) rainbow-quox/palette.svg \ $(wildcard rainbow-quox/*.svg) rainbow-quox/palette.svg \
$(wildcard dnd/*.png) $(wildcard dnd/*.webp) $(wildcard dnd/*/*.webp) # $(wildcard dnd/*.png) $(wildcard dnd/*.webp) $(wildcard dnd/*/*.webp)
CSS = $(shell find fonts -type f) \ CSS = $(shell find fonts -type f) \
$(patsubst %.scss,%.css, \ $(patsubst %.scss,%.css, \
$(wildcard rainbow-quox/style/*) $(wildcard style/*)) \ $(wildcard rainbow-quox/style/*) $(wildcard style/*)) \
dnd/base.css dnd/bio.css dnd/index.css $(wildcard dnd/*/style.css) # dnd/base.css dnd/bio.css dnd/index.css $(wildcard dnd/*/style.css)
SCRIPTS = $(patsubst %.ts,%.js,$(wildcard script/*.ts rainbow-quox/script/*.ts)) SCRIPTS = $(patsubst %.ts,%.js, \
$(wildcard script/*.ts rainbow-quox/script/color/*.ts \
rainbow-quox/script/*.ts))
MISC = $(shell find .well-known -type f) MISC = $(shell find .well-known -type f)
ALL = $(CSS) $(PAGES) $(MEDIA) $(SCRIPTS) $(MISC) ALL = $(CSS) $(PAGES) $(MEDIA) $(SCRIPTS) $(MISC)

View file

@ -2,11 +2,8 @@
font-family: Muller; font-family: Muller;
font-weight: 50; font-weight: 50;
src: src:
url(050.eot) format('embedded-opentype'), url(050.woff2) format('woff2'),
url(050.svg) format('svg'), url(050.ttf) format('truetype');
url(050.ttf) format('truetype'),
url(050.woff) format('woff'),
url(050.woff2) format('woff2');
} }
@font-face { @font-face {
@ -14,22 +11,16 @@
font-weight: 50; font-weight: 50;
font-style: italic; font-style: italic;
src: src:
url(050i.eot) format('embedded-opentype'), url(050i.woff2) format('woff2'),
url(050i.svg) format('svg'), url(050i.ttf) format('truetype');
url(050i.ttf) format('truetype'),
url(050i.woff) format('woff'),
url(050i.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: Muller; font-family: Muller;
font-weight: 100; font-weight: 100;
src: src:
url(100.eot) format('embedded-opentype'), url(100.woff2) format('woff2'),
url(100.svg) format('svg'), url(100.ttf) format('truetype');
url(100.ttf) format('truetype'),
url(100.woff) format('woff'),
url(100.woff2) format('woff2');
} }
@font-face { @font-face {
@ -37,22 +28,16 @@
font-weight: 100; font-weight: 100;
font-style: italic; font-style: italic;
src: src:
url(100i.eot) format('embedded-opentype'), url(100i.woff2) format('woff2'),
url(100i.svg) format('svg'), url(100i.ttf) format('truetype');
url(100i.ttf) format('truetype'),
url(100i.woff) format('woff'),
url(100i.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: Muller; font-family: Muller;
font-weight: 200; font-weight: 200;
src: src:
url(200.eot) format('embedded-opentype'), url(200.woff2) format('woff2'),
url(200.svg) format('svg'), url(200.ttf) format('truetype');
url(200.ttf) format('truetype'),
url(200.woff) format('woff'),
url(200.woff2) format('woff2');
} }
@font-face { @font-face {
@ -60,22 +45,16 @@
font-weight: 200; font-weight: 200;
font-style: italic; font-style: italic;
src: src:
url(200i.eot) format('embedded-opentype'), url(200i.woff2) format('woff2'),
url(200i.svg) format('svg'), url(200i.ttf) format('truetype');
url(200i.ttf) format('truetype'),
url(200i.woff) format('woff'),
url(200i.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: Muller; font-family: Muller;
font-weight: 300; font-weight: 300;
src: src:
url(300.eot) format('embedded-opentype'), url(300.woff2) format('woff2'),
url(300.svg) format('svg'), url(300.ttf) format('truetype');
url(300.ttf) format('truetype'),
url(300.woff) format('woff'),
url(300.woff2) format('woff2');
} }
@font-face { @font-face {
@ -83,22 +62,16 @@
font-weight: 300; font-weight: 300;
font-style: italic; font-style: italic;
src: src:
url(300i.eot) format('embedded-opentype'), url(300i.woff2) format('woff2'),
url(300i.svg) format('svg'), url(300i.ttf) format('truetype');
url(300i.ttf) format('truetype'),
url(300i.woff) format('woff'),
url(300i.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: Muller; font-family: Muller;
font-weight: 400; font-weight: 400;
src: src:
url(400.eot) format('embedded-opentype'), url(400.woff2) format('woff2'),
url(400.svg) format('svg'), url(400.ttf) format('truetype');
url(400.ttf) format('truetype'),
url(400.woff) format('woff'),
url(400.woff2) format('woff2');
} }
@font-face { @font-face {
@ -106,22 +79,16 @@
font-weight: 400; font-weight: 400;
font-style: italic; font-style: italic;
src: src:
url(400i.eot) format('embedded-opentype'), url(400i.woff2) format('woff2'),
url(400i.svg) format('svg'), url(400i.ttf) format('truetype');
url(400i.ttf) format('truetype'),
url(400i.woff) format('woff'),
url(400i.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: Muller; font-family: Muller;
font-weight: 500; font-weight: 500;
src: src:
url(500.eot) format('embedded-opentype'), url(500.woff2) format('woff2'),
url(500.svg) format('svg'), url(500.ttf) format('truetype');
url(500.ttf) format('truetype'),
url(500.woff) format('woff'),
url(500.woff2) format('woff2');
} }
@font-face { @font-face {
@ -129,22 +96,16 @@
font-weight: 500; font-weight: 500;
font-style: italic; font-style: italic;
src: src:
url(500i.eot) format('embedded-opentype'), url(500i.woff2) format('woff2'),
url(500i.svg) format('svg'), url(500i.ttf) format('truetype');
url(500i.ttf) format('truetype'),
url(500i.woff) format('woff'),
url(500i.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: Muller; font-family: Muller;
font-weight: 600; font-weight: 600;
src: src:
url(600.eot) format('embedded-opentype'), url(600.woff2) format('woff2'),
url(600.svg) format('svg'), url(600.ttf) format('truetype');
url(600.ttf) format('truetype'),
url(600.woff) format('woff'),
url(600.woff2) format('woff2');
} }
@font-face { @font-face {
@ -152,22 +113,16 @@
font-weight: 600; font-weight: 600;
font-style: italic; font-style: italic;
src: src:
url(600i.eot) format('embedded-opentype'), url(600i.woff2) format('woff2'),
url(600i.svg) format('svg'), url(600i.ttf) format('truetype');
url(600i.ttf) format('truetype'),
url(600i.woff) format('woff'),
url(600i.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: Muller; font-family: Muller;
font-weight: 700; font-weight: 700;
src: src:
url(700.eot) format('embedded-opentype'), url(700.woff2) format('woff2'),
url(700.svg) format('svg'), url(700.ttf) format('truetype');
url(700.ttf) format('truetype'),
url(700.woff) format('woff'),
url(700.woff2) format('woff2');
} }
@font-face { @font-face {
@ -175,22 +130,16 @@
font-weight: 700; font-weight: 700;
font-style: italic; font-style: italic;
src: src:
url(700i.eot) format('embedded-opentype'), url(700i.woff2) format('woff2'),
url(700i.svg) format('svg'), url(700i.ttf) format('truetype');
url(700i.ttf) format('truetype'),
url(700i.woff) format('woff'),
url(700i.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: Muller; font-family: Muller;
font-weight: 800; font-weight: 800;
src: src:
url(800.eot) format('embedded-opentype'), url(800.woff2) format('woff2'),
url(800.svg) format('svg'), url(800.ttf) format('truetype');
url(800.ttf) format('truetype'),
url(800.woff) format('woff'),
url(800.woff2) format('woff2');
} }
@font-face { @font-face {
@ -198,22 +147,16 @@
font-weight: 800; font-weight: 800;
font-style: italic; font-style: italic;
src: src:
url(800i.eot) format('embedded-opentype'), url(800i.woff2) format('woff2'),
url(800i.svg) format('svg'), url(800i.ttf) format('truetype');
url(800i.ttf) format('truetype'),
url(800i.woff) format('woff'),
url(800i.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: Muller; font-family: Muller;
font-weight: 900; font-weight: 900;
src: src:
url(900.eot) format('embedded-opentype'), url(900.woff2) format('woff2'),
url(900.svg) format('svg'), url(900.ttf) format('truetype');
url(900.ttf) format('truetype'),
url(900.woff) format('woff'),
url(900.woff2) format('woff2');
} }
@font-face { @font-face {
@ -221,9 +164,6 @@
font-weight: 900; font-weight: 900;
font-style: italic; font-style: italic;
src: src:
url(900i.eot) format('embedded-opentype'), url(900i.woff2) format('woff2'),
url(900i.svg) format('svg'), url(900i.ttf) format('truetype');
url(900i.ttf) format('truetype'),
url(900i.woff) format('woff'),
url(900i.woff2) format('woff2');
} }

View file

@ -29,8 +29,7 @@
<meta name=twitter:card content=summary_large_image> <meta name=twitter:card content=summary_large_image>
<meta name=twitter:image content=media/twittercard.webp> <meta name=twitter:image content=media/twittercard.webp>
<script src=script/shuffle.js type=module></script> <script src=script/cube_setup.js type=module></script>
<script src=script/run.js type=module></script>
<nav id=face-menu> <nav id=face-menu>
<menu class='menu hide-boxes'> <menu class='menu hide-boxes'>
@ -56,43 +55,36 @@
<p> im niss. whats up </p> <p> im niss. whats up </p>
<noscript> <h3> quick linx </h3>
<p> this page needs javascript for the full effect, sorry. </p>
<p> it was hard enough to make it work cross-browser as it is. <ul>
<small>[grumbles at safari]</small> </p> <li> <a href=https://gallery.niss.website>art gallery</a>
</noscript> <li> <a href=./rainbow-quox>quox palette generator</a>
<li> <a href=https://git.rhiannon.website>code hole</a>
<li> <a href=https://blog.niss.website>rarely-updated blog</a>
</ul>
</section> </section>
<section id=id> <section id=id>
<h2> so whats your deal </h2> <h2> so whats your deal </h2>
<dl> <dl class=bullety>
<dt>name <dt> name
<dd> <dd> <b>niss</b> or <b>q.t.</b>
<ul> <dd> always lowercase
<li> <b>niss</b> or <b>q.t.</b>
<li> always lowercase
</ul>
<dt>pronouns <dt> pronouns
<dd> <dd> <b>she</b>, <b>they</b>, or <b>it</b> in english. pick whichever
<ul> <dd> <b>sie</b> auf deutsch
<li> <b>she</b>, <b>they</b>, or <b>it</b> in english. pick whichever
<li> <b>sie</b> auf deutsch
</ul>
<dt>shapes <dt> shapes
<dd> <dd> <a href=https://gallery.niss.website/by-any/#gecs>gold dust day gecko</a>
<ul>
<li><a href=https://gallery.niss.website/by-any/#gecs>gold dust day gecko</a>
×2, from “real life” ×2, from “real life”
<li><a href=https://gallery.niss.website/by-any/#qt>quox</a>, <dd> <a href=https://gallery.niss.website/by-any/#qt>quox</a>,
from the tower of druaga from the tower of druaga
<li><a href=https://gallery.niss.website/by-any/#kesi>bubble dragon</a>, <dd> <a href=https://gallery.niss.website/by-any/#kesi>bubble dragon</a>,
from bubble bobble from bubble bobble
<li>some others that come and go <dd> some others that come and go
</ul>
</dl> </dl>
<div id=flags> <div id=flags>
@ -128,40 +120,21 @@
<section id=links> <section id=links>
<h2> where else do you exist </h2> <h2> where else do you exist </h2>
<section id=gecsites> <ul class='zoom boxy'>
<h3> gec sites </h3>
<ul class=zoom>
<li id=gallery> <a href=https://gallery.niss.website>gallery</a> <li id=gallery> <a href=https://gallery.niss.website>gallery</a>
<li id=code> <a href=https://git.rhiannon.website>code</a> <li id=code> <a href=https://git.rhiannon.website>code</a>
<li id=blog> <a href=https://blog.niss.website>blog</a> <li id=blog> <a href=https://blog.niss.website>blog</a>
</ul> <li id=chitter> <a href=https://chitter.xyz/@niss>fediverse</a>
</section> <li id=bluesky> <a href=https://niss.yummy.cricket>bluesky</a>
<!-- <li id=artfight> <a href=https://artfight.net/~nissss>art fight</a> -->
<section id=galleries>
<h3> gallery sites </h3>
<ul class=zoom>
<li id=itaku> <a href=https://itaku.ee/profile/niss>itaku</a> <li id=itaku> <a href=https://itaku.ee/profile/niss>itaku</a>
<li id=weasyl> <a href=https://www.weasyl.com/~niss>weasyl</a> <li id=weasyl> <a href=https://www.weasyl.com/~niss>weasyl</a>
<li id=fa> <a href=https://furaffinity.net/user/niss>furaffinity</a> <li id=fa> <a href=https://furaffinity.net/user/niss>furaffinity</a>
<li id=da> <a href=https://www.deviantart.com/2gecs>deviantart</a> <li id=da> <a href=https://www.deviantart.com/2gecs>deviantart</a>
<li id=kofi> <a href=https://ko-fi.com/nissss>ko-fi</a> <li id=kofi> <a href=https://ko-fi.com/nissss>ko-fi</a>
<!-- <li id=artfight> <a href=https://artfight.net/~nissss>art fight</a> -->
</ul> </ul>
</section> </section>
<section id=creaturefeature>
<h3> other stuff </h3>
<ul class=zoom>
<li id=chitter> <a href=https://chitter.xyz/@niss>fediverse</a>
<!-- <li id=cohost> <a href=https://cohost.org/niss>cohost</a> -->
<li id=bluesky> <a href=https://niss.yummy.cricket>bluesky</a>
</ul>
</section>
</section>
<section id=friends> <section id=friends>
<h2> more websites please </h2> <h2> more websites please </h2>
@ -171,7 +144,7 @@
<!-- btw unless i say otherwise, all these buttons are made by <!-- btw unless i say otherwise, all these buttons are made by
who they link to --> who they link to -->
<ul class='zoom shuffle'> <ul class='zoom boxy shuffle'>
<li id=dino> <li id=dino>
<a href=https://flussence.eu title=dino> <a href=https://flussence.eu title=dino>
<img src=media/buttons/dino.png alt=dino> <img src=media/buttons/dino.png alt=dino>
@ -220,7 +193,7 @@
<li id=abyss> <li id=abyss>
<a href=https://kobold60.com title=abyss> <a href=https://kobold60.com title=abyss>
<picture> <picture>
<source srcset=media/buttons/abyss_still.png <source srcset=media/buttons/abyss_still.gif
media='(prefers-reduced-motion: reduce)'> media='(prefers-reduced-motion: reduce)'>
<img src=media/buttons/abyss.gif alt=abyss> <img src=media/buttons/abyss.gif alt=abyss>
</picture> </picture>
@ -343,9 +316,9 @@
<li id=FaeAlchemist> <li id=FaeAlchemist>
<a href=https://faealchemist.neocities.org title=faealchemist> <a href=https://faealchemist.neocities.org title=faealchemist>
<picture> <picture>
<source srcset=media/buttons/FaeAlchemist_still.png <source srcset=media/buttons/FaeAlchemist_still.webp
media='(prefers-reduced-motion: reduce)'> media='(prefers-reduced-motion: reduce)'>
<img src=media/buttons/FaeAlchemist.png alt=faealchemist> <img src=media/buttons/FaeAlchemist.gif alt=faealchemist>
</picture> </picture>
</a> </a>
@ -441,13 +414,34 @@
<img src=media/buttons/deneb.gif alt=deneb> <img src=media/buttons/deneb.gif alt=deneb>
</picture> </picture>
</a> </a>
<li id=clocktower>
<a href=https://cyanic-clocktower.neocities.org
title='the clocktower headspace'>
<img src=media/buttons/clocktower.png
alt='the clocktower headspace'>
</a>
<li id=quat>
<a href=https://highlysuspect.agency title='highly suspect agency'>
<img src=media/buttons/hsa.gif alt='highly suspect agency'>
</a>
<li id=cooper>
<a href=https://ottr.uk title=otterspace>
<picture>
<source srcset=media/buttons/cooper_still.png
media='(prefers-reduced-motion: reduce)'>
<img src=media/buttons/cooper.gif alt=otterspace>
</picture>
</a>
</ul> </ul>
</section> </section>
<section id=otherlinks> <section id=otherlinks>
<h3> other cool things </h3> <h3> other cool things </h3>
<ul class='zoom shuffle'> <ul class='zoom boxy shuffle'>
<li> <li>
<a href=https://cohost.org id=cohost-button title='i was on cohost'> <a href=https://cohost.org id=cohost-button title='i was on cohost'>
<picture> <picture>

BIN
media/buttons/FaeAlchemist_still.gif (Stored with Git LFS)

Binary file not shown.

BIN
media/buttons/FaeAlchemist_still.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
media/buttons/clocktower.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
media/buttons/cooper.gif (Stored with Git LFS) Normal file

Binary file not shown.

BIN
media/buttons/cooper_still.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
media/buttons/hsa.gif (Stored with Git LFS) Normal file

Binary file not shown.

32
media/point_right.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="16" height="16" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<metadata>
<rdf:RDF xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc = "http://purl.org/dc/elements/1.1/"
>
<rdf:Description rdf:about="">
<dc:title>Mutant Standard emoji 2024.06</dc:title>
</rdf:Description>
<cc:work rdf:about="">
<cc:license rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/"/>
<cc:attributionName>Caius Nocturne</cc:attributionName>
<cc:attributionURL>http://mutant.tech/</cc:attributionURL>
</cc:work>
</rdf:RDF>
</metadata>
<path id="point_right--paw-" serif:id="point_right [paw]" d="M32,0l0,32l-32,0l0,-32l32,0Z" style="fill:none;"/>
<g id="Layer10">
<path d="M18.632,8l8.868,0c2.484,0 4.5,2.016 4.5,4.5c0,2.484 -2.016,4.5 -4.5,4.5l-3.112,0c0.073,0.321 0.112,0.656 0.112,1c0,1.624 -0.862,3.047 -2.152,3.839c0.191,0.714 0.209,1.485 0.018,2.252c-0.384,1.536 -1.516,2.688 -2.91,3.167c-0.022,0.153 -0.052,0.306 -0.09,0.458c-0.603,2.41 -3.048,3.877 -5.457,3.275c0,0 -2.276,-0.569 -4.709,-1.178c-5.407,-1.351 -9.2,-6.211 -9.197,-11.785l0.001,-0.408c-0.003,-0.04 -0.004,-0.08 -0.004,-0.12l0,-1.238c0,-5.596 3.581,-10.564 8.889,-12.333c3.743,-1.248 7.479,-2.493 7.479,-2.493c0.609,-0.203 1.28,-0.101 1.801,0.275c0.522,0.376 0.831,0.979 0.831,1.622l0,2.338l-0.005,0.28c-0.037,0.709 -0.16,1.396 -0.363,2.049Z"/>
</g>
<g id="Layer9">
<path d="M22,14l0,5l-1,1l-1,3l-1,2l-2,2l-5,0l0,-13l10,0Z" style="fill:#005766;"/>
<path d="M27.5,10c0.349,0.001 0.696,0.073 1.015,0.215c0.298,0.132 0.569,0.324 0.795,0.56c0.215,0.226 0.387,0.491 0.505,0.78c0.108,0.264 0.17,0.547 0.183,0.832c0.012,0.274 -0.021,0.549 -0.097,0.812c-0.068,0.234 -0.17,0.457 -0.303,0.661c-0.293,0.452 -0.732,0.803 -1.238,0.988c-0.276,0.101 -0.567,0.151 -0.86,0.152l-10,0c-0.067,0.001 -0.133,0.013 -0.195,0.039c-0.191,0.081 -0.315,0.279 -0.304,0.487c0.003,0.058 0.016,0.115 0.038,0.169c0.024,0.055 0.058,0.106 0.099,0.15c0.044,0.046 0.098,0.084 0.156,0.111c0.065,0.029 0.135,0.043 0.206,0.044l3.999,0c0.608,0.456 1.001,1.183 1.001,2c0,1.252 -0.923,2.291 -2.125,2.472l-4.265,-0.96c-0.07,-0.015 -0.141,-0.016 -0.211,-0.002c-0.063,0.013 -0.123,0.038 -0.176,0.074c-0.051,0.034 -0.095,0.076 -0.13,0.125c-0.034,0.047 -0.059,0.1 -0.075,0.156c-0.055,0.201 0.022,0.42 0.191,0.542c0.055,0.039 0.116,0.065 0.181,0.081l4.025,0.905c0.496,0.593 0.712,1.407 0.51,2.213c-0.278,1.116 -1.268,1.868 -2.365,1.894l-3.939,-0.985c-0.065,-0.015 -0.132,-0.02 -0.198,-0.009c-0.206,0.032 -0.373,0.194 -0.414,0.398c-0.011,0.057 -0.012,0.116 -0.003,0.173c0.009,0.06 0.029,0.117 0.059,0.17c0.032,0.056 0.075,0.105 0.125,0.145c0.056,0.045 0.12,0.075 0.189,0.093l3.606,0.902c0.027,0.276 0.007,0.561 -0.064,0.846c-0.335,1.338 -1.693,2.153 -3.032,1.818c0,0 -2.275,-0.569 -4.706,-1.178c-4.513,-1.129 -7.679,-5.185 -7.679,-9.838l0,-0.531l-0.004,-0.004l0,-1.238c0,-4.735 3.03,-8.939 7.521,-10.436c3.743,-1.247 7.479,-2.493 7.479,-2.493l0,2.338c0,0.814 -0.165,1.604 -0.47,2.329c-0.577,0.19 -1.921,1.818 -1.376,2l12.346,0Z" style="fill:#00E2D7;"/>
<path d="M12.29,10c-0.117,0 -0.234,-0.02 -0.344,-0.061c-0.202,-0.074 -0.378,-0.214 -0.495,-0.395c-0.053,-0.082 -0.094,-0.171 -0.121,-0.264c-0.031,-0.106 -0.044,-0.216 -0.039,-0.325c0.005,-0.114 0.03,-0.227 0.073,-0.333c0.047,-0.115 0.116,-0.222 0.202,-0.312c0.09,-0.094 0.199,-0.171 0.318,-0.224c0.128,-0.057 0.266,-0.086 0.406,-0.086l4.24,0c-0.317,0.751 -0.784,1.433 -1.376,2l-2.864,0Z" style="fill:#00A6AF;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -1,194 +1,12 @@
import * as R from './rand.js'; import { Rand } from './color/rand.js';
import { Color, Luma, Chroma, Hue, oklch, oklab, rgb } from './color/def.js';
export { Rand, Color, Luma, Chroma, Hue, oklch, oklab, rgb };
const max = Math.max; const max = Math.max;
const min = Math.min; const min = Math.min;
export type Luma = number;
export type Chroma = number;
export type Hue = number;
export type Alpha = number;
export type HueDistance = number;
const MAXL: Luma = 0.9;
const MINL: Luma = 0.4;
const MINL_LIGHT: Luma = 0.7;
const MAXL_DARK: Luma = 0.65;
const MINC_LIGHT: Chroma = 0.08;
const MAXC_LIGHT: Chroma = 0.1;
const MINC_DARK: Chroma = 0.12;
const MAXC_DARK: Chroma = 0.175;
// max spread for a sequence of analogous colors. unless that would put them
// too close together
const MAXH_WIDTH: HueDistance = 80;
// minimum distance between adjacent analogous colors
const MINH_SEP: HueDistance = 5;
// size of the wedge a "complementary" color can be in
const MAXH_COMPL: HueDistance = 40;
// size of the wedge a "triadic" color can be in
const MAXH_TRIAD: HueDistance = 25;
type LD = 'light' | 'dark';
export namespace Oklch {
export type Channel = 'l' | 'c' | 'h';
export type Channels = Record<Channel, number>;
export type ChannelMap = (x: number) => number;
export type ChannelMaps = Record<Channel, ChannelMap>;
// a function, or constant value, for each channel;
// or nothing, which indicates identity function
export type With = Partial<Record<Channel, number | ChannelMap>>;
export type With1 = ChannelMap | number | undefined;
}
function isLight(l: Luma): boolean { return l >= MINL_LIGHT; }
export namespace Rand { export type State = R.State; }
type CloseFar = 'close' | 'far';
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);
}
dullFor(l: Luma, baseC: Chroma): Chroma {
return this.float(baseC, isLight(l) ? MINC_LIGHT : MINC_DARK);
}
analogous1(baseH: Hue): Hue {
const size = this.float(MINH_SEP, 2 * MINH_SEP);
return this.boolean() ? baseH + size : baseH - size;
}
analogous(baseH: Hue, count: number): Hue[] {
const minWidth = min(count * MINH_SEP, MAXH_WIDTH * 0.8);
const width = this.float(minWidth, MAXH_WIDTH);
const sep = width / (count - 1);
const start = baseH - (width / 2);
const numbers = Array.from({length: count}, (_u, i) => start + i * sep);
return this.boolean() ? numbers : numbers.reverse();
}
complementary1(baseH: Hue): Hue {
return this.analogous1((baseH + 180) % 360);
}
complementary(baseH: Hue, count: number): Hue[] {
const angle = this.float(180 - MAXH_COMPL/2, 180 + MAXH_COMPL/2);
return this.analogous(baseH + angle, count);
}
triad(baseH: Hue): [Hue, Hue] {
const angle = this.float(120 - MAXH_TRIAD/2, 120 + MAXH_TRIAD/2);
return [baseH - angle, baseH + angle];
}
baseLuma(ld?: LD): Luma {
if (ld == 'light') {
return this.float(MINL_LIGHT, MAXL);
} else if (ld == 'dark') {
return this.float(MINL, MAXL_DARK);
} else {
return this.float(MINL, MAXL);
}
}
baseChroma(l: Luma): Chroma {
if (l >= MINL_LIGHT) {
return this.float(MINC_LIGHT, MAXC_LIGHT);
} else {
return this.float(MINC_DARK, MAXC_DARK);
}
}
baseHue(): Hue { return this.float(360); }
}
export class Oklch {
readonly l: Luma; readonly c: Chroma; readonly h: Hue;
static normHue(h: Hue) { return (h = h % 360) < 0 ? h + 360 : h; }
constructor(l: Luma, c: Chroma, h: Hue);
constructor(r: Rand, ld?: LD);
constructor(cs: Oklch.Channels);
constructor(ll: Luma | Oklch.Channels | Rand, cc?: Chroma | LD, hh?: Hue) {
if (hh !== undefined) {
this.l = ll as Luma;
this.c = cc as Chroma;
this.h = hh as Hue;
} else if (typeof ll == 'object' && 'l' in ll) {
const {l, c, h} = ll as Oklch.Channels;
this.l = l; this.c = c; this.h = h;
} else {
const r = ll as Rand;
this.l = r.baseLuma(cc as LD | undefined);
this.c = r.baseChroma(this.l);
this.h = r.baseHue();
}
}
with(maps: Oklch.With): Oklch {
function call(comp: Oklch.With1, x: number) {
switch (typeof comp) {
case 'number': return comp;
case 'function': return comp(x);
default: return x;
}
}
return new Oklch({
l: call(maps.l, this.l),
c: call(maps.c, this.c),
h: call(maps.h, this.h),
});
}
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);
}
}
}
export type SchemeType = 'triad' | 'fin-belly' | 'fin-body'; export type SchemeType = 'triad' | 'fin-belly' | 'fin-body';
@ -200,7 +18,7 @@ export type MiscLayer = 'eyes' | 'masks' | 'claws' | 'lines';
export type Layer = export type Layer =
OuterLayer | SockLayer | FinLayer | BellyLayer | MiscLayer; OuterLayer | SockLayer | FinLayer | BellyLayer | MiscLayer;
export type ColsOf<A extends string> = Record<A, Oklch>; export type ColsOf<A extends string> = Record<A, Color>;
export type OuterCols = ColsOf<OuterLayer>; export type OuterCols = ColsOf<OuterLayer>;
export type SockCols = ColsOf<SockLayer>; export type SockCols = ColsOf<SockLayer>;
export type FinCols = ColsOf<FinLayer>; export type FinCols = ColsOf<FinLayer>;
@ -219,77 +37,74 @@ export function makeColorInfo<A>(f: (l: Layer) => A): Record<Layer, A> {
return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record<Layer, A>; return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record<Layer, A>;
} }
export type BaseCol = 'outer' | 'belly' | 'fins'; type KnownPalette = Partial<Record<Layer, Color>>;
export type OptionalBaseCol = 'eyes' | 'stripes';
type KnownPalette =
Record<BaseCol, Oklch> & Partial<Record<OptionalBaseCol, Oklch>>;
export function colors(r: Rand = new Rand(), base?: KnownPalette): Scheme { export function colors(r: Rand = new Rand(), base?: KnownPalette): Scheme {
const outer = base?.outer ?? new Oklch(r, 'dark'); const outer = base?.outer ?? r.color('dark');
let outerCols: OuterCols = const spines = base?.spines ?? mkSpines(r, outer);
{ outer, spines: mkSpines(r, outer), vitiligo1: mkVitiligo(r, outer) }; const vitiligo1 = base?.vitiligo1 ?? mkVitiligo(r, outer);
const outerCols: OuterCols = { outer, spines, vitiligo1 };
const stripes = mkStripes(r); const stripes = base?.stripes ?? mkStripes(r);
let sockCols: SockCols = { stripes, cuffs: mkCuffs(r, stripes) }; const cuffs = base?.cuffs ?? mkCuffs(r, stripes);
const sockCols: SockCols = { stripes, cuffs };
let finCols: FinCols, bellyCols: BellyCols, type: SchemeType; let finCols: FinCols, bellyCols: BellyCols, type: SchemeType;
const whichBody = r.float(); const whichBody = r.float();
if (whichBody > 2/3) { if (whichBody > 2/3) {
type = 'triad'; type = 'triad';
const [f, b] = r.triad(outer.h); const [f, b] = r.triad(outer.hue);
finCols = mkFins(r, f, outer, base); bellyCols = mkBelly(r, b, base); finCols = mkFins(r, f, outer, base); bellyCols = mkBelly(r, b, base);
} else if (whichBody > 1/3) { } else if (whichBody > 1/3) {
type = 'fin-belly'; type = 'fin-belly';
const [f, b] = r.complementary(outer.h, 2); const [f, b] = r.complementary(outer.hue, 2);
finCols = mkFins(r, f!, outer, base); bellyCols = mkBelly(r, b!, base); finCols = mkFins(r, f!, outer, base); bellyCols = mkBelly(r, b!, base);
} else { } else {
type = 'fin-body'; type = 'fin-body';
finCols = mkFins(r, r.analogous1(outer.h), outer, base); finCols = mkFins(r, r.analogous1(outer.hue), outer, base);
bellyCols = mkBelly(r, r.complementary1(outer.h), base); bellyCols = mkBelly(r, r.complementary1(outer.hue), base);
} }
let miscCols = mkMisc(r, outerCols, finCols, bellyCols, base); const miscCols = mkMisc(r, outerCols, finCols, bellyCols, base);
return merge(outerCols, sockCols, finCols, bellyCols, miscCols, type); return merge(outerCols, sockCols, finCols, bellyCols, miscCols, type);
} }
function mkSpines(r: Rand, outer: Oklch): Oklch { function mkSpines(r: Rand, outer: Color): Color {
return outer.with({ return outer.with({ type: 'oklch',
l: l => r.darkFor(l), l: l => r.darkFor(l),
c: c => r.brightFor(outer.l, c), c: c => r.brightFor(outer.luma, c),
h: h => r.float(h + 12, h - 12), h: h => r.float(h + 12, h - 12),
}) })
} }
function mkVitiligo(r: Rand, outer: Oklch): Oklch { function mkVitiligo(r: Rand, outer: Color): Color {
return outer.with({ return outer.with({ type: 'oklch',
l: x => r.float(max(x, 0.94), 0.985), // exception to MAXL l: x => r.float(max(x, 0.94), 0.985), // exception to MAXL
c: x => r.float(min(x, 0.1), MINC_LIGHT), c: x => r.float(min(x, 0.1), r.mincLight),
}); });
} }
function mkStripes(r: Rand): Oklch { function mkStripes(r: Rand): Color {
return new Oklch({ return oklch(r.float(0.8, r.maxl),
l: r.float(0.8, MAXL), r.float(r.mincLight, r.maxcLight),
c: r.float(MINC_LIGHT, MAXC_LIGHT), r.baseHue());
h: r.baseHue(),
});
} }
function mkCuffs(r: Rand, sock: Oklch): Oklch { function mkCuffs(r: Rand, sock: Color): Color {
return sock.with({ return sock.with({ type: 'oklch',
l: l => r.float(l * 0.85, l * 0.65), l: l => r.float(l * 0.85, l * 0.65),
c: c => r.float(c, MAXC_LIGHT), c: c => r.float(c, r.maxcLight),
h: h => r.float(h + 8, h - 8), h: h => r.float(h + 8, h - 8),
}); });
} }
function mkFins(r: Rand, h: Hue, outer: Oklch, base?: KnownPalette): FinCols { function mkFins(r: Rand, h: Hue, outer: Color,
const baseFin1 = base?.fins; base?: KnownPalette): FinCols {
const [fin1Hue, fin2Hue, fin3Hue] = r.analogous(baseFin1?.h ?? h, 3); const baseFin1 = base?.fins1;
const [fin1Hue, fin2Hue, fin3Hue] = r.analogous(baseFin1?.hue ?? h, 3);
const direction: 'lighter' | 'darker' = r.choice(['lighter', 'darker']); const direction: 'lighter' | 'darker' = r.choice(['lighter', 'darker']);
@ -300,56 +115,52 @@ function mkFins(r: Rand, h: Hue, outer: Oklch, base?: KnownPalette): FinCols {
return direction == 'lighter' ? r.dullFor(l, c) : r.brightFor(l, c); return direction == 'lighter' ? r.dullFor(l, c) : r.brightFor(l, c);
} }
const fins1 = baseFin1 ?? oklch(ll(outer.l), cc(outer.l, outer.c), fin1Hue!); const fins1 = baseFin1 ??
const fins2 = oklch(ll(fins1.l), cc(fins1.l, fins1.c), fin2Hue!); oklch(ll(outer.luma), cc(outer.luma, outer.chroma), fin1Hue!);
const fins3 = oklch(ll(fins2.l), cc(fins2.l, fins2.c), fin3Hue!); const fins2 = base?.fins2 ??
const lighter = fins1.l >= fins3.l ? fins1 : fins3; oklch(ll(fins1.luma), cc(fins1.luma, fins1.chroma), fin2Hue!);
const vitiligo4 = mkVitiligo(r, lighter); const fins3 = base?.fins3 ??
oklch(ll(fins2.luma), cc(fins2.luma, fins2.chroma), fin3Hue!);
const lighter = fins1.luma >= fins3.luma ? fins1 : fins3;
const vitiligo4 = base?.vitiligo4 ?? mkVitiligo(r, lighter);
return { fins1, fins2, fins3, vitiligo4 }; return { fins1, fins2, fins3, vitiligo4 };
} }
function mkBelly(r: Rand, h: Hue, base?: KnownPalette): BellyCols { function mkBelly(r: Rand, h: Hue, base?: KnownPalette): BellyCols {
let baseBelly1 = base?.belly; const baseBelly1 = base?.belly1;
const [belly1Hue, belly2Hue] = r.analogous(baseBelly1?.h ?? h, 2); const [belly1Hue, belly2Hue] = r.analogous(baseBelly1?.hue ?? h, 2);
const belly1 = baseBelly1 ?? new Oklch({ const belly1 = baseBelly1 ??
l: r.float(0.7, MAXL), oklch(r.float(0.7, r.maxl), r.baseChroma(1), belly1Hue!);
c: r.baseChroma(1), const belly2 = base?.belly2 ?? belly1.with({ type: 'oklch',
h: belly1Hue! l: x => min(r.maxl, x * 1.1),
});
const belly2 = belly1.with({
l: x => min(MAXL, x * 1.1),
c: x => x * 0.9, c: x => x * 0.9,
h: belly2Hue!, h: [belly2Hue!],
}); });
const vitiligo3 = mkVitiligo(r, belly1); const vitiligo3 = base?.vitiligo3 ?? mkVitiligo(r, belly1);
const vitiligo2 = mkVitiligo(r, belly2); const vitiligo2 = base?.vitiligo2 ?? mkVitiligo(r, belly2);
return { belly1, belly2, vitiligo2, vitiligo3 }; return { belly1, belly2, vitiligo2, vitiligo3 };
} }
function mkMisc(r: Rand, o: OuterCols, f: FinCols, b: BellyCols, function mkMisc(r: Rand, o: OuterCols, f: FinCols, b: BellyCols,
base?: KnownPalette): MiscCols { base?: KnownPalette): MiscCols {
const masks = new Oklch({ const masks = base?.masks ??
l: r.float(0.8, MAXL), oklch(r.float(0.8, r.maxl), r.float(0.01, 0.06),
c: r.float(0.01, 0.06), r.analogous1(r.choice([o.outer, b.belly1, f.fins1]).hue));
h: r.analogous1(r.choice([o.outer, b.belly1, f.fins1]).h)
});
return { return {
masks, masks,
eyes: base?.eyes ?? new Oklch({ eyes: base?.eyes ?? oklch(
l: r.baseLuma('light'), r.baseLuma('light'),
c: r.float(0.28, MAXC_LIGHT), r.float(0.28, r.maxcLight),
h: r.boolean() ? r.analogous1(o.outer.h) : r.complementary1(o.outer.h) r.boolean() ? r.analogous1(o.outer.hue) : r.complementary1(o.outer.hue)
}), ),
claws: masks.with({ claws: base?.claws ??
l: x => min(MAXL, x + r.float(0, 0.1)), masks.with({ type: 'oklch',
c: r.float(0.01, 0.06), l: x => min(r.maxl, x + r.float(0, 0.1)),
c: [r.float(0.01, 0.06)],
h: h => r.analogous1(h), h: h => r.analogous1(h),
}), }),
lines: new Oklch({ lines: base?.lines ??
l: r.float(0.01, 0.06), oklch(r.float(0.01, 0.06), r.baseChroma(0), r.analogous1(o.outer.hue)),
c: r.baseChroma(0),
h: r.analogous1(o.outer.h)
}),
}; };
} }
@ -366,147 +177,69 @@ function merge({ outer, spines, vitiligo1 }: OuterCols,
} }
export namespace Rgb {
export type Channel = number;
export type Channels = { r: number, g: number, b: number };
}
export class Rgb {
readonly r: Rgb.Channel;
readonly g: Rgb.Channel;
readonly b: Rgb.Channel;
static clamp(x: Rgb.Channel) {
return min(max(0, Math.floor(x)), 255);
}
constructor(r: Rgb.Channel, g: Rgb.Channel, b: Rgb.Channel);
constructor({r, g, b}: Rgb.Channels);
constructor(rr: Rgb.Channel | Rgb.Channels, gg?: Rgb.Channel, bb?: Rgb.Channel) {
const C = Rgb.clamp;
if (typeof rr == 'number') {
this.r = C(rr!); this.g = C(gg!); this.b = C(bb!);
} else {
this.r = C(rr.r); this.g = C(rr.g); this.b = C(rr.b);
}
}
css() {
function h(x: Rgb.Channel) {
let s = x.toString(16);
return s.length == 2 ? s : '0' + s;
}
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>;
let rgbBuf: OffscreenCanvasRenderingContext2D;
export function toRgbViaCanvas(col: Oklch): Rgb {
rgbBuf ??= new OffscreenCanvas(1, 1).getContext('2d')!;
rgbBuf.fillStyle = col.css();
rgbBuf.fillRect(0, 0, 1, 1);
const pix = rgbBuf.getImageData(0, 0, 1, 1).data;
return rgb(pix[0]!, pix[1]!, pix[2]!);
}
export function toRgbs(col: Colors): Rgbs {
return makeColorInfo(l => col[l].rgb());
}
export function toHex({r, g, b}: Rgb): string {
function chan(n: number) {
let a = Math.floor(n).toString(16);
return a.length == 1 ? `0${a}` : a;
}
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> = { export const KNOWN: Record<string, KnownPalette> = {
niss: { niss: {
outer: oklch(0.83, 0.201, 151), outer: oklch(0.83, 0.201, 151),
belly: oklch(0.87, 0.082, 99), belly1: oklch(0.87, 0.082, 99),
fins: oklch(0.68, 0.178, 16), fins1: oklch(0.68, 0.178, 16),
eyes: oklch(0.73, 0.135, 242), eyes: oklch(0.73, 0.135, 242),
}, },
kesi: { kesi: {
outer: oklch(0.86, 0.147, 147), outer: oklch(0.86, 0.147, 147),
belly: oklch(0.96, 0.04, 108), belly1: oklch(0.96, 0.04, 108),
fins: oklch(0.94, 0.142, 102), fins1: oklch(0.94, 0.142, 102),
eyes: oklch(0.76, 0.115, 300), eyes: oklch(0.76, 0.115, 300),
}, },
60309: { 60309: {
outer: oklch(0.84, 0.068, 212), outer: oklch(0.84, 0.068, 212),
belly: oklch(0.56, 0.035, 233), belly1: oklch(0.56, 0.035, 233),
fins: oklch(0.55, 0.101, 268), fins1: oklch(0.55, 0.101, 268),
eyes: oklch(0.86, 0.146, 154), eyes: oklch(0.86, 0.146, 154),
}, },
'prickly pear': { 'prickly pear': {
outer: oklch(0.64, 0.087, 316), outer: oklch(0.64, 0.087, 316),
belly: oklch(0.88, 0.03, 88), belly1: oklch(0.88, 0.03, 88),
fins: oklch(0.6, 0.071, 142), fins1: oklch(0.6, 0.071, 142),
eyes: oklch(0.66, 0.091, 134), eyes: oklch(0.66, 0.091, 134),
}, },
'the goo': { 'the goo': {
outer: oklch(0.92, 0.046, 354), outer: oklch(0.92, 0.046, 354),
belly: oklch(0.83, 0.099, 354), belly1: oklch(0.83, 0.099, 354),
fins: oklch(0.74, 0.115, 354), fins1: oklch(0.74, 0.115, 354),
eyes: oklch(0.73, 0.149, 0), eyes: oklch(0.73, 0.149, 0),
}, },
lambda: { lambda: {
outer: oklch(0.71, 0.154, 58), outer: oklch(0.71, 0.154, 58),
belly: oklch(0.9, 0.05, 80), belly1: oklch(0.9, 0.05, 80),
fins: oklch(0.76, 0.16, 140), fins1: oklch(0.76, 0.16, 140),
eyes: oklch(0.82, 0.178, 141), eyes: oklch(0.82, 0.178, 141),
}, },
flussence: { flussence: {
outer: oklch(0.77, 0.118, 133), outer: oklch(0.77, 0.118, 133),
belly: oklch(0.71, 0.086, 253), belly1: oklch(0.71, 0.086, 253),
fins: oklch(0.58, 0.102, 254), fins1: oklch(0.58, 0.102, 254),
eyes: oklch(0.37, 0.107, 278), eyes: oklch(0.37, 0.107, 278),
}, },
serena: { serena: {
outer: oklch(0.69, 0.176, 349), outer: oklch(0.69, 0.176, 349),
belly: oklch(0.92, 0.04, 350), belly1: oklch(0.92, 0.04, 350),
fins: oklch(0.74, 0.138, 319), fins1: oklch(0.74, 0.138, 319),
eyes: oklch(0.65, 0.206, 4), eyes: oklch(0.65, 0.206, 4),
}, },
pippin: { pippin: {
outer: oklch(0.74, 0.08, 61), outer: oklch(0.74, 0.08, 61),
belly: oklch(0.82, 0.062, 70), belly1: oklch(0.82, 0.062, 70),
fins: oklch(0.52, 0.09, 45), fins1: oklch(0.52, 0.09, 45),
eyes: oklch(0.74, 0.167, 136), eyes: oklch(0.74, 0.167, 136),
}, },
su: { su: {
outer: oklch(0.29, 0.012, 219), outer: oklch(0.29, 0.012, 219),
belly: oklch(0.89, 0.01, 256), belly1: oklch(0.89, 0.01, 256),
fins: oklch(0.53, 0.093, 20), fins1: oklch(0.53, 0.093, 20),
// eyes: oklch(0.53, 0.109, 254),
}, },
trans: { trans: {
outer: oklch(0.83, 0.065, 228), outer: oklch(0.83, 0.065, 228),
belly: oklch(0.95, 0.021, 137), belly1: oklch(0.95, 0.021, 137),
fins: oklch(0.86, 0.069, 352), fins1: oklch(0.86, 0.069, 352),
// eyes: oklch(0.57, 0.158, 273),
}, },
}; };

View file

@ -0,0 +1,127 @@
export type Oklch = { type: 'oklch', l: number, c: number, h: number };
export type Oklab = { type: 'oklab', l: number, a: number, b: number };
export type Srgb = { type: 'srgb', r: number, g: number, b: number };
export type Lrgb = { type: 'lrgb', r: number, g: number, b: number };
export type AnyColor = Oklch | Oklab | Srgb;
type Deg = number;
type Rad = number;
function deg2rad(θ: Deg): Rad { return θ / 180 * Math.PI; }
function rad2deg(θ: Rad): Deg { return θ * 180 / Math.PI; }
function dcos(θ: Deg): number { return Math.cos(deg2rad(θ)); }
function dsin(θ: Deg): number { return Math.sin(deg2rad(θ)); }
function datan2(b: number, a: number): Deg { return rad2deg(Math.atan2(b, a)); }
export function normDeg(θ: Deg): Deg {
θ %= 360;
return θ < 0 ? θ + 360 : θ;
}
export function oklch2oklab({ l, c, h }: Oklch): Oklab {
return { type: 'oklab', l, a: c * dcos(h), b: c * dsin(h) };
}
export function oklab2oklch({ l, a, b }: Oklab): Oklch {
return { type: 'oklch', l, c: Math.sqrt(a*a + b*b), h: datan2(b, a) }
}
// https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
export function lrgb2oklab({ r, g, b }: Lrgb): Oklab {
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
const l_ = Math.cbrt(l);
const m_ = Math.cbrt(m);
const s_ = Math.cbrt(s);
return { type: 'oklab',
l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
};
}
export function oklab2lrgb({ l, a, b }: Oklab): Lrgb {
const L_ = l + 0.3963377774 * a + 0.2158037573 * b;
const M_ = l - 0.1055613458 * a - 0.0638541728 * b;
const S_ = l - 0.0894841775 * a - 1.2914855480 * b;
const L = L_ * L_ * L_;
const M = M_ * M_ * M_;
const S = S_ * S_ * S_;
return { type: 'lrgb',
r: clamp(+4.0767416621 * L - 3.3077115913 * M + 0.2309699292 * S),
g: clamp(-1.2684380046 * L + 2.6097574011 * M - 0.3413193965 * S),
b: clamp(-0.0041960863 * L - 0.7034186147 * M + 1.7076147010 * S),
};
}
function clamp(x: number): number {
return Math.max(0, Math.min(1, x));
}
// https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F
function γ(x: number): number {
return x >= 0.0031308 ? 1.055 * x ** (1/2.4) - 0.055
: 12.92 * x;
}
function γ̂(x: number): number {
return x >= 0.04045 ? ((x + 0.055)/1.055) ** 2.4
: x / 12.92;
}
export function lrgb2srgb({ r, g, b }: Lrgb): Srgb {
return { type: 'srgb', r: γ(r), g: γ(g), b: γ(b) };
}
export function srgb2lrgb({ r, g, b }: Srgb): Lrgb {
return { type: 'lrgb', r: γ̂(r), g: γ̂(g), b: γ̂(b) };
}
export function oklab2srgb(c: Oklab): Srgb {
return lrgb2srgb(oklab2lrgb(c));
}
export function oklch2srgb(c: Oklch): Srgb {
return oklab2srgb(oklch2oklab(c));
}
export function srgb2oklab(c: Srgb): Oklab {
return lrgb2oklab(srgb2lrgb(c));
}
export function srgb2oklch(c: Srgb): Oklch {
return oklab2oklch(srgb2oklab(c));
}
export function toOklch(c: AnyColor): Oklch {
switch (c.type) {
case 'oklch': return c;
case 'oklab': return oklab2oklch(c);
case 'srgb': return srgb2oklch(c);
}
}
export function toOklab(c: AnyColor): Oklab {
switch (c.type) {
case 'oklch': return oklch2oklab(c);
case 'oklab': return c;
case 'srgb': return srgb2oklab(c);
}
}
export function toSrgb(c: AnyColor): Srgb {
switch (c.type) {
case 'oklch': return oklch2srgb(c);
case 'oklab': return oklab2srgb(c);
case 'srgb': return c;
}
}

View file

@ -0,0 +1,136 @@
import { Oklch, Oklab, Srgb as Rgb, AnyColor, normDeg } from './conv.js';
import * as Conv from './conv.js';
export { Oklch, Oklab, Rgb, AnyColor };
export type CSSFormat = 'oklch' | 'oklab' | 'rgb' | 'hex';
export type ChannelMapper<A> = [A] | ((x: A) => A);
export type ColorMapper<C extends { type: string }> =
{ type: C['type'] } &
{ [k in Exclude<keyof C, 'type'>]?: ChannelMapper<C[k]> };
export function apply<A>(f: ChannelMapper<A> | undefined, x: A): A {
if (typeof f == 'undefined') return x;
else if (typeof f == 'function') return f(x);
else return f[0];
}
export class Color {
readonly oklch: Oklch;
readonly oklab: Oklab;
readonly srgb: Rgb;
constructor(c: AnyColor) {
this.oklch = Conv.toOklch(c);
this.oklab = Conv.toOklab(c);
this.srgb = Conv.toSrgb(c);
}
get luma() { return this.oklch.l; }
get chroma() { return this.oklch.c; }
get hue() { return this.oklch.h; }
get labA() { return this.oklab.a; }
get labB() { return this.oklab.b; }
get red() { return Math.floor(255 * this.srgb.r); }
get green() { return Math.floor(255 * this.srgb.g); }
get blue() { return Math.floor(255 * this.srgb.b); }
css(format: CSSFormat = 'hex', α = 1): string {
switch (format) {
case 'oklch': {
const { l, c, h } = this.oklch;
return `oklch(${pc(l)} ${pc(c, 0.4)} ${deg(h)} / ${pc(α)})`;
}
case 'oklab': {
const { l, a, b } = this.oklab;
return `oklab(${pc(l)} ${pc(a)} ${pc(b)} / ${pc(α)})`;
}
case 'rgb': {
const { r, g, b } = this.srgb;
return `rgb(${pc(r)} ${pc(g)} ${pc(b)} / ${pc(α)})`;
}
case 'hex': {
const { r, g, b } = this.srgb;
return α == 1 ? `#${hex(r)}${hex(g)}${hex(b)}` :
`#${hex(r)}${hex(g)}${hex(b)}${hex(α)}`;
}
}
function hex(c: number) {
const d = Math.min(255, Math.max(0, Math.floor(255 * c)));
const str = d.toString(16);
return str.length == 1 ? `0${str}` : str;
}
function pc(c: number, max = 1) {
c *= 100 / max;
return `${c.toFixed(0)}%`;
}
function deg(θ: number) {
return `${normDeg(θ).toFixed(0)}deg`;
}
}
with(maps: ColorMapper<Oklch>): Color;
with(maps: ColorMapper<Rgb>): Color;
with(maps: ColorMapper<Oklch | Rgb>): Color {
switch (maps.type) {
case 'oklch': {
const { l, c, h } = this.oklch;
const { l: ll, c: cc, h: hh } = maps as ColorMapper<Oklch>;
return oklch(apply(ll, l), apply(cc, c), apply(hh, h));
}
case 'srgb': {
const { r, g, b } = this.srgb;
const { r: rr, g: gg, b: bb } = maps as ColorMapper<Rgb>;
return rgb(apply(rr, r), apply(gg, g), apply(bb, b));
}
}
}
static validate(x: unknown): Color | undefined {
if (typeof x == 'object' && x != null) {
if ('l' in x && 'c' in x && 'h' in x) {
const { l, c, h } = x;
if (num(l) && num(c) && num(h)) return oklch(l, c, h);
} else if ('l' in x && 'a' in x && 'b' in x) {
const { l, a, b } = x;
if (num(l) && num(a) && num(b)) return oklab(l, a, b);
} else if ('r' in x && 'g' in x && 'b' in x) {
const { r, g, b } = x;
if (num(r) && num(g) && num(b)) return rgb(r, g, b);
}
}
function num(x: unknown): x is number { return typeof x == 'number'; }
}
toJSON(): unknown { const { l, c, h } = this.oklch; return { l, c, h }; }
}
export function oklch(l: number, c: number, h: number): Color {
return new Color({ type: 'oklch', l, c, h });
}
export function oklab(l: number, a: number, b: number): Color {
return new Color({ type: 'oklab', l, a, b });
}
export function rgb(r: number, g: number, b: number,
style: 'int' | 'float' = 'int'): Color {
return style == 'int' ?
new Color({ type: 'srgb', r: r / 255, g: g / 255, b: b / 255 }) :
new Color({ type: 'srgb', r, g, b });
}
export type Luma = number;
export type Chroma = number;
export type Hue = number;
export type Alpha = number;
export type HueDistance = number;

View file

@ -0,0 +1,120 @@
import * as R from '../rand.js';
import * as Color from './def.js';
const max = Math.max;
const min = Math.min;
export type State = R.State;
export type CloseFar = 'close' | 'far';
export type LightDark = 'light' | 'dark';
export class Rand extends R.Rand {
maxl: Color.Luma = 0.9;
minl: Color.Luma = 0.4;
minlLight: Color.Luma = 0.7;
maxlDark: Color.Luma = 0.65;
mincLight: Color.Chroma = 0.08;
maxcLight: Color.Chroma = 0.1;
mincDark: Color.Chroma = 0.12;
maxcDark: Color.Chroma = 0.175;
// max spread for a sequence of analogous colors. unless that would put them
// too close together
maxhWidth: Color.HueDistance = 80;
// minimum distance between adjacent analogous colors
minhSep: Color.HueDistance = 5;
// size of the wedge a "complementary" color can be in
maxhCompl: Color.HueDistance = 40;
// size of the wedge a "triadic" color can be in
maxhTriad: Color.HueDistance = 25;
constructor();
constructor([a, b, c, d]: State);
constructor(str: string);
constructor(st?: State | string) {
if (st === undefined) super();
else if (typeof st === 'string') super(st);
else super(st);
}
isLight(l: Color.Luma): boolean { return l >= this.minlLight; }
lightFor(baseL: Color.Luma, d: CloseFar = 'close'): Color.Luma {
const maxl = d == 'close' ? min(this.maxl, baseL * 1.25) : this.maxl;
return this.float(baseL, maxl);
}
darkFor(baseL: Color.Luma, d: CloseFar = 'close'): Color.Luma {
const minl = d == 'close' ? max(this.minl, baseL * 0.8) : this.minl
return this.float(minl, baseL);
}
brightFor(l: Color.Luma, baseC: Color.Chroma): Color.Chroma {
return this.float(baseC, this.isLight(l) ? this.maxcLight : this.maxcDark);
}
dullFor(l: Color.Luma, baseC: Color.Chroma): Color.Chroma {
return this.float(baseC, this.isLight(l) ? this.mincLight : this.mincDark);
}
analogous1(baseH: Color.Hue): Color.Hue {
const size = this.float(this.minhSep, 2 * this.minhSep);
return this.boolean() ? baseH + size : baseH - size;
}
analogous(baseH: Color.Hue, count: number): Color.Hue[] {
const minWidth = min(count * this.minhSep, this.maxhWidth * 0.8);
const width = this.float(minWidth, this.maxhWidth);
const sep = width / (count - 1);
const start = baseH - (width / 2);
const numbers = Array.from({length: count}, (_u, i) => start + i * sep);
return this.boolean() ? numbers : numbers.reverse();
}
complementary1(baseH: Color.Hue): Color.Hue {
return this.analogous1((baseH + 180) % 360);
}
complementary(baseH: Color.Hue, count: number): Color.Hue[] {
const angle = this.float(180 - this.maxhCompl/2, 180 + this.maxhCompl/2);
return this.analogous(baseH + angle, count);
}
triad(baseH: Color.Hue): [Color.Hue, Color.Hue] {
const angle = this.float(120 - this.maxhTriad/2, 120 + this.maxhTriad/2);
return [baseH - angle, baseH + angle];
}
baseLuma(ld?: LightDark): Color.Luma {
if (ld == 'light') {
return this.float(this.minlLight, this.maxl);
} else if (ld == 'dark') {
return this.float(this.minl, this.maxlDark);
} else {
return this.float(this.minl, this.maxl);
}
}
baseChroma(l: Color.Luma): Color.Chroma {
if (l >= this.minlLight) {
return this.float(this.mincLight, this.maxcLight);
} else {
return this.float(this.mincDark, this.maxcDark);
}
}
baseHue(): Color.Hue { return this.float(360); }
color(ld?: LightDark): Color.Color {
const l = this.baseLuma(ld);
const c = this.baseChroma(l);
const h = this.baseHue();
return Color.oklch(l, c, h);
}
}

View file

@ -1,19 +1,17 @@
import { Colors as Oklchs, Rgbs } from './color.js'; import { Colors } from './color.js';
import * as Color from './color.js'; import * as Color from './color.js';
export class HistoryItem { export class HistoryItem {
name: string; name: string;
oklch: Oklchs; cols: Colors;
rgb: Rgbs;
constructor(name: string, oklch: Oklchs, rgb: Rgbs) { constructor(name: string, cols: Colors) {
this.oklch = oklch; this.cols = cols;
this.rgb = rgb;
this.name = name; this.name = name;
} }
asHtml(): HTMLButtonElement { asHtml(): HTMLButtonElement {
const { lines, outer, belly1: belly, fins1: fins } = this.rgb; const { lines, outer, belly1: belly, fins1: fins } = this.cols;
const content = ` const content = `
<svg class=history-colors width=30 height=25 viewBox="-10 -10 140 120"> <svg class=history-colors width=30 height=25 viewBox="-10 -10 140 120">
@ -37,7 +35,7 @@ export class HistoryItem {
<span class=history-name>${this.name}</span> <span class=history-name>${this.name}</span>
`; `;
let button = document.createElement('button'); const button = document.createElement('button');
button.className = 'history-item'; button.className = 'history-item';
button.dataset.name = this.name; button.dataset.name = this.name;
button.innerHTML = content; button.innerHTML = content;
@ -54,7 +52,7 @@ export class History {
add(name: string): void { this.items.push(name); } add(name: string): void { this.items.push(name); }
*iterNames(maxLength: number = 100): Iterable<string> { *iterNames(maxLength: number = 100): Iterable<string> {
let seen = new Set<string>; const seen = new Set<string>;
let done = 0; let done = 0;
for (let i = this.items.length - 1; i >= 0; i--) { for (let i = this.items.length - 1; i >= 0; i--) {
@ -71,10 +69,9 @@ export class History {
// pass a negative number to iterate over all // pass a negative number to iterate over all
*iterItems(maxLength?: number): Iterable<HistoryItem> { *iterItems(maxLength?: number): Iterable<HistoryItem> {
for (const name of this.iterNames(maxLength)) { for (const name of this.iterNames(maxLength)) {
const oklch = Color.colors(new Color.Rand(name), Color.KNOWN[name]); const cols = Color.colors(new Color.Rand(name), Color.KNOWN[name]);
const rgbs = Color.toRgbs(oklch);
yield new HistoryItem(name, oklch, rgbs); yield new HistoryItem(name, cols);
} }
} }
@ -107,8 +104,8 @@ export class History {
} }
prune(maxLength?: number): void { prune(maxLength?: number): void {
let keep = []; const keep = [];
for (let name of this.iterNames(maxLength)) keep.push(name); for (const name of this.iterNames(maxLength)) keep.push(name);
this.items = keep.reverse(); this.items = keep.reverse();
} }
} }

View file

@ -4,14 +4,18 @@ async function loadBitmap(url: string): Promise<ImageBitmap> {
const img0 = new Image; const img0 = new Image;
const img: Promise<ImageBitmapSource> = new Promise((ok, err) => { const img: Promise<ImageBitmapSource> = new Promise((ok, err) => {
img0.addEventListener('load', () => ok(img0)); img0.addEventListener('load', () => ok(img0));
img0.addEventListener('error', () => err(`couldn't load file: ${url}`)); img0.addEventListener('error', () => {
err(new Error(`couldn't load file: ${url}`));
});
}); });
img0.src = url; img0.src = url;
return createImageBitmap(await img); return createImageBitmap(await img);
} }
export type Buffer = OffscreenCanvasRenderingContext2D; export type Buffer =
CanvasCompositing & CanvasDrawImage & CanvasImageData &
CanvasRect & CanvasState;
function dataViaBuffer(bmp: ImageBitmap, buf: Buffer): ImageData { function dataViaBuffer(bmp: ImageBitmap, buf: Buffer): ImageData {
buf.clearRect(0, 0, bmp.width, bmp.height); buf.clearRect(0, 0, bmp.width, bmp.height);
@ -19,34 +23,10 @@ function dataViaBuffer(bmp: ImageBitmap, buf: Buffer): ImageData {
return buf.getImageData(0, 0, bmp.width, bmp.height); return buf.getImageData(0, 0, bmp.width, bmp.height);
} }
async function loadDataLocking(url: string, buf: Buffer): Promise<ImageData> {
return loadBitmap(url).then(i =>
navigator.locks.request('imagebuf', () => dataViaBuffer(i, buf)));
}
async function loadDataFresh(url: string): Promise<ImageData> {
const img = await loadBitmap(url);
let buf = new OffscreenCanvas(img.width, img.height).getContext('2d')!;
return dataViaBuffer(img, buf);
}
export function loadImageData(url: string, buf?: Buffer): Promise<ImageData> {
if (buf && navigator.locks) return loadDataLocking(url, buf);
else return loadDataFresh(url);
}
export const WIDTH = 1040; export const WIDTH = 1040;
export const HEIGHT = 713; export const HEIGHT = 713;
export function makeBuffer(width = WIDTH, height = HEIGHT): Buffer {
return new OffscreenCanvas(width, height).getContext('2d')!;
}
function makeBufferIfLocks(width?: number, height?: number): Buffer | undefined {
if (navigator.locks) return makeBuffer(width, height);
}
export type Layer = 'stroke' | 'static' | 'eyeshine' | Color.Layer; export type Layer = 'stroke' | 'static' | 'eyeshine' | Color.Layer;
// in compositing order // in compositing order
@ -61,14 +41,16 @@ export function makeLayerInfo<A>(f: (l: Layer) => A): Record<Layer, A> {
export async function makeLayerInfoAsync<A>(f: (l: Layer) => Promise<A>): export async function makeLayerInfoAsync<A>(f: (l: Layer) => Promise<A>):
Promise<Record<Layer, A>> { Promise<Record<Layer, A>> {
let list = await Promise.all(allLayers.map(l => f(l).then(res => [l, res]))); const list = await Promise.all(allLayers.map(l => f(l).then(res => [l, res])));
return Object.fromEntries(list); return Object.fromEntries(list) as Promise<Record<Layer, A>>;
} }
export function loadLayers(dir: string): Promise<Record<Layer, ImageData>> { export async function
let buf = makeBufferIfLocks(WIDTH, HEIGHT); loadLayers(dir: string, buf: Buffer): Promise<Record<Layer, ImageData>> {
return makeLayerInfoAsync(l => loadImageData(`./${dir}/${l}.webp`, buf)); const bitmaps =
await makeLayerInfoAsync(l => loadBitmap(`./${dir}/${l}.webp`));
return makeLayerInfo(l => dataViaBuffer(bitmaps[l], buf));
} }
@ -76,7 +58,7 @@ export type Position = [x: number, y: number];
export type Positions = Record<Layer, Position>; export type Positions = Record<Layer, Position>;
export async function loadPos(dir: string): Promise<Positions> { export async function loadPos(dir: string): Promise<Positions> {
return (await fetch(`./${dir}/pos.json`)).json(); return (await fetch(`./${dir}/pos.json`)).json() as Promise<Positions>;
} }
@ -95,10 +77,10 @@ export type Data = {
export type ComposedData = Required<Data>; export type ComposedData = Required<Data>;
export async function loadData(): Promise<Data> { export async function loadData(buf: Buffer): Promise<Data> {
let [fl, fp, bl, bp] = await Promise.all([ const [fl, fp, bl, bp] = await Promise.all([
loadLayers('front'), loadPos('front'), loadLayers('front', buf), loadPos('front'),
loadLayers('back'), loadPos('back') loadLayers('back', buf), loadPos('back')
]); ]);
return { return {
front: makeLayerInfo(l => [fl[l], fp[l]]), front: makeLayerInfo(l => [fl[l], fp[l]]),
@ -107,13 +89,13 @@ export async function loadData(): Promise<Data> {
} }
function recolor({ data }: ImageData, { r, g, b }: Color.Rgb) { function recolor({ data }: ImageData, col: Color.Color) {
for (let i = 0; i < data.length; i += 4) { for (let i = 0; i < data.length; i += 4) {
data[i] = r; data[i+1] = g; data[i+2] = b; data[i] = col.red; data[i+1] = col.green; data[i+2] = col.blue;
} }
} }
export async function recolorAll(layers: Data, cols: Color.Rgbs) { export async function recolorAll(layers: Data, cols: Color.Colors) {
await Promise.all(Color.allLayers.map(l => { await Promise.all(Color.allLayers.map(l => {
recolor(layers.front[l][0], cols[l]); recolor(layers.front[l][0], cols[l]);
recolor(layers.back[l][0], cols[l]); recolor(layers.back[l][0], cols[l]);
@ -139,7 +121,7 @@ async function compose(buf: Buffer, layers: ComposeLayer[],
export async function export async function
ensureComposed(buf: Buffer, data: Data): Promise<ComposedData> { ensureComposed(buf: Buffer, data: Data): Promise<ComposedData> {
let { front, back } = data; const { front, back } = data;
data.frontImage ??= await composeLayers(front); data.frontImage ??= await composeLayers(front);
data.backImage ??= await composeLayers(back); data.backImage ??= await composeLayers(back);
return data as ComposedData; return data as ComposedData;
@ -148,14 +130,12 @@ ensureComposed(buf: Buffer, data: Data): Promise<ComposedData> {
return compose(buf, allLayers.map(l => makeLayer(l, sdata)), WIDTH, HEIGHT); return compose(buf, allLayers.map(l => makeLayer(l, sdata)), WIDTH, HEIGHT);
} }
function makeLayer(l: Layer, sdata: SideData): ComposeLayer { function makeLayer(l: Layer, sdata: SideData): ComposeLayer {
let [i, p] = sdata[l]; const [i, p] = sdata[l];
return [i, p, l == 'eyeshine' ? 'luminosity' : 'source-over']; return [i, p, l == 'eyeshine' ? 'luminosity' : 'source-over'];
} }
} }
export async function redraw(ctx: CanvasRenderingContext2D, export function redraw(ctx: CanvasImageData, data: ComposedData, side: Side) {
buf: Buffer, data: ComposedData, side: Side) {
await ensureComposed(buf, data);
ctx.putImageData(data[`${side}Image`], 0, 0); ctx.putImageData(data[`${side}Image`], 0, 0);
} }

View file

@ -1,18 +1,19 @@
import { Rgb, Rgbs, rgb } from './color.js'; import { Color, Colors, rgb } from './color.js';
import { Layer } from './layer.js'; import { Layer } from './layer.js';
export type Color = export type Swatch =
Exclude<Layer, 'eyeshine' | 'stroke' | 'static'> Exclude<Layer, 'eyeshine' | 'stroke' | 'static'>
| 'collars' | 'bells' | 'tongues' | 'socks' | 'sclera'; | 'collars' | 'bells' | 'tongues' | 'socks' | 'sclera';
// in palette order // in palette order
export const COLORS: Color[] = export const SWATCHES: Swatch[] =
['lines', 'outer', 'vitiligo1', 'spines', 'fins1', 'fins2', 'fins3', ['lines', 'outer', 'vitiligo1', 'spines',
'vitiligo4', 'belly1', 'vitiligo3', 'belly2', 'vitiligo2', 'sclera', 'socks', 'stripes', 'cuffs',
'eyes', 'tongues', 'masks', 'claws', 'socks', 'stripes', 'cuffs', 'vitiligo4', 'belly1', 'vitiligo3', 'belly2', 'vitiligo2',
'collars', 'bells']; 'fins1', 'fins2', 'fins3', 'masks', 'claws',
'sclera', 'eyes', 'tongues', 'collars', 'bells'];
export const NAMES: Partial<Record<Color, string>> = { export const NAMES: Partial<Record<Swatch, string>> = {
outer: 'outer body', outer: 'outer body',
stripes: 'sock stripes', stripes: 'sock stripes',
cuffs: 'sock cuffs', cuffs: 'sock cuffs',
@ -27,13 +28,13 @@ export const NAMES: Partial<Record<Color, string>> = {
vitiligo4: 'fins vitiligo', vitiligo4: 'fins vitiligo',
}; };
export function name(l: Color): string { export function name(l: Swatch): string {
return NAMES[l] ?? l; return NAMES[l] ?? l;
} }
export type StaticColor = Exclude<Color, Layer>; export type StaticColor = Exclude<Swatch, Layer>;
export const STATIC_COLS: Record<StaticColor, Rgb> = { export const STATIC_COLS: Record<StaticColor, Color> = {
collars: rgb(206, 75, 101), collars: rgb(206, 75, 101),
bells: rgb(235, 178, 79), bells: rgb(235, 178, 79),
tongues: rgb(222, 165, 184), tongues: rgb(222, 165, 184),
@ -41,23 +42,23 @@ export const STATIC_COLS: Record<StaticColor, Rgb> = {
sclera: rgb(238, 239, 228), sclera: rgb(238, 239, 228),
}; };
export function get(col: Color, palette: Rgbs): Rgb { export function get(col: Swatch, palette: Colors): Color {
type PPalette = Partial<Record<Color, Rgb>>; type PPalette = Partial<Record<Swatch, Color>>;
let p = palette as PPalette; const p = palette as PPalette;
let s = STATIC_COLS as PPalette; const s = STATIC_COLS as PPalette;
return (p[col] ?? s[col])!; return (p[col] ?? s[col])!;
} }
export function make(seed: string, palette: Rgbs): Blob { export function make(seed: string, palette: Colors): Blob {
let lines = [ const lines = [
"GIMP Palette\n", "GIMP Palette\n",
`Name: quox ${seed}\n`, `Name: quox ${seed}\n`,
"Columns: 6\n\n", "Columns: 6\n\n",
]; ];
for (const col of COLORS) { for (const sw of SWATCHES) {
let { r, g, b } = get(col, palette); const col = get(sw, palette);
lines.push(`${r} ${g} ${b} ${name(col)}\n`); lines.push(`${col.red} ${col.green} ${col.blue} ${name(sw)}\n`);
} }
return new Blob(lines, { type: 'application/x-gimp-palette' }); return new Blob(lines, { type: 'application/x-gimp-palette' });

View file

@ -16,7 +16,7 @@ function message(msg: string, size = 100) {
function urlState(): string | undefined { function urlState(): string | undefined {
let hash = document.location.hash?.substring(1); const hash = document.location.hash?.substring(1);
if (hash != '' && hash !== undefined) return decodeURI(hash); if (hash != '' && hash !== undefined) return decodeURI(hash);
} }
@ -28,37 +28,35 @@ type ApplyStateOpts = {
seed: string, seed: string,
side?: Layer.Side, side?: Layer.Side,
firstLoad?: boolean, firstLoad?: boolean,
buf?: Layer.Buffer,
history?: History, history?: History,
done?: Done, done?: Done,
}; };
async function async function
applyState(data: Layer.Data, opts: ApplyStateOpts): Promise<string> { applyState(data: Layer.Data, opts: ApplyStateOpts): Promise<string> {
let { side, seed, firstLoad, buf, history, done } = opts; const { seed, history } = opts;
let { side, firstLoad, done } = opts;
side ??= 'front'; side ??= 'front';
firstLoad ??= false; firstLoad ??= false;
buf ??= Layer.makeBuffer();
done ??= () => {}; done ??= () => {};
let rand = new Color.Rand(seed); const rand = new Color.Rand(seed);
const oklch = Color.colors(rand, Color.KNOWN[seed]); const cols = Color.colors(rand, Color.KNOWN[seed]);
const rgb = Color.toRgbs(oklch);
const newSeed = rand.alphaNum(); const newSeed = rand.alphaNum();
await Layer.recolorAll(data, rgb); await Layer.recolorAll(data, cols);
updateBg(oklch); updateBg(cols);
updateSvgs(oklch, rgb); updateSvgs(cols);
updateLabel(seed); updateLabel(seed);
updateUrl(seed); updateUrl(seed);
if (firstLoad) { if (firstLoad) {
await instantUpdateImage(side, await Layer.ensureComposed(buf, data)); await instantUpdateImage(side, data);
done(); done();
} else { } else {
await animateUpdateImage(buf, side, data, done); await animateUpdateImage(side, data, done);
} }
if (history) history.addSave(seed); if (history) history.addSave(seed);
@ -74,8 +72,9 @@ function getCanvasCtx(id: CanvasId) {
} }
async function async function
instantUpdateImage(side: Layer.Side, data: Layer.ComposedData) { instantUpdateImage(side: Layer.Side, data: Layer.Data) {
getCanvasCtx('main').putImageData(data[`${side}Image`], 0, 0); const cdata = await Layer.ensureComposed(getCanvasCtx('aux'), data);
getCanvasCtx('main').putImageData(cdata[`${side}Image`], 0, 0);
} }
type Done = () => void; type Done = () => void;
@ -83,10 +82,9 @@ type Done = () => void;
const noAnim = matchMedia('(prefers-reduced-motion: reduce)'); const noAnim = matchMedia('(prefers-reduced-motion: reduce)');
async function async function
animateUpdateImage(buf: Layer.Buffer, side: Layer.Side, animateUpdateImage(side: Layer.Side, data: Layer.Data, done: Done) {
data: Layer.Data, done: Done) {
if (noAnim.matches) { if (noAnim.matches) {
instantUpdateImage(side, await Layer.ensureComposed(buf, data)); await instantUpdateImage(side, data);
done(); done();
return; return;
} }
@ -97,11 +95,11 @@ animateUpdateImage(buf: Layer.Buffer, side: Layer.Side,
const aux = getCanvasCtx('aux'); const aux = getCanvasCtx('aux');
document.documentElement.dataset.running = 'reroll'; document.documentElement.dataset.running = 'reroll';
const cdata = await Layer.ensureComposed(buf, data); const cdata = await Layer.ensureComposed(aux, data);
Layer.redraw(aux, buf, cdata, side); Layer.redraw(aux, cdata, side);
aux.canvas.addEventListener('animationend', async () => { aux.canvas.addEventListener('animationend', () => {
await Layer.redraw(main, buf, cdata, side); Layer.redraw(main, cdata, side);
aux.canvas.style.removeProperty('animation'); aux.canvas.style.removeProperty('animation');
delete document.documentElement.dataset.running; delete document.documentElement.dataset.running;
done(); done();
@ -113,10 +111,9 @@ animateUpdateImage(buf: Layer.Buffer, side: Layer.Side,
} }
async function async function
animateSwapImage(buf: Layer.Buffer, newSide: Layer.Side, animateSwapImage(newSide: Layer.Side, data: Layer.ComposedData, done: Done) {
data: Layer.ComposedData, done: Done) {
if (noAnim.matches) { if (noAnim.matches) {
instantUpdateImage(newSide, data); await instantUpdateImage(newSide, data);
done(); done();
return; return;
} }
@ -127,9 +124,9 @@ animateSwapImage(buf: Layer.Buffer, newSide: Layer.Side,
const aux = getCanvasCtx('aux'); const aux = getCanvasCtx('aux');
document.documentElement.dataset.running = 'swap'; document.documentElement.dataset.running = 'swap';
await Layer.redraw(aux, buf, data, newSide); Layer.redraw(aux, data, newSide);
aux.canvas.addEventListener('animationend', async () => { aux.canvas.addEventListener('animationend', () => {
const image = aux.getImageData(0, 0, Layer.WIDTH, Layer.HEIGHT); const image = aux.getImageData(0, 0, Layer.WIDTH, Layer.HEIGHT);
main.putImageData(image, 0, 0); main.putImageData(image, 0, 0);
@ -144,7 +141,7 @@ animateSwapImage(buf: Layer.Buffer, newSide: Layer.Side,
} }
function updateBg(cols: Color.Colors) { function updateBg(cols: Color.Colors) {
document.documentElement.style.setProperty('--hue', `${cols.outer.h}`); document.documentElement.style.setProperty('--hue', `${cols.outer.hue}`);
} }
function updateLabel(seed: string) { function updateLabel(seed: string) {
@ -152,21 +149,21 @@ function updateLabel(seed: string) {
if (stateLabel) stateLabel.innerHTML = seed; if (stateLabel) stateLabel.innerHTML = seed;
} }
function updateSvgs(oklch: Color.Colors, rgb: Color.Rgbs) { function updateSvgs(cols: Color.Colors) {
const paletteObj = document.getElementById('palette') as HTMLObjectElement; const paletteObj = document.getElementById('palette') as HTMLObjectElement;
const palette = paletteObj.contentDocument as XMLDocument | null; const palette = paletteObj.contentDocument as XMLDocument | null;
if (palette) { if (palette) {
palette.documentElement.style.setProperty('--hue', `${oklch.outer.h}`); palette.documentElement.style.setProperty('--hue', `${cols.outer.hue}`);
const get = (id: string) => palette.getElementById(id); const get = (id: string) => palette.getElementById(id);
for (const layer of Color.allLayers) { for (const layer of Color.allLayers) {
let col = rgb[layer].css(); const col = cols[layer].css();
let elem; let elem;
// main group // main group
if (elem = get(`i-${layer}`)) { if ((elem = get(`i-${layer}`))) {
if (oklch[layer].l < 0.6) { if (cols[layer].luma < 0.6) {
elem.classList.add('light'); elem.classList.remove('dark'); elem.classList.add('light'); elem.classList.remove('dark');
} else { } else {
elem.classList.add('dark'); elem.classList.remove('light'); elem.classList.add('dark'); elem.classList.remove('light');
@ -175,9 +172,9 @@ function updateSvgs(oklch: Color.Colors, rgb: Color.Rgbs) {
} }
// label // label
if (elem = get(`c-${layer}`)) elem.innerHTML = col; if ((elem = get(`c-${layer}`))) elem.innerHTML = col;
// minor swatch, if applicable // minor swatch, if applicable
if (elem = get(`s-${layer}`)) elem.style.setProperty('--col', col); if ((elem = get(`s-${layer}`))) elem.style.setProperty('--col', col);
} }
} }
} }
@ -188,14 +185,12 @@ function showHistory(history: History, data: Layer.Data,
const list = document.getElementById('history-items'); const list = document.getElementById('history-items');
if (!list) return; if (!list) return;
list.innerHTML = ''; list.innerHTML = '';
let { side, firstLoad, buf, done } = opts; const { side, firstLoad, done } = opts;
for (const item of history.iterItems()) { for (const item of history.iterItems()) {
const elem = item.asHtml(); const elem = item.asHtml();
let allOpts = { side, firstLoad, buf, done, seed: item.name, history }; const allOpts = { side, firstLoad, done, seed: item.name, history };
elem.addEventListener('click', () => { elem.addEventListener('click', () => void applyState(data, allOpts));
applyState(data, allOpts);
});
list.appendChild(elem); list.appendChild(elem);
} }
@ -211,13 +206,13 @@ function showHistory(history: History, data: Layer.Data,
function closeHistory() { function closeHistory() {
document.getElementById('history-items')?. document.getElementById('history-items')?.
scroll({ top: 0, left: 0, behavior: 'smooth' }); scroll({ top: 0, left: 0, behavior: 'smooth' });
let field = document.getElementById('history-close-target'); const field = document.getElementById('history-close-target');
if (field) field.parentElement?.removeChild(field); if (field) field.parentElement?.removeChild(field);
document.documentElement.dataset.state = 'ready'; document.documentElement.dataset.state = 'ready';
} }
function download(seed: string) { function download(seed: string) {
const colors = Color.toRgbs(Color.colors(new Color.Rand(seed))); const colors = Color.colors(new Color.Rand(seed));
const blob = Palette.make(seed, colors); const blob = Palette.make(seed, colors);
// there must be a better way to push out a file than // there must be a better way to push out a file than
@ -233,14 +228,15 @@ function download(seed: string) {
async function setup() { async function setup() {
message('loading layers…'); message('loading layers…');
let data = await Layer.loadData().catch(e => { message(e, 30); throw e }); const aux = getCanvasCtx('aux');
let history = History.load();
let buf = Layer.makeBuffer(); 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 prevSeed = urlState() ?? new Color.Rand().alphaNum();
let seed = let seed =
await applyState(data, { seed: prevSeed, buf, history, firstLoad: true }); await applyState(data, { seed: prevSeed, history, firstLoad: true });
let side: Layer.Side = 'front'; let side: Layer.Side = 'front';
const reroll = document.getElementById('reroll')!; const reroll = document.getElementById('reroll')!;
@ -248,6 +244,10 @@ async function setup() {
addListeners(); addListeners();
function asyncHandler(h: (e: Event) => Promise<void>): (e: Event) => void {
return (e: Event) => void h(e);
}
// these ones don't need to be toggled // these ones don't need to be toggled
document.getElementById('hideui')?.addEventListener('click', () => { document.getElementById('hideui')?.addEventListener('click', () => {
document.documentElement.dataset.state = 'fullquox'; document.documentElement.dataset.state = 'fullquox';
@ -258,38 +258,38 @@ async function setup() {
document.getElementById('history-button')?.addEventListener('click', () => { document.getElementById('history-button')?.addEventListener('click', () => {
// does this need the add/remove listeners dance // does this need the add/remove listeners dance
// actually does anything any more? // actually does anything any more?
showHistory(history, data, { side, buf }); showHistory(history, data, { side });
}); });
document.getElementById('close-history')?.addEventListener('click', closeHistory); document.getElementById('close-history')?.addEventListener('click', closeHistory);
document.getElementById('current-name')?.addEventListener('focusout', async e => { document.getElementById('current-name')?.addEventListener('focusout', asyncHandler(async e => {
const space = String.raw`(\n|\s|<br>|&nbsp;)`; const space = String.raw`(\n|\s|<br>|&nbsp;)`;
const re = new RegExp(`^${space}+|${space}+$`, 'msgu'); const re = new RegExp(`^${space}+|${space}+$`, 'msgu');
let elem = e.target as HTMLElement; const elem = e.target as HTMLElement;
let str = elem.innerText.replaceAll(re, ''); let str = elem.innerText.replaceAll(re, '');
if (!str) str = new Color.Rand().alphaNum(); if (!str) str = new Color.Rand().alphaNum();
elem.innerText = str; elem.innerText = str;
// todo allow images cos it's funny // todo allow images cos it's funny
prevSeed = seed; prevSeed = seed;
seed = await applyState(data, { side, seed: str, buf, history }); seed = await applyState(data, { side, seed: str, history });
}); }));
document.getElementById('download-button')?.addEventListener('click', () => { document.getElementById('download-button')?.addEventListener('click', () => {
download(prevSeed); download(prevSeed);
}); });
document.documentElement.dataset.state = 'ready'; document.documentElement.dataset.state = 'ready';
async function run(task: (k: Done) => Promise<void>): Promise<void> { function run(task: (k: Done) => Promise<void>): void {
removeListeners(); removeListeners();
await task(addListeners); void task(addListeners);
} }
function updateFromUrl() { function updateFromUrl() {
run(async k => { run(async k => {
const newSeed = urlState(); const newSeed = urlState();
if (newSeed) { if (newSeed) {
const opts = { history, side, seed: newSeed, buf, done: k }; const opts = { history, side, seed: newSeed, done: k };
prevSeed = seed; prevSeed = seed;
seed = await applyState(data, opts); seed = await applyState(data, opts);
} }
@ -298,14 +298,14 @@ async function setup() {
function runReroll() { function runReroll() {
run(async k => { run(async k => {
prevSeed = seed; prevSeed = seed;
seed = await applyState(data, { side, seed, buf, history, done: k }); seed = await applyState(data, { side, seed, history, done: k });
}); });
} }
function runSwap() { function runSwap() {
run(async k => { run(async k => {
side = Layer.swapSide(side); side = Layer.swapSide(side);
const cdata = await Layer.ensureComposed(buf, data); const cdata = await Layer.ensureComposed(aux, data);
await animateSwapImage(buf, side, cdata, k); await animateSwapImage(side, cdata, k);
}); });
} }
@ -321,4 +321,4 @@ async function setup() {
} }
} }
document.addEventListener('DOMContentLoaded', setup); document.addEventListener('DOMContentLoaded', () => void setup());

View file

@ -73,7 +73,7 @@ export class Rand implements Randy {
h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233); h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213); h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179); h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1; h1 ^= (h2 ^ h3 ^ h4); h2 ^= h1; h3 ^= h1; h4 ^= h1;
return [h1>>>0, h2>>>0, h3>>>0, h4>>>0]; return [h1>>>0, h2>>>0, h3>>>0, h4>>>0];
} }
@ -112,7 +112,7 @@ export class Rand implements Randy {
#next(): number { #next(): number {
this.#a |= 0; this.#b |= 0; this.#c |= 0; this.#d |= 0; this.#a |= 0; this.#b |= 0; this.#c |= 0; this.#d |= 0;
let t = (this.#a + this.#b | 0) + this.#d | 0; const t = (this.#a + this.#b | 0) + this.#d | 0;
this.#d = this.#d + 1 | 0; this.#d = this.#d + 1 | 0;
this.#a = this.#b ^ this.#b >>> 9; this.#a = this.#b ^ this.#b >>> 9;
this.#b = this.#c + (this.#c << 3) | 0; this.#b = this.#c + (this.#c << 3) | 0;

View file

@ -205,6 +205,7 @@ $button-fg: oklch(0.98 0.1 var(--c-hue));
@include history-box; @include history-box;
position: absolute; position: absolute;
top: 0; left: 0; top: 0; left: 0;
font-size: 150%;
display: grid; display: grid;
grid-template: "a c" "b c" / auto min-content; grid-template: "a c" "b c" / auto min-content;
gap: 0 15px; gap: 0 15px;
@ -281,7 +282,7 @@ $button-fg: oklch(0.98 0.1 var(--c-hue));
@layer layering { @layer layering {
#main { z-index: 0; } #main { z-index: 0; }
#aux { z-index: 1; } #aux { z-index: 1; }
#buttons, #palette-holder, #back, #showui { z-index: 2; } #buttons, #palette-holder, #back, #showui, #download-button { z-index: 2; }
#history-close-target { z-index: 3; } #history-close-target { z-index: 3; }
#history { z-index: 4; } #history { z-index: 4; }
} }

View file

@ -1,3 +1,5 @@
import { shuffle } from './shuffle.js';
/** one of the six document sections */ /** one of the six document sections */
export type Pane = 'hello' | 'id' | 'activities' | 'links' | 'friends' | 'six'; export type Pane = 'hello' | 'id' | 'activities' | 'links' | 'friends' | 'six';
@ -9,6 +11,9 @@ namespace Cube {
/** location on the cube in space */ /** location on the cube in space */
export type Face = 'front' | 'top' | 'back' | 'bottom' | 'left' | 'right'; export type Face = 'front' | 'top' | 'back' | 'bottom' | 'left' | 'right';
export const allFaces: Face[] =
['front', 'top', 'back', 'bottom', 'left', 'right'];
/** /**
* - for front, left, right: up is up * - for front, left, right: up is up
* - for back: up is down (lol) * - for back: up is down (lol)
@ -17,6 +22,8 @@ export type Face = 'front' | 'top' | 'back' | 'bottom' | 'left' | 'right';
*/ */
export type Orientation = 'up' | 'left' | 'down' | 'right'; export type Orientation = 'up' | 'left' | 'down' | 'right';
export const allOrientations: Orientation[] = ['up', 'left', 'down', 'right'];
export type Place = [Face, Orientation]; export type Place = [Face, Orientation];
@ -24,6 +31,8 @@ function table<A extends string, B = A>(m: Record<A, B>): (x: A) => B {
return x => m[x]; return x => m[x];
} }
const NO_TRANS = 'rotateY(0deg)';
const doCwO = const doCwO =
table<Orientation>({up: 'right', right: 'down', down: 'left', left: 'up'}); table<Orientation>({up: 'right', right: 'down', down: 'left', left: 'up'});
@ -108,18 +117,20 @@ export function applyMoves(p: Place, ms: Rotation[]): Place {
/** the sequence of movements to put this place on the front */ /** the sequence of movements to put this place on the front */
export function toFrontUpright([f, o]: Place): [RotateXY[], RotateZ[]] { export function toFrontUpright([f, o]: Place): [RotateXY[], RotateZ[]] {
const toFront: (f: Face) => RotateXY[] = function toFront(f: Face, o: Orientation): RotateXY[] {
table<Face, RotateXY[]>({ return table<Face, RotateXY[]>({
front: [], top: ['down'], back: ['left', 'left'], front: [], top: ['down'],
back: o == 'up' ? ['down', 'down'] : ['left', 'left'],
bottom: ['up'], left: ['right'], right: ['left'] bottom: ['up'], left: ['right'], right: ['left']
}); })(f);
}
const toUpright: (o: Orientation) => RotateZ[] = const toUpright: (o: Orientation) => RotateZ[] =
table<Orientation, RotateZ[]>({ table<Orientation, RotateZ[]>({
up: [], left: ['cw'], down: ['cw', 'cw'], right: ['ccw'] up: [], left: ['cw'], down: ['cw', 'cw'], right: ['ccw']
}); });
const directions = toFront(f); const directions = toFront(f, o);
const rotations = toUpright(applyMoves([f, o], directions)[1]); const rotations = toUpright(applyMoves([f, o], directions)[1]);
return [directions, rotations]; return [directions, rotations];
} }
@ -133,7 +144,7 @@ const movementToTransform = table<Rotation, string>({
/** the css `transform` value corresponding to this sequence of movements */ /** the css `transform` value corresponding to this sequence of movements */
export function movementsToTransform(ms: Rotation[]): string { export function movementsToTransform(ms: Rotation[]): string {
return ms.length > 0 ? ms.map(movementToTransform).join(' ') : 'none'; return ms.length > 0 ? ms.map(movementToTransform).join(' ') : NO_TRANS;
} }
@ -155,7 +166,7 @@ const orientationToTransform = table<Orientation, string>({
export function placeToTransform([f, o]: Place): string { export function placeToTransform([f, o]: Place): string {
const ft = faceToTransform(f); const ft = faceToTransform(f);
const ot = orientationToTransform(o); const ot = orientationToTransform(o);
return ft || ot ? `${ft} ${ot}` : 'none'; return ft || ot ? `${ft} ${ot}` : NO_TRANS;
} }
@ -171,6 +182,19 @@ let current: Conf = {
// the back face is 'down' so it has the same visual orientation as other side // the back face is 'down' so it has the same visual orientation as other side
// faces // faces
export function randomConf(): Conf {
let faces = shuffle(allFaces);
function ori() { return allOrientations[Math.floor(Math.random() * 4)]!; }
let c: Partial<Conf> = {};
for (let i = 0; i < allPanes.length; ++i) {
c[allPanes[i]!] = [faces[i]!, ori()];
}
return c as Conf;
}
current = randomConf();
/** apply the css transforms to each pane element */ /** apply the css transforms to each pane element */
export function applyConfiguration(): void { export function applyConfiguration(): void {
for (const pane of allPanes) { for (const pane of allPanes) {
@ -183,7 +207,7 @@ export function applyConfiguration(): void {
} }
export function move(c: Conf, ...ms: Rotation[]): Conf { export function move(c: Conf, ...ms: Rotation[]): Conf {
let res: Partial<Conf> = {}; const res: Partial<Conf> = {};
for (const pane of allPanes) { res[pane] = applyMoves(c[pane], ms) } for (const pane of allPanes) { res[pane] = applyMoves(c[pane], ms) }
return res as Conf; return res as Conf;
} }
@ -222,7 +246,7 @@ export function animateMoveWith(ds: RotateXY[], rs: RotateZ[]): void {
delete cube.dataset.moving; delete cube.dataset.moving;
outer.style.transition = cube.style.transition = 'none'; outer.style.transition = cube.style.transition = 'none';
outer.style.transform = cube.style.transform = 'none'; outer.style.transform = cube.style.transform = NO_TRANS;
current = move(current, ...ds, ...rs); current = move(current, ...ds, ...rs);
applyConfiguration(); applyConfiguration();
@ -237,7 +261,7 @@ export function animateMoveTo(pane: Pane): void {
export function squashCube() { export function squashCube() {
for (const pane of allPanes) { for (const pane of allPanes) {
const elem = document.getElementById(pane)! const elem = document.getElementById(pane)!
elem.style.setProperty('--base-transform', 'none'); elem.style.setProperty('--base-transform', NO_TRANS);
} }
} }
@ -289,8 +313,7 @@ export function fadeTo(newPane: Pane): void {
} }
const reducedMotion =
let reducedMotion =
matchMedia(`(prefers-reduced-motion: reduce), matchMedia(`(prefers-reduced-motion: reduce),
(max-height: 649px), (max-width: 649px)`); (max-height: 649px), (max-width: 649px)`);
@ -301,7 +324,7 @@ function switchTo(pane: Pane): void {
} }
function setup(): void { export function setup(): void {
const here = location.hash.slice(1) || 'hello'; const here = location.hash.slice(1) || 'hello';
for (const pane of allPanes) { for (const pane of allPanes) {
@ -320,7 +343,3 @@ function setup(): void {
} }
} }
} }
document.addEventListener('DOMContentLoaded', setup);
export {};

7
script/cube_setup.ts Normal file
View file

@ -0,0 +1,7 @@
import * as Cube from './cube.js';
import * as Shuffle from './shuffle.js';
document.addEventListener('DOMContentLoaded', () => {
Cube.setup();
Shuffle.setup();
});

View file

@ -1,5 +1,5 @@
function shuffle<A>(subject: A[]): A[] { export function shuffle<A>(subject: A[]): A[] {
let res = Array.from(subject); const res = Array.from(subject);
for (let i = 0; i < res.length - 1; ++i) { for (let i = 0; i < res.length - 1; ++i) {
const j = i + Math.floor(Math.random() * (res.length - i)); const j = i + Math.floor(Math.random() * (res.length - i));
@ -16,16 +16,16 @@ function shuffle<A>(subject: A[]): A[] {
function group<A>(subject: A[], keepTogether: A[][]): A[][] { function group<A>(subject: A[], keepTogether: A[][]): A[][] {
type Value = {array: A[], added: boolean}; type Value = {array: A[], added: boolean};
let groups: Map<A, Value> = new Map; const groups: Map<A, Value> = new Map;
for (const xs of keepTogether) { for (const xs of keepTogether) {
let value = {array: xs, added: false}; const value = {array: xs, added: false};
for (const x of xs) { groups.set(x, value); } for (const x of xs) { groups.set(x, value); }
} }
let res = []; const res = [];
for (const x of subject) { for (const x of subject) {
let group = groups.get(x); const group = groups.get(x);
if (group?.added) { continue; } if (group?.added) { continue; }
else if (group) { else if (group) {
group.added = true; group.added = true;
@ -38,15 +38,15 @@ function group<A>(subject: A[], keepTogether: A[][]): A[][] {
return res; return res;
} }
function groupedShuffle<A>(subject: A[], keepTogether: A[][]): A[] { export function groupedShuffle<A>(subject: A[], keepTogether: A[][]): A[] {
return shuffle(group(subject, keepTogether)).flat(); return shuffle(group(subject, keepTogether)).flat();
} }
function shuffleAll() { export function setup() {
let groups = [group('myno', 'abyss'), group('clip', 'cervine')]; const groups = [group('myno', 'abyss'), group('clip', 'cervine')];
for (const elem of Array.from(document.getElementsByClassName('shuffle'))) { for (const elem of Array.from(document.getElementsByClassName('shuffle'))) {
let shuffled = groupedShuffle(Array.from(elem.children), groups); const shuffled = groupedShuffle(Array.from(elem.children), groups);
elem.innerHTML = ''; elem.innerHTML = '';
for (const child of shuffled) { for (const child of shuffled) {
@ -55,11 +55,7 @@ function shuffleAll() {
} }
function group(...xs: string[]) { function group(...xs: string[]) {
let elements = xs.map(x => document.getElementById(x)); const elements = xs.map(x => document.getElementById(x));
return elements.every(x => x) ? elements as HTMLElement[] : []; return elements.every(x => x) ? elements as HTMLElement[] : [];
} }
} }
document.addEventListener('DOMContentLoaded', shuffleAll);
export {};

View file

@ -1,23 +1,33 @@
@import url(../fonts/muller/muller.css); @layer outer, face-menu, cube, face-base, faces;
@import url(../fonts/muller/muller.css) layer(outer);
@import url(properties.css);
/*
@import url(../fonts/muller/muller.css) layer(outer)
(not (prefers-reduced-data: reduce));
if a browser doesn't understand the media query it DOESN'T do
the import. so this doesn't work until prefers-reduced-data
actually exists
*/
/* OUTER */ @layer outer {
* { box-sizing: border-box; }
* { box-sizing: border-box; } :root, body {
html, body {
min-height: 100vh; min-height: 100vh;
min-height: 100lvh; min-height: 100lvh;
margin: 0;
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }
html { :root {
font-size: large; font-size: large;
font-family: Muller, sans-serif;
font-weight: 500;
color-scheme: light dark;
color: black; color: black;
--gradient: --gradient:
linear-gradient(120deg in oklch, linear-gradient(120deg in oklch,
oklch(93% 27.1% 96deg), oklch(93% 27.1% 96deg),
@ -25,24 +35,10 @@ html {
oklch(80% 29.3% 303deg), oklch(80% 29.3% 303deg),
oklch(84% 23% 233deg), oklch(84% 23% 233deg),
oklch(89% 25% 161deg)); oklch(89% 25% 161deg));
--shadow-hsl: 330deg 40% 40%;
--base-background: var(--gradient) fixed;
background: var(--base-background);
--menu-bg-hsl: 60deg 100% 96%; --menu-bg-hsl: 60deg 100% 96%;
}
@media (prefers-reduced-data: reduce) { @media (prefers-color-scheme: dark) {
html { color: #ffd;
font-family: sans-serif;
font-weight: normal;
}
}
html, body { margin: 0; }
@media (prefers-color-scheme: dark) {
html {
--gradient: --gradient:
linear-gradient(20deg, linear-gradient(20deg,
hsl(300deg 30% 20%), hsl(300deg 30% 20%),
@ -51,23 +47,30 @@ html, body { margin: 0; }
hsl(30deg 30% 20%), hsl(30deg 30% 20%),
hsl(350deg 30% 20%)); hsl(350deg 30% 20%));
--menu-bg-hsl: 260deg 100% 8%; --menu-bg-hsl: 260deg 100% 8%;
color: #ffd; }
--shadow-hsl: 330deg 40% 40%;
--base-background: var(--gradient) fixed;
background: var(--base-background);
font-family: Muller, sans-serif;
font-weight: 500;
@media (prefers-reduced-data: reduce) {
font-family: sans-serif;
font-weight: normal;
}
} }
} }
/* TOP MENU */ @layer face-menu {
#face-menu {
#face-menu {
align-self: center; align-self: center;
margin: 10px; margin: 10px;
}
.menu input, .menu label { font-size: 125%;
cursor: pointer;
}
.menu { menu {
display: flex; display: flex;
place-content: center; place-content: center;
place-items: center; place-items: center;
@ -77,38 +80,42 @@ html, body { margin: 0; }
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
font-size: 125%;
background: hsl(var(--menu-bg-hsl) / 65%); background: hsl(var(--menu-bg-hsl) / 65%);
border: 2px solid hsl(var(--menu-bg-hsl)); border: 2px solid hsl(var(--menu-bg-hsl));
box-shadow: 0 0 10px 5px hsl(var(--menu-bg-hsl) / 30%); box-shadow: 0 0 10px 5px hsl(var(--menu-bg-hsl) / 30%);
} border-radius: 10px;
@media (prefers-reduced-transparency: reduce) { @media (prefers-reduced-transparency: reduce) {
background: hsl(var(--menu-bg-hsl)); background: hsl(var(--menu-bg-hsl));
} }
}
.hide-boxes input { input, label { cursor: pointer; }
input {
appearance: none; appearance: none;
width: 0; width: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.menu li {
display: flex; label {
list-style: none;
flex: 1 0 4em;
}
.menu label {
padding: .25em 1.25em; padding: .25em 1.25em;
flex: 1 0 auto; flex: 1 0 auto;
text-align: center; text-align: center;
}
li {
display: flex;
list-style: none;
flex: 1 0 4em;
}
}
} }
/* BASE FACE STYLES */ @layer face-base {
#cube > section {
#cube > section {
--base-background: --base-background:
repeating-linear-gradient(var(--bg-angle), repeating-linear-gradient(var(--bg-angle),
transparent, transparent 0.8em, transparent, transparent 0.8em,
@ -123,6 +130,8 @@ html, body { margin: 0; }
border: var(--border-thickness) solid white; border: var(--border-thickness) solid white;
padding: 2em; padding: 2em;
border-radius: 30px;
color: hsl(var(--hue) 40% 10%); color: hsl(var(--hue) 40% 10%);
--text-shadow-color: white; --text-shadow-color: white;
text-shadow: text-shadow:
@ -131,42 +140,7 @@ html, body { margin: 0; }
-1px -1px 1px var(--text-shadow-color), -1px -1px 1px var(--text-shadow-color),
1px -1px 1px var(--text-shadow-color); 1px -1px 1px var(--text-shadow-color);
scrollbar-color: @media (prefers-color-scheme: dark) {
hsl(calc(var(--hue) + 180deg) 90% 60%)
hsl(var(--hue) 50% 95%);
}
#cube a {
color: hsl(calc(var(--hue) + 180deg) 90% 20%);
font-weight: 600;
text-decoration: none;
}
#cube a:hover {
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-color: currentcolor;
text-decoration-thickness: 2px;
}
h2 { margin-top: 0; }
h3 {
text-align: center;
margin-bottom: 0.25em;
}
h2 + section h3 { margin-top: 0; }
strong { font-weight: 700; }
#cube section ::selection {
color: white;
background: hsl(calc(var(--hue) + 180deg), 50%, 30%);
text-shadow: none;
}
@media (prefers-color-scheme: dark) {
#cube > section {
--base-background: --base-background:
repeating-linear-gradient(var(--bg-angle), repeating-linear-gradient(var(--bg-angle),
transparent, transparent 0.8em, transparent, transparent 0.8em,
@ -182,123 +156,175 @@ strong { font-weight: 700; }
--text-shadow-color: black; --text-shadow-color: black;
} }
scrollbar-color:
hsl(calc(var(--hue) + 180deg) 90% 60%)
hsl(var(--hue) 50% 95%);
::selection {
color: white;
background: hsl(calc(var(--hue) + 180deg), 50%, 30%);
text-shadow: none;
}
}
#cube a { #cube a {
color: hsl(calc(var(--hue) + 180deg) 90% 20%);
font-weight: 600;
text-decoration: none;
&:hover {
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-color: currentcolor;
text-decoration-thickness: 2px;
}
@media (prefers-color-scheme: dark) {
color: hsl(calc(var(--hue) + 180deg) 100% 85%); color: hsl(calc(var(--hue) + 180deg) 100% 85%);
} }
}
h2 { margin-top: 0; }
h3 {
text-align: center;
margin-bottom: 0.25em;
}
h2 + section h3 { margin-top: 0; }
strong { font-weight: 700; }
.artcredit {
position: absolute;
bottom: 0;
left: 1em;
font-size: smaller;
font-style: italic;
background: hsl(var(--menu-bg-hsl) / 75%);
padding: .1em .5em;
}
ul { padding-left: 0; }
#cube :is(:not(.boxy) > li, .bullety dd) {
list-style: none;
&::before {
content: url(../media/point_right.svg);
vertical-align: -20%;
height: 1em;
padding-right: 0.5ex;
}
}
} }
@layer faces.hello {
/* SPECIFIC FACE STYLES */ #hello {
/* the separate "#whatever ::selection" selector is because in
* chrome, ::selection doesn't inherit variables */
#hello, #hello ::selection {
--hue: 310deg; --hue: 310deg;
--bg-angle: 135deg; --bg-angle: 135deg;
}
/* this one makes more sense to show if there is a paint before /* this one makes more sense to show if there is a paint before
* the script runs */ * the script runs */
#hello {
z-index: 1; z-index: 1;
}
/* extra #cube selector for specificity */ /* the quick linx */
#cube #hello { h3 { text-align: left; }
--bg-image: url(../media/wave.webp); --bg-image: url(../media/wave.webp);
background: background:
var(--bg-image) bottom right / auto 60% no-repeat, var(--bg-image) bottom right / auto 60% no-repeat,
var(--base-background); var(--base-background);
@media (prefers-color-scheme: dark) {
--bg-image: url(../media/wave-neon.webp);
}
@media (prefers-reduced-data: reduce) {
--bg-image: url(../media/wave.l.webp);
}
@media (prefers-color-scheme: dark) and (prefers-reduced-data: reduce) {
--bg-image: url(../media/wave-neon.l.webp);
}
}
} }
@media (prefers-color-scheme: dark) { @layer faces.id {
#cube #hello { --bg-image: url(../media/wave-neon.webp); } #id {
}
@media (prefers-reduced-data: reduce) {
#cube #hello { --bg-image: url(../media/wave.l.webp); }
}
@media (prefers-color-scheme: dark) and (prefers-reduced-data: reduce) {
#cube #hello { --bg-image: url(../media/wave-neon.l.webp); }
}
#id, #id ::selection, #flags img {
--hue: 10deg; --hue: 10deg;
--bg-angle: 45deg; --bg-angle: 45deg;
}
#id dl { dl {
display: grid; display: grid;
grid-template-columns: min-content auto; grid-template-columns: min-content auto;
gap: 1em 2em; gap: 0 2em;
} }
#id dt { font-weight: 600; } dt { font-weight: 600; }
#id dd { margin: 0; } dd {
#id dd a { font-weight: 700; } margin: 0 0 0 1em;
grid-column: 2 / 3;
a { font-weight: 700; }
}
dt, dt + dd { margin-top: 1em; }
}
#id dl ul { #flags {
padding-left: 1em;
list-style: '❧ ';
}
#flags {
display: flex; display: flex;
flex-flow: wrap; flex-flow: wrap;
justify-content: center; justify-content: center;
gap: 1em; gap: 1em;
margin-top: 3em; margin-top: 3em;
}
#flags img { img {
height: 2em; height: 2em;
border: 2px solid hsl(var(--hue) 30% 5% / 60%); border: 2px solid hsl(var(--hue) 30% 5% / 60%);
rotate: 8deg; rotate: 8deg;
}
}
} }
@layer faces.activities {
#activities, #activities ::selection { #activities {
--hue: 90deg; --hue: 60deg;
--bg-angle: -60deg; --bg-angle: -60deg;
}
#cube #activities {
/* height of quobl.webp is 58% of width */ /* height of quobl.webp is 58% of width */
--bg-image: url(../media/quobl.webp); --bg-image: url(../media/quobl.webp);
background: background:
var(--bg-image) bottom left 2ex / 50% auto no-repeat local, var(--bg-image) bottom left 2ex / 50% auto no-repeat local,
var(--base-background); var(--base-background);
padding-bottom: 29%; padding-bottom: 29%;
@media (prefers-color-scheme: dark) {
--bg-image: url(../media/quobl-neon.webp);
}
@media (prefers-reduced-data: reduce) {
--bg-image: url(../media/quobl.l.webp);
}
@media (prefers-color-scheme: dark) and (prefers-reduced-data: reduce) {
--bg-image: url(../media/quobl-neon.webp);
}
}
} }
@media (prefers-color-scheme: dark) {
#cube #activities { --bg-image: url(../media/quobl-neon.webp); }
}
@media (prefers-reduced-data: reduce) {
#cube #activities { --bg-image: url(../media/quobl.l.webp); }
}
@media (prefers-color-scheme: dark) and (prefers-reduced-data: reduce) {
#cube #activities { --bg-image: url(../media/quobl-neon.webp); }
}
#links, #links ::selection { @layer faces.links {
#links {
--hue: 130deg; --hue: 130deg;
--bg-angle: 210deg; --bg-angle: 210deg;
}
#links ul { ul {
font-size: 125%;
padding: 0; padding: 0;
width: 100%; width: 100%;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 0.5ex;
} }
#links li { li {
flex: 1 0 10em; flex: 1 0 40%;
list-style: none; list-style: none;
--icon-bg: var(--fg); }
}
#links a { a {
height: 3em; height: 3em;
padding-left: calc(3em + 1ex); padding-left: calc(3em + 1ex);
display: flex; display: flex;
@ -307,145 +333,140 @@ strong { font-weight: 700; }
border: 2px solid black; border: 2px solid black;
background: background:
linear-gradient(to right, transparent 3em, var(--bg) 3em), linear-gradient(to right, transparent 3em, var(--bg) 3em),
var(--icon) calc(1.5em - 16px) center / 32px auto no-repeat, var(--icon) 0.5em center / 2em auto no-repeat,
var(--icon-bg); var(--icon-bg);
border-radius: 10px;
color: var(--fg); color: var(--fg);
text-shadow: none; text-shadow: none;
} &:hover { text-decoration: none; }
}
}
#links a:hover { text-decoration: none; } /* (overridable default) */
@layer { #links li { --icon-bg: var(--fg); } }
#links #gallery { #gallery {
--icon: url(../media/favicon.webp); --icon: url(../media/favicon.webp);
--fg: hsl(280deg 38% 43%); --fg: hsl(280deg 38% 43%);
--bg: hsl(100deg 99% 81%); --bg: hsl(100deg 99% 81%);
image-rendering: pixelated; image-rendering: pixelated;
} }
#links #code { #code {
--icon: url(../media/icons/forgejo.svg); --icon: url(../media/icons/forgejo.svg);
--fg: white; --fg: white;
--icon-bg: #171e26; --icon-bg: #171e26;
--bg: #c2410c; --bg: #c2410c;
} }
#links #blog { #blog {
--icon: url(../media/icons/blog.webp); --icon: url(../media/icons/blog.webp);
--fg: #ffeebb; --fg: #ffeebb;
--bg: #332255; --bg: #332255;
/* image-rendering: pixelated; */ image-rendering: pixelated;
} a {
#links #blog a {
background-position: center, left calc(1.5em - 51px) center, center; background-position: center, left calc(1.5em - 51px) center, center;
background-size: contain; background-size: contain;
} }
}
#links #itaku { #itaku {
--icon: url(../media/icons/itaku.svg); --icon: url(../media/icons/itaku.svg);
--fg: #ffeb3b; --fg: #ffeb3b;
--bg: #303030; --bg: #303030;
} }
#links #weasyl { #weasyl {
--icon: url(../media/icons/weasyl.svg); --icon: url(../media/icons/weasyl.svg);
--fg: white; --fg: white;
--bg: #970000; --bg: #970000;
} }
#links #fa { #fa {
--icon: url(../media/icons/furaffinity.webp); --icon: url(../media/icons/furaffinity.webp);
--icon-bg: #20242a; --icon-bg: #20242a;
--bg: #353b45; --bg: #353b45;
--fg: white; --fg: white;
} }
#links #da { #da {
--icon: url(../media/icons/deviantart.webp); --icon: url(../media/icons/deviantart.webp);
--icon-bg: #000608; --icon-bg: #000608;
--bg: #314537; --bg: #314537;
--fg: #e7ede4; --fg: #e7ede4;
} }
#links #kofi { #kofi {
--icon: url(../media/icons/ko-fi.webp); --icon: url(../media/icons/ko-fi.webp);
--icon-bg: #def3ff; --icon-bg: #def3ff;
--bg: white; --bg: white;
--fg: #ff5966; --fg: #ff5966;
} }
#links #artfight { #artfight {
--icon: url(../media/icons/artfight.webp); --icon: url(../media/icons/artfight.webp);
--icon-bg: #222222; --icon-bg: #222222;
--bg: #a65178; --bg: #a65178;
--fg: white; --fg: white;
} }
#links #chitter { #chitter {
--icon: url(../media/icons/chitter.webp); --icon: url(../media/icons/chitter.webp);
--bg: #582c58; --bg: #582c58;
--icon-bg: #2c162c; --icon-bg: #2c162c;
--fg: white; --fg: white;
} }
#links #cohost { #bluesky {
--icon: url(../media/icons/cohost.svg);
--bg: #ffab5c;
--icon-bg: #83254f;
--fg: black;
}
#links #bluesky {
--icon: url(../media/icons/bluesky.svg); --icon: url(../media/icons/bluesky.svg);
--bg: #161e27; --bg: #161e27;
--fg: white; --fg: white;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
/* most link colours are fine in both modes. except: */ /* most link colours are fine in both modes. except: */
#gallery {
#links #gallery {
--icon-bg: hsl(280deg 42% 30%); --icon-bg: hsl(280deg 42% 30%);
--bg: hsl(280deg 38% 43%); --bg: hsl(280deg 38% 43%);
--fg: hsl(100deg 99% 81%); --fg: hsl(100deg 99% 81%);
} }
#links #weasyl { #weasyl {
--icon-bg: #252d32; --icon-bg: #252d32;
} }
#links #kofi { #kofi {
--icon-bg: #2b3a44; --icon-bg: #2b3a44;
--bg: #192025; --bg: #192025;
--fg: #dce7eb; --fg: #dce7eb;
} }
#links #bluesky { #bluesky {
--bg: #161e27; --bg: #161e27;
--icon-bg: #1e2936; --icon-bg: #1e2936;
--fg: white; --fg: white;
} }
}
} }
@layer faces.friends {
#friends, #friends ::selection { #friends {
--hue: 190deg; --hue: 190deg;
--bg-angle: 300deg; --bg-angle: 300deg;
}
#cube #friends {
display: grid; display: grid;
grid: "hdr" min-content grid: "hdr" min-content
"links1" auto "links1" auto
"links2" auto "links2" auto
"buttons" auto; "buttons" auto;
}
#friends img { image-rendering: pixelated; } img { image-rendering: pixelated; }
#friendlinks, #otherlinks { align-self: start; } > section {
align-self: start;
#friendlinks ul, #otherlinks ul { ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
@ -454,18 +475,20 @@ strong { font-weight: 700; }
margin: 0 auto; margin: 0 auto;
align-items: start; align-items: start;
gap: 4px; gap: 0;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
} }
#friendlinks img, #otherlinks img { img {
width: 88px; width: 88px;
height: 31px; height: 31px;
object-fit: none; object-fit: none;
} display: block;
}
}
#friends .txt { .txt {
display: inline-block; display: inline-block;
width: 88px; width: 88px;
height: 31px; height: 31px;
@ -476,68 +499,60 @@ strong { font-weight: 700; }
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
text-shadow: none; text-shadow: none;
} }
#friends a:hover { text-decoration: none; } a:hover { text-decoration: none; }
}
#friends #khr a { background: #ffab71; color: #71153e; } #khr a { background: #ffab71; color: #71153e; }
#friends #ionchy a { background: #feca2f; color: #1b1505; } #ionchy a { background: #feca2f; color: #1b1505; }
#friends #tenna a { background: #6095da; color: #243224; } #tenna a { background: #6095da; color: #243224; }
#friends #river a { background: #98d8e7; color: #d67d28; } #river a { background: #98d8e7; color: #d67d28; }
#friends #spiral a { background: #ef4d5a; color: #1f1f1f; } #spiral a { background: #ef4d5a; color: #1f1f1f; }
#friends #codl a { background: #87261f; color: #edb970; } #codl a { background: #87261f; color: #edb970; }
#friends #violet a { background: #8c2bd8; color: #dddddd; } #violet a { background: #8c2bd8; color: #dddddd; }
#friends #brin a { background: #1e1e1e; color: #ff4400; } #brin a { background: #1e1e1e; color: #ff4400; }
#friends #konsti a { background: #060038; color: #ffcccc; } #konsti a { background: #060038; color: #ffcccc; }
#friends #lena a { background: #e3ccf7; color: #000000; } #lena a { background: #e3ccf7; color: #000000; }
#friends #serena a { background: #e787ad; color: #204; } #serena a { background: #e787ad; color: #204; }
#friends #deneb a { background: #540f00; color: #ee6bfa; } #deneb a { background: #540f00; color: #ee6bfa; }
#nissbuttons { #nissbuttons {
margin: 2em auto 0; margin: 2em auto 0;
align-self: end; align-self: end;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 1em; gap: 1em;
}
#nissbuttons h3 { h3 {
margin: 0;
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
text-align: right; text-align: right;
text-wrap: balance; text-wrap: balance;
}
}
} }
#nissbuttons * { margin: 0; }
#six, #six ::selection { @layer faces.six {
#six, #six ::selection {
--hue: 250deg; --hue: 250deg;
--bg-angle: 130deg; --bg-angle: 130deg;
}
#cube #six {
--bg-image: url(../media/kesi.webp); --bg-image: url(../media/kesi.webp);
background: background:
var(--bg-image) bottom right / 100% auto no-repeat, var(--bg-image) bottom right / 100% auto no-repeat,
var(--base-background); var(--base-background);
}
.artcredit { @media (prefers-color-scheme: dark) {
position: absolute; --bg-image: url(../media/kesi-neon.webp);
bottom: 0; }
left: 1em; @media (prefers-reduced-data: reduce) {
font-size: smaller; --bg-image: url(../media/kesi.l.webp);
font-style: italic; }
background: hsl(var(--menu-bg-hsl) / 75%); @media (prefers-color-scheme: dark) and (prefers-reduced-data: reduce) {
padding: .1em .5em; --bg-image: url(../media/kesi-neon.l.webp);
} }
}
@media (prefers-color-scheme: dark) {
#cube #six { --bg-image: url(../media/kesi-neon.webp); }
}
@media (prefers-reduced-data: reduce) {
#cube #activities { --bg-image: url(../media/kesi.l.webp); }
}
@media (prefers-color-scheme: dark) and (prefers-reduced-data: reduce) {
#cube #activities { --bg-image: url(../media/kesi-neon.l.webp); }
} }

View file

@ -1,9 +1,8 @@
@media (prefers-reduced-motion: no-preference) and @media (prefers-reduced-motion: no-preference) and
(min-height: 650px) and (min-width: 650px) { (min-height: 650px) and (min-width: 650px) {
/* BACKGROUND STUFF */ @layer outer {
:root {
html {
--bg-60309: url(../media/bg/60309.png); --bg-60309: url(../media/bg/60309.png);
--bg-kesi: url(../media/bg/kesi.png); --bg-kesi: url(../media/bg/kesi.png);
--bg-korai: url(../media/bg/korai.png); --bg-korai: url(../media/bg/korai.png);
@ -14,10 +13,8 @@ html {
--bg-prickly: url(../media/bg/prickly.png); --bg-prickly: url(../media/bg/prickly.png);
--bg-qt: url(../media/bg/qt.png); --bg-qt: url(../media/bg/qt.png);
--bg-qt2: url(../media/bg/qt2.png); --bg-qt2: url(../media/bg/qt2.png);
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html {
--bg-60309: url(../media/bg/60309_neon.png); --bg-60309: url(../media/bg/60309_neon.png);
--bg-kesi: url(../media/bg/kesi_neon.png); --bg-kesi: url(../media/bg/kesi_neon.png);
--bg-korai: url(../media/bg/korai_neon.png); --bg-korai: url(../media/bg/korai_neon.png);
@ -29,15 +26,14 @@ html {
--bg-qt: url(../media/bg/qt_neon.png); --bg-qt: url(../media/bg/qt_neon.png);
--bg-qt2: url(../media/bg/qt2_neon.png); --bg-qt2: url(../media/bg/qt2_neon.png);
} }
}
html { background-blend-mode: overlay;
@media (prefers-color-scheme: dark) {
background-blend-mode: soft-light;
}
background: background:
/*
url(../media/bg/kesi_sprite_front.png) top 100px right no-repeat,
url(../media/bg/kesi_sprite_mid.png) top 150px center repeat-x,
url(../media/bg/kesi_sprite_back.png) top 200px left no-repeat,
*/
var(--bg-60309) bottom 29% right calc(44% - var(--half)) var(--bg-60309) bottom 29% right calc(44% - var(--half))
/ auto 17% no-repeat, / auto 17% no-repeat,
var(--bg-kesi) bottom 23% left calc(44% - var(--half)) var(--bg-kesi) bottom 23% left calc(44% - var(--half))
@ -57,106 +53,96 @@ html {
var(--bg-prickly) bottom 9% right calc(28% - var(--half)) var(--bg-prickly) bottom 9% right calc(28% - var(--half))
/ auto 15% no-repeat, / auto 15% no-repeat,
var(--base-background); var(--base-background);
background-blend-mode: overlay;
}
@media (prefers-color-scheme: dark) { @media ((prefers-reduced-data: reduce),
html { (prefers-reduced-transparency: reduce)) {
background-blend-mode: soft-light;
}
}
@media (prefers-reduced-data: reduce) {
html {
background: var(--base-background); background: var(--base-background);
background-blend-mode: unset; background-blend-mode: initial;
}
} }
}
body {
/* LAYOUT BASICS */
body {
display: grid; display: grid;
grid-template-rows: 5em 1fr; grid-template-rows: 5em 1fr;
perspective: 440vw; perspective: 440vw;
perspective-origin: 50% 120%; perspective-origin: 50% 120%;
} }
/* MENU ANIMATION */ menu {
label {
.menu label { position: relative; } position: relative;
.menu label::after { &::after {
content: ' '; content: ' ';
z-index: -1; z-index: -1;
position: absolute; position: absolute;
inset: 0 0 100% 0; inset: 0 0 100% 0;
background: hsl(var(--menu-bg-hsl)); background: hsl(var(--menu-bg-hsl));
@media not (prefers-reduced-motion: reduce) {
transition: inset .15s linear; transition: inset .15s linear;
} }
.menu :checked + label::after { }
}
:checked + label::after {
bottom: 0; bottom: 0;
}
}
} }
@layer cube {
/* CUBE ASSEMBLY */ :root {
/* the rest is in cube.ts */
html {
--side: min(65vh, 70vw); --side: min(65vh, 70vw);
--half: calc(var(--side) * .5); --half: calc(var(--side) * .5);
--nhalf: calc(0em - var(--half)); --nhalf: calc(0em - var(--half));
--breathe: calc(var(--side) * .1); --breathe: calc(var(--side) * 0.02);
} }
#outer { #outer {
--transform-origin: 50% 50% calc(var(--nhalf) - min(6vh, 6vw)); --transform-origin: 50% 50% calc(var(--nhalf) - min(6vh, 6vw));
} }
#outer, #cube { #outer, #cube {
transform-style: preserve-3d; transform-style: preserve-3d;
transform-origin: var(--transform-origin); transform-origin: var(--transform-origin);
width: calc(var(--side) + 10vw); width: calc(var(--side) + 10vw);
height: calc(var(--side) + 10vh); height: calc(var(--side) + 10vh);
margin: auto; margin: auto;
} }
#cube { #cube {
position: relative; position: relative;
}
#cube > section { > section {
position: absolute; position: absolute;
inset: 5vh 5vw; inset: 5vh 5vw;
overflow: auto; overflow: auto;
transform-origin: var(--transform-origin); transform-origin: var(--transform-origin);
transform: var(--base-transform); transform: var(--base-transform);
overscroll-behavior: contain; overscroll-behavior: contain;
} }
@supports (scrollbar-color: pink orange) { /* safari detector */ &:not([data-moving]) > section:not([data-state=active]) {
#cube:not([data-moving]) > section { animation: breathe 7.5s infinite ease-in-out;
animation: breathe 9s infinite ease-in-out; }
} }
@keyframes breathe { @keyframes breathe {
35% { transform: translateZ(var(--breathe)) var(--base-transform); } 40% { transform: var(--base-transform) translateZ(var(--breathe)); }
} }
}
@media (prefers-reduced-motion: no-preference) { @media not (prefers-reduced-motion: reduce) {
.zoom * { transition: all 0.25s ease-in; } .zoom * { transition: all 0.25s ease-in; }
.zoom > :hover { .zoom > :hover {
scale: 110%; scale: 110%;
filter: drop-shadow(4px 4px 5px rgb(0 0 0 / 60%)); filter: drop-shadow(4px 4px 5px rgb(0 0 0 / 60%));
&:nth-child(5n) { rotate: 4deg; }
&:nth-child(5n+1) { rotate: -2deg; }
&:nth-child(5n+2) { rotate: 1deg; }
&:nth-child(5n+3) { rotate: -3deg; }
&:nth-child(10n+4) { rotate: 4deg; }
&:nth-child(10n+9) { rotate: -1deg; }
}
} }
.zoom > :hover:nth-child(5n) { rotate: 4deg; }
.zoom > :hover:nth-child(5n+1) { rotate: -2deg; }
.zoom > :hover:nth-child(5n+2) { rotate: 1deg; }
.zoom > :hover:nth-child(5n+3) { rotate: -3deg; }
.zoom > :hover:nth-child(10n+4) { rotate: 4deg; }
.zoom > :hover:nth-child(10n+9) { rotate: -1deg; }
}
} }

View file

@ -1,122 +1,120 @@
@media (prefers-reduced-motion: reduce), @media (prefers-reduced-motion: reduce),
(max-height: 649px), (max-width: 649px) { (max-height: 649px), (max-width: 649px) {
html { :root {
--side: 0px; --side: 0px;
--half: 0px; --half: 0px;
--nhalf: 0px; --nhalf: 0px;
} }
/* LAYOUT */ @layer outer {
body {
body {
display: grid; display: grid;
grid-template: "menu" 5em "body" 1fr; grid-template: "menu" 5em "body" 1fr;
/* height: 100vh; height: 100dvh; */ }
/* width: 100vw; width: 100dvw; */
}
#face-menu { grid-area: menu; } #face-menu { grid-area: menu; }
#outer { grid-area: body; }
#outer { #outer {
grid-area: body;
position: relative; position: relative;
height: 90%; height: 90%;
width: 90%; width: 90%;
margin: auto; margin: auto;
}
#outer::after { &::after {
content: ' '; content: ' ';
position: absolute; position: absolute;
inset: 0; inset: 0;
box-shadow: 0 0 40px hsl(var(--shadow-hsl) / 40%); box-shadow: 0 0 40px hsl(var(--shadow-hsl) / 40%);
mix-blend-mode: multiply; mix-blend-mode: multiply;
}
}
} }
/* not really a cube any more. but */ /* not really a cube any more. but */
#cube, #cube > section { @layer cube {
#cube, #cube > section {
position: absolute; position: absolute;
inset: 0; inset: 0;
} }
#cube > section { overflow: auto; } #cube > section { overflow: auto; }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html { --shadow-hsl: 60deg 100% 96%; } html { --shadow-hsl: 60deg 100% 96%; }
}
} }
/* BACKGROUND FIXES */ /* BACKGROUND FIXES */
@media (max-width: 649px) { @layer faces {
#cube #hello { @layer hello {
#hello {
@media (max-width: 649px) {
background-size: auto 80%, auto, auto; background-size: auto 80%, auto, auto;
background-position: bottom -20% right 60%, center, center; background-position: bottom -20% right 60%, center, center;
} }
}
@media (max-height: 649px) { @media (max-height: 649px) {
#cube #hello {
background-size: auto 80%, auto, auto; background-size: auto 80%, auto, auto;
background-position: bottom right, center, center; background-position: bottom right, center, center;
} }
} }
}
@layer activities {
@media (max-width: 649px) { #activities {
#cube #activities { @media (max-width: 649px) {
background-size: auto 30%, auto, auto; background-size: auto 30%, auto, auto;
background-position: bottom left 70%, center, center; background-position: bottom left 70%, center, center;
} }
} }
}
#cube #six { @layer six {
#six {
background-position: bottom left 70%, center, center; background-position: bottom left 70%, center, center;
}
@media (max-width: 649px) { @media (max-width: 649px) {
#cube #six {
background-size: auto 100%, auto, auto; background-size: auto 100%, auto, auto;
background-position: bottom left 70%, center, center; background-position: bottom left 70%, center, center;
} }
}
@media (max-height: 649px) { @media (max-height: 649px) {
#cube #six {
background-size: cover; background-size: cover;
background-position: top 15% center; background-position: top 15% center;
} }
}
}
} }
/* TRANSITIONS */ @layer cube {
#face-menu {
label { transition: background 0.1s ease-in; }
:checked + label { background: hsl(var(--menu-bg-hsl)); }
}
#face-menu :checked + label { #cube > section {
background: hsl(var(--menu-bg-hsl)); &[data-state=entering] {
}
#face-menu label {
transition: background 0.1s ease-in;
}
#cube > :is(section[data-state=entering], #a) {
z-index: 1; z-index: 1;
opacity: 0; opacity: 0;
} }
#cube > :is(section[data-state=active], #a) { &[data-state=active] {
z-index: 1; z-index: 1;
opacity: 1; opacity: 1;
transition: opacity 0.1s ease-in; transition: opacity 0.1s ease-in;
} }
#cube > :is(section[data-state=leaving], #a) { &[data-state=leaving] {
z-index: -1; z-index: -1;
opacity: 0; opacity: 0;
transition: opacity 0s 0.1s ease-in; transition: opacity 0s 0.1s ease-in;
} }
#cube > :is(section[data-state=hidden], #a) { &[data-state=hidden] {
display: none; display: none;
} }
}
} }

44
style/properties.css Normal file
View file

@ -0,0 +1,44 @@
/* face backgrounds */
@property --hue {
syntax: "<angle>";
inherits: true;
initial-value: 0deg;
}
@property --bg-angle {
syntax: "<angle>";
inherits: true;
initial-value: 45deg;
}
/* link buttons */
@property --icon {
syntax: "<url>";
inherits: true;
initial-value: url(../media/favicon.webp);
}
@property --fg {
syntax: "<color>";
inherits: true;
initial-value: white;
}
@property --bg {
syntax: "<color>";
inherits: true;
initial-value: black;
}
@property --icon-bg {
syntax: "<color>";
inherits: true;
initial-value: white;
}
/* the initial value can't be `var(--bg)` because only "computationally
* independent" values are allowed
* https://drafts.css-houdini.org/css-properties-values-api/#initial-value-descriptor
*/