diff --git a/Makefile b/Makefile index 243abe0..59126da 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,16 @@ -PAGES = index.html pubkey.txt rainbow-quox/index.html velzek/index.html +PAGES = index.html pubkey.txt rainbow-quox/index.html \ + dnd/index.html $(wildcard dnd/*/index.html) MEDIA = \ $(wildcard media/*.png) $(wildcard media/*.gif) $(wildcard media/*.webp) \ $(wildcard media/flags/*) $(wildcard media/buttons/*) \ $(wildcard media/icons/*) $(wildcard media/bg/*) 8831.png 8831-quox.png \ $(wildcard rainbow-quox/front/*) $(wildcard rainbow-quox/back/*) \ $(wildcard rainbow-quox/*.svg) rainbow-quox/palette.svg \ - $(wildcard velzek/*.webp) $(wildcard velzek/*.png) + $(wildcard dnd/*.png) $(wildcard dnd/*.webp) $(wildcard dnd/*/*.webp) CSS = $(shell find fonts -type f) \ $(patsubst %.scss,%.css, \ $(wildcard rainbow-quox/style/*) $(wildcard style/*)) \ - velzek/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)) MISC = $(shell find .well-known -type f) ALL = $(CSS) $(PAGES) $(MEDIA) $(SCRIPTS) $(MISC) diff --git a/velzek/style.css b/dnd/base.css similarity index 72% rename from velzek/style.css rename to dnd/base.css index cac4894..3de9e93 100644 --- a/velzek/style.css +++ b/dnd/base.css @@ -1,5 +1,5 @@ -@layer base, header, footer, images, switcher, phone; -@import url(../fonts/junicodevf/junicodevf.css) layer(base); +@layer fonts, base, header, footer, images, switcher, phone; +@import url(../fonts/junicodevf/junicodevf.css) layer(fonts); @layer base { * { box-sizing: border-box; } @@ -10,8 +10,8 @@ --text-color: hsl(350deg 40% 15%); --accent-color: hsl(80deg 50% 35%); --shadow-color: hsl(from var(--text-color) h 20% 3% / 20%); - --shadow: drop-shadow(3px 2px 0 var(--shadow-color)); - --shadow2: drop-shadow(-2px 0 0 hsl(from var(--text-color) h 20% 7% / 15%)); + --shadow: 3px 2px 0 var(--shadow-color); + --shadow2: -2px 0 0 hsl(from var(--text-color) h 20% 7% / 15%); } :root { @@ -34,17 +34,18 @@ background: url(beige-paper.png), hsl(40deg 80% 80%); background-blend-mode: multiply; border: 10px solid currentcolor; - filter: var(--shadow) var(--shadow2); + box-shadow: var(--shadow), var(--shadow2); } - figure { filter: var(--shadow2); } } @layer base.fonts { :root { font-family: JunicodeVF, serif; - font-feature-settings: - "ccmp", "calt", "liga", "loca", "rlig", "kern", "mark", "mkmk"; + --base-features: + "ccmp", "calt", "liga", "loca", "rlig", "kern", "mark", "mkmk", + "ss09", "cv69" 6; /* nice */ + font-feature-settings: var(--base-features); font-variation-settings: "ENLA" 25; font-stretch: 125%; font-weight: 450; @@ -72,32 +73,6 @@ } } -@layer base.headings { - h1, h2, h3, h4, h5, h6 { - font-stretch: 75%; - font-variation-settings: "ENLA" 0; - } - - h2 { - margin: 1em; - position: relative; - border-bottom: 3px double currentcolor; - - font-size: 225%; - font-weight: 500; - - &::before { - content: '•'; - font-size: 80%; - font-feature-settings: "ornm" 5; - position: absolute; - left: -1.15em; - bottom: 7%; - rotate: -5deg; - } - } -} - @layer base.other { a { color: inherit; @@ -111,19 +86,35 @@ font-style: italic; } - dl { margin: 0 1em; } - dt { font-weight: bold; } - dd { margin: 0; } + .amp { + font-size: 75%; + font-weight: 550; + } - @media (width >= 70rem) { - dl { - display: grid; - grid-template-columns: auto 1fr; - column-gap: 1em; + small { font-stretch: 100%; } +} + +@layer base.headings { + h1, h2, h3, h4, h5, h6 { + font-stretch: 75%; + font-feature-settings: var(--base-features), + "cv02" 1, "cv08" 1, "cv10" 1, "cv12" 10, "cv14" 6, "cv15" 4, "cv16" 1, + "cv24" 5, "cv38" 2, "cv48" 1; + font-variation-settings: "ENLA" 0; + + small { font-stretch: 65%; } + } + + h2 { + margin: 0 0 0.5rem; + font-size: 225%; + font-weight: 500; + + &::before { + content: '•'; + font-size: 80%; + font-feature-settings: "ornm" 2; } - - dt { grid-column-start: 1; } - dd { grid-column-start: 2; } } } @@ -173,7 +164,7 @@ } @layer images { - #velzek-img-holder { + #char-img-holder { display: grid; grid-template: "i"; align-items: center; @@ -184,37 +175,36 @@ margin: auto; } - img { - filter: var(--shadow2); + .mainfig { max-width: 100%; - &.bordered { border: 10px solid currentcolor; } } + .bordered { + box-shadow: var(--shadow), var(--shadow2); + border: 10px solid currentcolor; + } + #char-pic { filter: drop-shadow(var(--shadow)); } + + figure img { width: 100%; } @media (width >= 70rem) { - figure { + .mainfig { width: 480px; float: right; margin: 0 calc(0px - var(--protrude)) 1em 1em; img { width: 100%; } } - #velzek-pic { - shape-outside: polygon(100% 0%, 13% 0%, 13% 25%, 0% 27%, - 0% 51%, 18% 60%, 21% 100%, 100% 100%); - } - - #ekkel-pic { rotate: -2deg; } - #suveesha-pic { rotate: 3deg; } + section:nth-of-type(even) .mainfig { rotate: -1deg; } + section:nth-of-type(odd) .mainfig { rotate: 1.5deg; } + #char-pic { rotate: 0deg; } } } @layer switcher { - #velzek-pic img { - transition: all ease 200ms 175ms; - } + #pic1, #pic2 { transition: all ease 200ms 175ms; } - :root:has(#switch:checked) #clothes-pic, - :root:not(:has(#switch:checked)) #armour-pic { + :root:has(#switch:checked) #pic1, + :root:not(:has(#switch:checked)) #pic2 { opacity: 0; transition-delay: 0ms; pointer-events: none; @@ -225,7 +215,7 @@ position: absolute; } - #velzek-pic label { + #char-pic label { display: block; width: min-content; margin: 0 auto 1em; diff --git a/velzek/beige-paper.png b/dnd/beige-paper.png similarity index 100% rename from velzek/beige-paper.png rename to dnd/beige-paper.png diff --git a/dnd/bio.css b/dnd/bio.css new file mode 100644 index 0000000..39e00cb --- /dev/null +++ b/dnd/bio.css @@ -0,0 +1,38 @@ +@import url(base.css); + +@layer base.headings { + h2 { + margin: 2rem 3rem; + position: relative; + + border-bottom: 3px double currentcolor; + + &::before { + font-feature-settings: "ornm" 5; + position: absolute; + left: -1.15em; + bottom: 7%; + rotate: -5deg; + } + } +} + +@layer base.other { + table { + min-width: 20rem; + margin: 0 3rem; + border-bottom: 2px solid currentcolor; + } + caption { + border-top: 2px solid currentcolor; + border-bottom: 1px solid currentcolor; + + font-weight: 700; + text-align: center; + .and { font-weight: 450; } + } + + th, td, caption { padding: 0.125lh 0.5em 0.0625lh; } + + tbody th { text-align: right; } +} diff --git a/velzek/classy-fabric.png b/dnd/classy-fabric.png similarity index 100% rename from velzek/classy-fabric.png rename to dnd/classy-fabric.png diff --git a/dnd/index.css b/dnd/index.css new file mode 100644 index 0000000..16f6994 --- /dev/null +++ b/dnd/index.css @@ -0,0 +1,71 @@ +@import url(base.css); + +@layer { + header { row-gap: 0; } + header p { + grid-area: 2/1/2/4; + margin: 0; + text-align: center; + + font-size: 1.5rem; + font-weight: 600; + font-stretch: 90%; + } +} + +@layer { + .portrait { + margin: 0; + border: 4px solid currentcolor; + box-shadow: var(--shadow); + img { + display: block; + width: 200px; + height: 200px; + } + } + + .char { + width: 80%; + margin-left: auto; + margin-right: auto; + + padding-left: 1em; + padding-right: 1em; + + display: grid; + gap: 0.5em 2em; + + &:nth-of-type(odd) { + grid-template: + "portrait name" auto + "portrait desc" 1fr / min-content auto; + } + &:nth-of-type(even) { + grid-template: + "name portrait" auto + "desc portrait" 1fr / auto min-content; + } + + .portrait { grid-area: portrait; } + h2 { grid-area: name; margin: 0; } + > div, p { grid-area: desc; align-self: start; } + } +} + +@layer { + .char { + margin-top: 2em; + padding-top: 2em; + border-top: 3px double currentcolor; + } + .char:last-of-type { + padding-bottom: 2em; + border-bottom: 3px double currentcolor; + } + + h2 small { + font-size: 70%; + font-weight: 700; + } +} diff --git a/dnd/index.html b/dnd/index.html new file mode 100644 index 0000000..5237e5a --- /dev/null +++ b/dnd/index.html @@ -0,0 +1,83 @@ + + + +d&d chars + + + + + +
+

d&d chars

+

blorbos from my tabletop +

+ + +
+
+ + + +
+ +

+ velzek hawthorne +

+ +

+ anthropologist observing the humanoid society in marikest, when a suspicious + sequence of events plunge the city into chaos. + (2024–) +

+ +
+
+ + + +
+ +

+ (call me) marigold +

+ +

+ disgraced acolyte of bahamut sent out into the world to atone for her + mistakes. + (2024–) +

+ +
+
+ +
+ +

+ nex +

+ +

+ went to hell and back to stop the world from disintegrating. you know, + normal stuff. + (2020–24) +

+ +
+
+ +
+ +

+ kezda +

+ +

+ little lizard who saw magic one time and never stopped thinking about it +
+ (2019; game abandoned but i still like this lil gremlin) +

+ + + diff --git a/dnd/kezda.webp b/dnd/kezda.webp new file mode 100644 index 0000000..9615727 --- /dev/null +++ b/dnd/kezda.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84889cc3c07b72572efac3d4a2aa5120946c2f3689d21de548b3b2ed0418f258 +size 7602 diff --git a/dnd/kezda/index.html b/dnd/kezda/index.html new file mode 100644 index 0000000..39fa785 --- /dev/null +++ b/dnd/kezda/index.html @@ -0,0 +1,28 @@ + + + +kezda + + + + + + +
+

kezda

+
+ +
+

basic info

+ +
+ kezda with their familar poking out of a hole in their hat +
+ + a lil creature +
+ + diff --git a/dnd/kezda/kezda.webp b/dnd/kezda/kezda.webp new file mode 100644 index 0000000..68c7cc3 --- /dev/null +++ b/dnd/kezda/kezda.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1522761b3c8c882348018df874c5166ea6f2257e0ffc60ad2e7b5b14bc3cf589 +size 34762 diff --git a/dnd/kezda/kezda2x.webp b/dnd/kezda/kezda2x.webp new file mode 100644 index 0000000..3307bc5 --- /dev/null +++ b/dnd/kezda/kezda2x.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3bdfdca3389270f2828515c7fc89255f464d560d05a595c45b1043fb5dc27ec +size 86876 diff --git a/dnd/kezda/src/kezda-full.kra b/dnd/kezda/src/kezda-full.kra new file mode 100644 index 0000000..6cfc0da --- /dev/null +++ b/dnd/kezda/src/kezda-full.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddb0310e2477891474493330e6af61d5a78c3e3ba7fc2e639d54613075d29bdd +size 1806236 diff --git a/dnd/kezda/style.css b/dnd/kezda/style.css new file mode 100644 index 0000000..5117218 --- /dev/null +++ b/dnd/kezda/style.css @@ -0,0 +1,3 @@ +@import url(../bio.css); + + diff --git a/dnd/kezda2x.webp b/dnd/kezda2x.webp new file mode 100644 index 0000000..0e95e0e --- /dev/null +++ b/dnd/kezda2x.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76ab6baf47f3a3da6702629dc8b33faf18a644240fb977276ceb4715392eecdd +size 29554 diff --git a/dnd/marigold.webp b/dnd/marigold.webp new file mode 100644 index 0000000..5b4c9d2 --- /dev/null +++ b/dnd/marigold.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d211a5b1e6c080c216b7c4df974a088a56fc692aac7c26cca25dd10194d566b +size 13806 diff --git a/dnd/marigold/index.html b/dnd/marigold/index.html new file mode 100644 index 0000000..387e731 --- /dev/null +++ b/dnd/marigold/index.html @@ -0,0 +1,35 @@ + + + +marigold + + + + + + + +
+

marigold

+
+ +
+

basic info

+ + + +
warlock of bahamut +
height 198 cm (6ʹ 6ʺ) +
weight 121 kg (267 ℔) +
age 37 +
+ +

+ let’s get it out of the way. her actual name is, um, let’s see here… + [ˈqχḁʂx̩kɬ̩ːχ]. which is why she goes by ‘marigold’ + among the humanoids. +

+ + diff --git a/dnd/marigold/style.css b/dnd/marigold/style.css new file mode 100644 index 0000000..4ee941d --- /dev/null +++ b/dnd/marigold/style.css @@ -0,0 +1,6 @@ +@import url(../bio.css); + +.ipa { + font-feature-settings: "ss03" 1; + font-variation-settings: "ENLA" 0; +} diff --git a/dnd/marigold2x.webp b/dnd/marigold2x.webp new file mode 100644 index 0000000..60ed6e4 --- /dev/null +++ b/dnd/marigold2x.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a568dbba03fc5694bff95dd39785dcd60c44d93129396078a2f0d1dddcbcff7 +size 59848 diff --git a/dnd/nex.kra-autosave.kra b/dnd/nex.kra-autosave.kra new file mode 100644 index 0000000..35651f4 --- /dev/null +++ b/dnd/nex.kra-autosave.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e112a526af68ebc0d3d38fc0778383a9774e4cc7a793e3261b7c861c44ca235 +size 266113 diff --git a/dnd/nex.webp b/dnd/nex.webp new file mode 100644 index 0000000..5d40a2b --- /dev/null +++ b/dnd/nex.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:040cb5a75480d2531220fc9ffe7ae3b53f1972859bc707615e4946bd909e7dcc +size 11400 diff --git a/dnd/nex/index.html b/dnd/nex/index.html new file mode 100644 index 0000000..3a0afcb --- /dev/null +++ b/dnd/nex/index.html @@ -0,0 +1,22 @@ + + + +nex + + + + + + + +
+

nex

+
+ +
+

basic info

+
+ + diff --git a/dnd/nex2x.webp b/dnd/nex2x.webp new file mode 100644 index 0000000..7b044a6 --- /dev/null +++ b/dnd/nex2x.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:532577340ef26027f56d0e2bb64af46759fc0409b4ce2506c7c10249af16e82d +size 38638 diff --git a/dnd/src/kezda.kra b/dnd/src/kezda.kra new file mode 100644 index 0000000..8128943 --- /dev/null +++ b/dnd/src/kezda.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5a1223583e49404f8c793193f4d646844102e2c419162ac750c59528b22c00a +size 1350142 diff --git a/dnd/src/marigold.kra b/dnd/src/marigold.kra new file mode 100644 index 0000000..99ae534 --- /dev/null +++ b/dnd/src/marigold.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dd0f7aaceda0d3d48f080af4af3cea64ecb8ec642ed1f30243fea6eeb5832a5 +size 5978311 diff --git a/dnd/src/nex.kra b/dnd/src/nex.kra new file mode 100644 index 0000000..131d5dc --- /dev/null +++ b/dnd/src/nex.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecdb9ea0372c2de9af613092452b92faa2baa615ea222f01c98373a67bc26201 +size 6118966 diff --git a/dnd/src/velzek.kra b/dnd/src/velzek.kra new file mode 100644 index 0000000..ff5502c --- /dev/null +++ b/dnd/src/velzek.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eedeb81b2fdd30a7186c4d9a4b794383803eeb750451ce0b387b9eec4a4a056e +size 7874406 diff --git a/dnd/velzek.webp b/dnd/velzek.webp new file mode 100644 index 0000000..e9ea264 --- /dev/null +++ b/dnd/velzek.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4787f6f3a64557f9f1ba013ce3af6411b3ebe6fe4044de45b6bff79fd230510 +size 13502 diff --git a/dnd/velzek/armour.s.webp b/dnd/velzek/armour.s.webp new file mode 100644 index 0000000..28a6d27 --- /dev/null +++ b/dnd/velzek/armour.s.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f21e7756e02bd2fad3b40afea724980e1785fe71379abbbd06ba5aaa2c616911 +size 36174 diff --git a/dnd/velzek/armour.s2x.webp b/dnd/velzek/armour.s2x.webp new file mode 100644 index 0000000..b71b42a --- /dev/null +++ b/dnd/velzek/armour.s2x.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:454cf6c7b94fabb17e3db8677037b90e2d57c409af5d7d927e341170b6d7811f +size 91868 diff --git a/velzek/armour.webp b/dnd/velzek/armour.webp similarity index 100% rename from velzek/armour.webp rename to dnd/velzek/armour.webp diff --git a/dnd/velzek/clothes.s.webp b/dnd/velzek/clothes.s.webp new file mode 100644 index 0000000..8d33088 --- /dev/null +++ b/dnd/velzek/clothes.s.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0ccfc7ee5c154ca2c439edb2b7ce781a15adedc13af9101c72652de0674ddad +size 39720 diff --git a/dnd/velzek/clothes.s2x.webp b/dnd/velzek/clothes.s2x.webp new file mode 100644 index 0000000..dc66fbe --- /dev/null +++ b/dnd/velzek/clothes.s2x.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:462307197185a1c7298acb4563e271775dce1055abb239c3b9d1a805c2f604f1 +size 99460 diff --git a/velzek/clothes.webp b/dnd/velzek/clothes.webp similarity index 100% rename from velzek/clothes.webp rename to dnd/velzek/clothes.webp diff --git a/velzek/icon.webp b/dnd/velzek/icon.webp similarity index 100% rename from velzek/icon.webp rename to dnd/velzek/icon.webp diff --git a/velzek/index.html b/dnd/velzek/index.html similarity index 66% rename from velzek/index.html rename to dnd/velzek/index.html index 0ba897f..3210a27 100644 --- a/velzek/index.html +++ b/dnd/velzek/index.html @@ -8,14 +8,6 @@ - - - - - - - -

velzek hawthorne

@@ -23,36 +15,33 @@

basic info

-
+
-
-

- cleric of suveesha - and anthropologist - -

-
height
74 cm (2ʹ 5ʺ) -
weight
18 kg (40 ℔) -
age
32 -
year of birth
1187 -
alignment
neutral good -
+ + +
+ cleric of suveesha + and anthropologist +
height 74 cm (2ʹ 5ʺ) +
weight 18 kg (40 ℔) +
age 32 +
year of birth 1187 +

velzek is a kobold from a community called @@ -60,24 +49,26 @@ eight years ago, she and four other kobolds arrived at the temple of berei, next to the green on the south border of marikest. -

- her companions are: - -

-
yarva bitterbrush
26, he/him - -
keshku aestivae
28, she/her - -
volek ruba
23, she/her - -
tokil arceuthus
25, he/him - -
+ + +
velzek’s companions +
yarva bitterbrush 26, he/him +
keshku æstivæ 28, she/her +
volek ruba 23, she/her +
tokil arceuthus 25, he/him +
+

until arriving in marikest, the concept of a surname was totally unknown to the kobolds, so they all invented names based on plants for themselves once - they became needed. + they became needed. in the city they’ve been under the mentorship of a + halfling named bobbie fairchild.

the purpose of the kobolds’ arrival in marikest is to study outside @@ -91,9 +82,9 @@

ekkel

-
+
- ekkel is north of the ruins of avaro’s dungeon @@ -104,7 +95,7 @@ the windswept wall. despite the historical friction between kobolds and humanoids, ekkel has enjoyed peace for decades, due to its location far away from any major surface roads. like all burrows, ekkel is considered to be a - single huge family; kobolds don't consciously keep track of closer kinship + single huge family; kobolds don’t consciously keep track of closer kinship bonds.

@@ -126,8 +117,10 @@

suveesha

-
- +
+ + +

@@ -146,10 +139,10 @@

while amongst the humanoids, velzek and the others have been instructed to - adopt berei’s symbols for familiarity: a bundle of wheat, a sickle, and a + adopt berei’s more familiar symbols: a bundle of wheat, a sickle, and a rising sun.

diff --git a/dnd/velzek/map_k.s.webp b/dnd/velzek/map_k.s.webp new file mode 100644 index 0000000..90c4adf --- /dev/null +++ b/dnd/velzek/map_k.s.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b63f59886ba49ad391235c91193a76a68839f5a754fcf72d63652676bb3196f +size 74660 diff --git a/dnd/velzek/map_k.s2x.webp b/dnd/velzek/map_k.s2x.webp new file mode 100644 index 0000000..b011374 --- /dev/null +++ b/dnd/velzek/map_k.s2x.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a7ce46d4514e066c859bc07243a65ef9974aa5a89f69c6501f25f9e29e8f6d7 +size 281220 diff --git a/velzek/map_k_full.webp b/dnd/velzek/map_k_full.webp similarity index 100% rename from velzek/map_k_full.webp rename to dnd/velzek/map_k_full.webp diff --git a/velzek/src/koboldowo.png b/dnd/velzek/src/koboldowo.png similarity index 100% rename from velzek/src/koboldowo.png rename to dnd/velzek/src/koboldowo.png diff --git a/dnd/velzek/src/map_k_full.kra b/dnd/velzek/src/map_k_full.kra new file mode 100644 index 0000000..3b682d2 --- /dev/null +++ b/dnd/velzek/src/map_k_full.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9eeb8c0af5989115f66288858bcbab1b79eea6f02920de077f1abd846a0a4b4 +size 43950802 diff --git a/dnd/velzek/src/suveesha.kra b/dnd/velzek/src/suveesha.kra new file mode 100644 index 0000000..7852db2 --- /dev/null +++ b/dnd/velzek/src/suveesha.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee1c7d69c79ceada273e5e67152fa72dbad05085c11449f6817925a648f5e337 +size 3564434 diff --git a/dnd/velzek/src/velzek.kra b/dnd/velzek/src/velzek.kra new file mode 100644 index 0000000..111537f --- /dev/null +++ b/dnd/velzek/src/velzek.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b27b27fc732464790da6db965a5c8a06a53de63d163663d0e72009760435423e +size 54608598 diff --git a/dnd/velzek/style.css b/dnd/velzek/style.css new file mode 100644 index 0000000..144c30d --- /dev/null +++ b/dnd/velzek/style.css @@ -0,0 +1,8 @@ +@import url(../bio.css); + +@layer { + #char-pic { + shape-outside: polygon(100% 0%, 13% 0%, 13% 25%, 0% 27%, + 0% 51%, 18% 60%, 21% 100%, 100% 100%); + } +} diff --git a/dnd/velzek/suveesha.s.webp b/dnd/velzek/suveesha.s.webp new file mode 100644 index 0000000..5f24280 --- /dev/null +++ b/dnd/velzek/suveesha.s.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35ed9e13f71104a2fb06cb48952fafc8e3710abb7e23b94f21782a4163956031 +size 42238 diff --git a/dnd/velzek/suveesha.s2x.webp b/dnd/velzek/suveesha.s2x.webp new file mode 100644 index 0000000..56403dc --- /dev/null +++ b/dnd/velzek/suveesha.s2x.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b347734640aa6c35bfbbfe594449d537e8c46d023706db155f9791059c30e6b +size 129808 diff --git a/velzek/suveesha.webp b/dnd/velzek/suveesha.webp similarity index 100% rename from velzek/suveesha.webp rename to dnd/velzek/suveesha.webp diff --git a/dnd/velzek2x.webp b/dnd/velzek2x.webp new file mode 100644 index 0000000..2ce318b --- /dev/null +++ b/dnd/velzek2x.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8222f32d216aa50a9094dbde7683d0036d0a2eb2ab0b911817a0e5fc63caaaaf +size 49264 diff --git a/rainbow-quox/edit.svg b/rainbow-quox/edit.svg new file mode 100644 index 0000000..fc19cae --- /dev/null +++ b/rainbow-quox/edit.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rainbow-quox/script/history.ts b/rainbow-quox/script/history.ts index af8f71e..47fe42e 100644 --- a/rainbow-quox/script/history.ts +++ b/rainbow-quox/script/history.ts @@ -13,11 +13,11 @@ export class HistoryItem { } asHtml(): HTMLButtonElement { - const { lines: bg, outer, belly1: belly, fins1: fins } = this.rgb; + const { lines, outer, belly1: belly, fins1: fins } = this.rgb; const content = ` - + fin colour: ${fins.css()} @@ -53,21 +53,23 @@ export class History { add(name: string): void { this.items.push(name); } - *iterNames(maxLength?: number | null): Iterable { + *iterNames(maxLength: number = 100): Iterable { let seen = new Set; let done = 0; - if (maxLength === undefined) maxLength = 100; for (let i = this.items.length - 1; i >= 0; i--) { - if (maxLength !== null && done > maxLength) break; + if (maxLength >= 0 && done > maxLength) break; const name = this.items[i]!; + if (!name || seen.has(name)) continue; - seen.add(name); done++; yield name; + seen.add(name); + done++; } } - *iterItems(maxLength?: number | null): Iterable { + // pass a negative number to iterate over all + *iterItems(maxLength?: number): Iterable { for (const name of this.iterNames(maxLength)) { const oklch = Color.colors(new Color.Rand(name), Color.KNOWN[name]); const rgbs = Color.toRgbs(oklch); @@ -77,37 +79,26 @@ export class History { } static validate(x: unknown): History | undefined { - if (!Array.isArray(x)) return; - if (!x.every(i => typeof i === 'string')) return; - return new History(x); + if (Array.isArray(x) && x.every(i => typeof i == 'string')) + return new History(x); } - toJSON() { return this.items; } + toJSON(): unknown { return this.items; } - save(persist = true) { + save(persist = true): void { const storage = persist ? localStorage : sessionStorage; storage.setItem('history', JSON.stringify(this)); } - // if the json was invalid, return it - // if no history exists just start a new one - static load(): History | string { + // if no history exists, or it's invalid, just start a new one + static load(): History { const json = sessionStorage.getItem('history') ?? localStorage.getItem('history'); - if (json != null) { - let h = History.validate(JSON.parse(json)); - if (h) { h.prune(); return h; } - else return json; - } else { - return new History; - } - } - // if the json is invalid, discard it - static loadOrClear(): History { - const h = History.load(); - return h instanceof History ? h : new History; + if (json === null) return new History; + + return History.validate(JSON.parse(json)) ?? new History; } addSave(name: string, persist = true): void { @@ -115,14 +106,9 @@ export class History { this.save(persist); } - prune(maxLength?: number | null) { + prune(maxLength?: number): void { let keep = []; for (let name of this.iterNames(maxLength)) keep.push(name); this.items = keep.reverse(); } - - pruneSave(maxLength?: number | null, persist = true) { - this.prune(maxLength); - this.save(persist); - } } diff --git a/rainbow-quox/script/layer.ts b/rainbow-quox/script/layer.ts new file mode 100644 index 0000000..4727b20 --- /dev/null +++ b/rainbow-quox/script/layer.ts @@ -0,0 +1,161 @@ +import * as Color from './color.js'; + +async function loadBitmap(url: string): Promise { + const img0 = new Image; + const img: Promise = new Promise((ok, err) => { + img0.addEventListener('load', () => ok(img0)); + img0.addEventListener('error', () => err(`couldn't load file: ${url}`)); + }); + img0.src = url; + return createImageBitmap(await img); +} + + +export type Buffer = OffscreenCanvasRenderingContext2D; + +function dataViaBuffer(bmp: ImageBitmap, buf: Buffer): ImageData { + buf.clearRect(0, 0, bmp.width, bmp.height); + buf.drawImage(bmp, 0, 0); + return buf.getImageData(0, 0, bmp.width, bmp.height); +} + +async function loadDataLocking(url: string, buf: Buffer): Promise { + return loadBitmap(url).then(i => + navigator.locks.request('imagebuf', () => dataViaBuffer(i, buf))); +} + +async function loadDataFresh(url: string): Promise { + 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 { + if (buf && navigator.locks) return loadDataLocking(url, buf); + else return loadDataFresh(url); +} + + +export const WIDTH = 1040; +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; + +// in compositing order +export const allLayers: Layer[] = + ['stroke', 'static', 'outer', 'spines', 'stripes', 'cuffs', 'fins1', 'fins2', + 'fins3', 'belly1', 'belly2', 'masks', 'claws', 'vitiligo1', 'vitiligo2', + 'vitiligo3', 'vitiligo4', 'eyes', 'eyeshine', 'lines']; + +export function makeLayerInfo(f: (l: Layer) => A): Record { + return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record; +} + +export async function makeLayerInfoAsync(f: (l: Layer) => Promise): +Promise> { + let list = await Promise.all(allLayers.map(l => f(l).then(res => [l, res]))); + return Object.fromEntries(list); +} + + +export function loadLayers(dir: string): Promise> { + let buf = makeBufferIfLocks(WIDTH, HEIGHT); + return makeLayerInfoAsync(l => loadImageData(`./${dir}/${l}.webp`, buf)); +} + + +export type Position = [x: number, y: number]; +export type Positions = Record; + +export async function loadPos(dir: string): Promise { + return (await fetch(`./${dir}/pos.json`)).json(); +} + + +export type Side = 'front' | 'back'; + +export function swapSide(s: Side): Side { + return s == 'front' ? 'back' : 'front'; +} + +export type SideData = Record; + +export type Data = { + front: SideData, back: SideData, + frontImage?: ImageData, backImage?: ImageData, +}; + +export type ComposedData = Required; + +export async function loadData(): Promise { + let [fl, fp, bl, bp] = await Promise.all([ + loadLayers('front'), loadPos('front'), + loadLayers('back'), loadPos('back') + ]); + return { + front: makeLayerInfo(l => [fl[l], fp[l]]), + back: makeLayerInfo(l => [bl[l], bp[l]]), + } +} + + +function recolor({ data }: ImageData, { r, g, b }: Color.Rgb) { + for (let i = 0; i < data.length; i += 4) { + data[i] = r; data[i+1] = g; data[i+2] = b; + } +} + +export async function recolorAll(layers: Data, cols: Color.Rgbs) { + await Promise.all(Color.allLayers.map(l => { + recolor(layers.front[l][0], cols[l]); + recolor(layers.back[l][0], cols[l]); + })); + delete layers.frontImage; delete layers.backImage; +} + +export type ComposeLayer = [ImageData, Position, GlobalCompositeOperation]; + +async function compose(buf: Buffer, layers: ComposeLayer[], + width: number, height: number): Promise { + buf.save(); + buf.clearRect(0, 0, width, height); + const bmps = await Promise.all(layers.map(async ([l, [x, y], m]) => + [await createImageBitmap(l), x, y, m] as const)); + for (const [bmp, x, y, m] of bmps) { + buf.globalCompositeOperation = m; + buf.drawImage(bmp, x, y); + } + buf.restore(); + return buf.getImageData(0, 0, width, height); +} + +export async function +ensureComposed(buf: Buffer, data: Data): Promise { + let { front, back } = data; + data.frontImage ??= await composeLayers(front); + data.backImage ??= await composeLayers(back); + return data as ComposedData; + + function composeLayers(sdata: SideData): Promise { + return compose(buf, allLayers.map(l => makeLayer(l, sdata)), WIDTH, HEIGHT); + } + function makeLayer(l: Layer, sdata: SideData): ComposeLayer { + let [i, p] = sdata[l]; + return [i, p, l == 'eyeshine' ? 'luminosity' : 'source-over']; + } +} + + +export async function redraw(ctx: CanvasRenderingContext2D, + buf: Buffer, data: ComposedData, side: Side) { + await ensureComposed(buf, data); + ctx.putImageData(data[`${side}Image`], 0, 0); +} diff --git a/rainbow-quox/script/palette.ts b/rainbow-quox/script/palette.ts new file mode 100644 index 0000000..1b4501b --- /dev/null +++ b/rainbow-quox/script/palette.ts @@ -0,0 +1,64 @@ +import { Rgb, Rgbs, rgb } from './color.js'; +import { Layer } from './layer.js'; + +export type Color = + Exclude + | 'collars' | 'bells' | 'tongues' | 'socks' | 'sclera'; + +// in palette order +export const COLORS: Color[] = + ['lines', 'outer', 'vitiligo1', 'spines', 'fins1', 'fins2', 'fins3', + 'vitiligo4', 'belly1', 'vitiligo3', 'belly2', 'vitiligo2', 'sclera', + 'eyes', 'tongues', 'masks', 'claws', 'socks', 'stripes', 'cuffs', + 'collars', 'bells']; + +export const NAMES: Partial> = { + outer: 'outer body', + stripes: 'sock stripes', + cuffs: 'sock cuffs', + fins1: 'fins (outer)', + fins2: 'fins (mid)', + fins3: 'fins (inner)', + belly1: 'belly 1', + belly2: 'belly 2', + vitiligo1: 'outer body vitiligo', + vitiligo2: 'belly 2 vitiligo', + vitiligo3: 'belly 1 vitiligo', + vitiligo4: 'fins vitiligo', +}; + +export function name(l: Color): string { + return NAMES[l] ?? l; +} + +export type StaticColor = Exclude; + +export const STATIC_COLS: Record = { + collars: rgb(206, 75, 101), + bells: rgb(235, 178, 79), + tongues: rgb(222, 165, 184), + socks: rgb(238, 239, 228), + sclera: rgb(238, 239, 228), +}; + +export function get(col: Color, palette: Rgbs): Rgb { + type PPalette = Partial>; + let p = palette as PPalette; + let s = STATIC_COLS as PPalette; + return (p[col] ?? s[col])!; +} + +export function make(seed: string, palette: Rgbs): Blob { + let lines = [ + "GIMP Palette\n", + `Name: quox ${seed}\n`, + "Columns: 6\n\n", + ]; + + for (const col of COLORS) { + let { r, g, b } = get(col, palette); + lines.push(`${r} ${g} ${b} ${name(col)}\n`); + } + + return new Blob(lines, { type: 'application/x-gimp-palette' }); +} diff --git a/rainbow-quox/script/quox.ts b/rainbow-quox/script/quox.ts index 0228127..ab19ebf 100644 --- a/rainbow-quox/script/quox.ts +++ b/rainbow-quox/script/quox.ts @@ -1,177 +1,16 @@ import * as Color from './color.js'; import { History } from './history.js'; +import * as Layer from './layer.js'; +import * as Palette from './palette.js'; -async function loadBitmap(url: string): Promise { - const img0 = new Image; - const img: Promise = new Promise((ok, err) => { - img0.addEventListener('load', () => ok(img0)); - img0.addEventListener('error', () => err(`couldn't load file: ${url}`)); - }); - img0.src = url; - return createImageBitmap(await img); -} - - -type Buffer = OffscreenCanvasRenderingContext2D; - -function dataViaBuffer(bmp: ImageBitmap, buf: Buffer): ImageData { - buf.clearRect(0, 0, bmp.width, bmp.height); - buf.drawImage(bmp, 0, 0); - return buf.getImageData(0, 0, bmp.width, bmp.height); -} - -async function loadDataLocking(url: string, buf: Buffer): Promise { - return loadBitmap(url).then(i => - navigator.locks.request('imagebuf', () => dataViaBuffer(i, buf))); -} - -async function loadDataFresh(url: string): Promise { - const img = await loadBitmap(url); - let buf = new OffscreenCanvas(img.width, img.height).getContext('2d')!; - return dataViaBuffer(img, buf); -} - -function loadImageData(url: string, buf?: Buffer): Promise { - if (buf && navigator.locks) return loadDataLocking(url, buf); - else return loadDataFresh(url); -} - -const WIDTH = 1040; -const HEIGHT = 713; - -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) makeBuffer(width, height); - else return undefined; -} - -export type Layer = 'stroke' | 'static' | 'eyeshine' | Color.Layer; - -// in compositing order -export const allLayers: Layer[] = - ['stroke', 'static', 'outer', 'spines', 'stripes', 'cuffs', 'fins1', 'fins2', - 'fins3', 'belly1', 'belly2', 'masks', 'claws', 'vitiligo1', 'vitiligo2', - 'vitiligo3', 'vitiligo4', 'eyes', 'eyeshine', 'lines']; - -function makeLayerInfo(f: (l: Layer) => A): Record { - return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record; -} - -async function makeLayerInfoAsync(f: (l: Layer) => Promise): -Promise> { - let list = await Promise.all(allLayers.map(l => f(l).then(res => [l, res]))); - return Object.fromEntries(list); -} - - -function loadLayers(dir: string): Promise> { - let buf = makeBufferIfLocks(WIDTH, HEIGHT); - return makeLayerInfoAsync(l => loadImageData(`./${dir}/${l}.webp`, buf)); -} - - - -type Position = [x: number, y: number]; -type Positions = Record; - -async function loadPos(dir: string): Promise { - return (await fetch(`./${dir}/pos.json`)).json(); -} - - -type Side = 'front' | 'back'; - -function swapSide(s: Side): Side { - return s == 'front' ? 'back' : 'front'; -} - -type SideData = Record; - -type LayerData = { - front: SideData, back: SideData, - frontImage?: ImageData, backImage?: ImageData, -}; - -type ComposedData = Required; - -async function loadData(): Promise { - let [fl, fp, bl, bp] = await Promise.all([ - loadLayers('front'), loadPos('front'), - loadLayers('back'), loadPos('back') - ]); - return { - front: makeLayerInfo(l => [fl[l], fp[l]]), - back: makeLayerInfo(l => [bl[l], bp[l]]), - } -} - - -function singleColor({ data }: ImageData, { r, g, b }: Color.Rgb) { - for (let i = 0; i < data.length; i += 4) { - data[i] = r; data[i+1] = g; data[i+2] = b; - } -} - -async function recolorLayers(layers: LayerData, cols: Color.Rgbs) { - await Promise.all(Color.allLayers.map(l => { - singleColor(layers.front[l][0], cols[l]); - singleColor(layers.back[l][0], cols[l]); - })); - delete layers.frontImage; delete layers.backImage; -} - -type ComposeLayer = [ImageData, Position, GlobalCompositeOperation]; - -async function compose(buf: Buffer, layers: ComposeLayer[], - width: number, height: number): Promise { - buf.save(); - buf.clearRect(0, 0, width, height); - const bmps = await Promise.all(layers.map(async ([l, [x, y], m]) => - [await createImageBitmap(l), x, y, m] as const)); - for (const [bmp, x, y, m] of bmps) { - buf.globalCompositeOperation = m; - buf.drawImage(bmp, x, y); - } - buf.restore(); - return buf.getImageData(0, 0, width, height); -} - -async function -ensureComposed(buf: Buffer, data: LayerData): Promise { - let { front, back } = data; - data.frontImage ??= await composeLayers(front); - data.backImage ??= await composeLayers(back); - return data as ComposedData; - - function composeLayers(sdata: SideData): Promise { - return compose(buf, allLayers.map(l => makeLayer(l, sdata)), WIDTH, HEIGHT); - } - function makeLayer(l: Layer, sdata: SideData): ComposeLayer { - let [i, p] = sdata[l]; - return [i, p, l == 'eyeshine' ? 'luminosity' : 'source-over']; - } -} - - -async function redraw(ctx: CanvasRenderingContext2D, - buf: Buffer, data: ComposedData, side: Side) { - await ensureComposed(buf, data); - ctx.putImageData(data[`${side}Image`], 0, 0); -} - - -function message(msg: string, error = false) { +function message(msg: string, size = 100) { const ctx = getCanvasCtx('main'); - const size = error ? 30 : 100; ctx.save(); - ctx.clearRect(0, 0, WIDTH, HEIGHT); + ctx.clearRect(0, 0, Layer.WIDTH, Layer.HEIGHT); ctx.font = `bold ${size}px Muller, sans-serif`; ctx.textAlign = 'center'; - ctx.fillText(msg, WIDTH/2, HEIGHT/2, WIDTH-10); + ctx.fillText(msg, Layer.WIDTH/2, Layer.HEIGHT/2, Layer.WIDTH-10); ctx.restore(); } @@ -187,19 +26,19 @@ function updateUrl(seed: string): void { type ApplyStateOpts = { seed: string, - side?: Side, + side?: Layer.Side, firstLoad?: boolean, - buf?: Buffer, + buf?: Layer.Buffer, history?: History, done?: Done, }; async function -applyState(data: LayerData, opts: ApplyStateOpts): Promise { +applyState(data: Layer.Data, opts: ApplyStateOpts): Promise { let { side, seed, firstLoad, buf, history, done } = opts; side ??= 'front'; firstLoad ??= false; - buf ??= new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!; + buf ??= Layer.makeBuffer(); done ??= () => {}; let rand = new Color.Rand(seed); @@ -208,7 +47,7 @@ applyState(data: LayerData, opts: ApplyStateOpts): Promise { const rgb = Color.toRgbs(oklch); const newSeed = rand.alphaNum(); - await recolorLayers(data, rgb); + await Layer.recolorAll(data, rgb); updateBg(oklch); updateSvgs(oklch, rgb); @@ -216,7 +55,7 @@ applyState(data: LayerData, opts: ApplyStateOpts): Promise { updateUrl(seed); if (firstLoad) { - await instantUpdateImage(side, await ensureComposed(buf, data)); + await instantUpdateImage(side, await Layer.ensureComposed(buf, data)); done(); } else { await animateUpdateImage(buf, side, data, done); @@ -235,7 +74,7 @@ function getCanvasCtx(id: CanvasId) { } async function -instantUpdateImage(side: Side, data: ComposedData) { +instantUpdateImage(side: Layer.Side, data: Layer.ComposedData) { getCanvasCtx('main').putImageData(data[`${side}Image`], 0, 0); } @@ -244,9 +83,10 @@ type Done = () => void; const noAnim = matchMedia('(prefers-reduced-motion: reduce)'); async function -animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) { +animateUpdateImage(buf: Layer.Buffer, side: Layer.Side, + data: Layer.Data, done: Done) { if (noAnim.matches) { - instantUpdateImage(side, await ensureComposed(buf, data)); + instantUpdateImage(side, await Layer.ensureComposed(buf, data)); done(); return; } @@ -257,11 +97,11 @@ animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) { const aux = getCanvasCtx('aux'); document.documentElement.dataset.running = 'reroll'; - const cdata = await ensureComposed(buf, data); - redraw(aux, buf, cdata, side); + const cdata = await Layer.ensureComposed(buf, data); + Layer.redraw(aux, buf, cdata, side); aux.canvas.addEventListener('animationend', async () => { - await redraw(main, buf, cdata, side); + await Layer.redraw(main, buf, cdata, side); aux.canvas.style.removeProperty('animation'); delete document.documentElement.dataset.running; done(); @@ -273,7 +113,8 @@ animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) { } async function -animateSwapImage(buf: Buffer, newSide: Side, data: ComposedData, done: Done) { +animateSwapImage(buf: Layer.Buffer, newSide: Layer.Side, + data: Layer.ComposedData, done: Done) { if (noAnim.matches) { instantUpdateImage(newSide, data); done(); @@ -286,10 +127,10 @@ animateSwapImage(buf: Buffer, newSide: Side, data: ComposedData, done: Done) { const aux = getCanvasCtx('aux'); document.documentElement.dataset.running = 'swap'; - await redraw(aux, buf, data, newSide); + await Layer.redraw(aux, buf, data, newSide); aux.canvas.addEventListener('animationend', async () => { - const image = aux.getImageData(0, 0, WIDTH, HEIGHT); + const image = aux.getImageData(0, 0, Layer.WIDTH, Layer.HEIGHT); main.putImageData(image, 0, 0); main.canvas.style.removeProperty('animation'); @@ -342,7 +183,7 @@ function updateSvgs(oklch: Color.Colors, rgb: Color.Rgbs) { } -function showHistory(history: History, data: LayerData, +function showHistory(history: History, data: Layer.Data, opts: Omit) { const list = document.getElementById('history-items'); if (!list) return; @@ -369,7 +210,7 @@ function showHistory(history: History, data: LayerData, function closeHistory() { 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'); if (field) field.parentElement?.removeChild(field); document.documentElement.dataset.state = 'ready'; @@ -377,18 +218,7 @@ function closeHistory() { function download(seed: string) { const colors = Color.toRgbs(Color.colors(new Color.Rand(seed))); - - let lines = [ - "GIMP Palette\n", - `Name: quox ${seed}\n\n`, - ]; - - for (const name of Color.allLayers) { - let { r, g, b } = colors[name]; - lines.push(`${r} ${g} ${b} ${name}\n`); - } - - const blob = new Blob(lines, { type: 'application/x-gimp-palette' }); + const blob = Palette.make(seed, colors); // there must be a better way to push out a file than // this autohotkey-ass nonsense @@ -403,15 +233,15 @@ function download(seed: string) { async function setup() { message('loading layers…'); - let data = await loadData().catch(e => { message(e, true); throw e }); - let history = History.loadOrClear(); + let data = await Layer.loadData().catch(e => { message(e, 30); throw e }); + let history = History.load(); - let buf = new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!; + let buf = Layer.makeBuffer(); let prevSeed = urlState() ?? new Color.Rand().alphaNum(); let seed = await applyState(data, { seed: prevSeed, buf, history, firstLoad: true }); - let side: Side = 'front'; + let side: Layer.Side = 'front'; const reroll = document.getElementById('reroll')!; const swap = document.getElementById('swap')!; @@ -473,8 +303,8 @@ async function setup() { } function runSwap() { run(async k => { - side = swapSide(side); - const cdata = await ensureComposed(buf, data); + side = Layer.swapSide(side); + const cdata = await Layer.ensureComposed(buf, data); await animateSwapImage(buf, side, cdata, k); }); } diff --git a/rainbow-quox/style/defs.scss b/rainbow-quox/style/defs.scss new file mode 100644 index 0000000..74b1202 --- /dev/null +++ b/rainbow-quox/style/defs.scss @@ -0,0 +1,71 @@ +$box-texture: url(3px-tile.png); +$box-bg: oklch(0.3 0.2 var(--hue)); +$box-fg: oklch(0.95 0.075 var(--c-hue)); + +$button-bg: oklch(0.5 0.25 var(--hue)); +$button-fg: oklch(0.98 0.1 var(--c-hue)); + + +// https://oakreef.ie/transy :) +$transition-duration: 250ms; +$transition-curve: cubic-bezier(.47,.74,.61,1.2); + +@mixin transy($prop: transform, $duration: $transition-duration) { + transition: $prop $duration $transition-curve; +} + +@mixin shadow { + filter: drop-shadow(6px 6px 0 oklch(0.4 0.2 var(--hue) / 0.45)); + @media (prefers-color-scheme: dark) { + filter: drop-shadow(6px 6px 0 oklch(0.1 0.125 var(--hue) / 0.45)); + } +} + + +@mixin box-base { + @include shadow; + // respecify font family for