Compare commits

..

2 commits

Author SHA1 Message Date
dff263856c rainbow quox 2024-12-28 22:27:09 +01:00
cad73b232d some d&d stuff 2024-12-28 22:16:07 +01:00
71 changed files with 844 additions and 402 deletions

View file

@ -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 = \ MEDIA = \
$(wildcard media/*.png) $(wildcard media/*.gif) $(wildcard media/*.webp) \ $(wildcard media/*.png) $(wildcard media/*.gif) $(wildcard media/*.webp) \
$(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 velzek/*.webp) $(wildcard velzek/*.png) $(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/*)) \
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)) SCRIPTS = $(patsubst %.ts,%.js,$(wildcard script/*.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

@ -1,5 +1,5 @@
@layer base, header, footer, images, switcher, phone; @layer fonts, base, header, footer, images, switcher, phone;
@import url(../fonts/junicodevf/junicodevf.css) layer(base); @import url(../fonts/junicodevf/junicodevf.css) layer(fonts);
@layer base { @layer base {
* { box-sizing: border-box; } * { box-sizing: border-box; }
@ -10,8 +10,8 @@
--text-color: hsl(350deg 40% 15%); --text-color: hsl(350deg 40% 15%);
--accent-color: hsl(80deg 50% 35%); --accent-color: hsl(80deg 50% 35%);
--shadow-color: hsl(from var(--text-color) h 20% 3% / 20%); --shadow-color: hsl(from var(--text-color) h 20% 3% / 20%);
--shadow: drop-shadow(3px 2px 0 var(--shadow-color)); --shadow: 3px 2px 0 var(--shadow-color);
--shadow2: drop-shadow(-2px 0 0 hsl(from var(--text-color) h 20% 7% / 15%)); --shadow2: -2px 0 0 hsl(from var(--text-color) h 20% 7% / 15%);
} }
:root { :root {
@ -34,17 +34,18 @@
background: url(beige-paper.png), hsl(40deg 80% 80%); background: url(beige-paper.png), hsl(40deg 80% 80%);
background-blend-mode: multiply; background-blend-mode: multiply;
border: 10px solid currentcolor; border: 10px solid currentcolor;
filter: var(--shadow) var(--shadow2); box-shadow: var(--shadow), var(--shadow2);
} }
figure { filter: var(--shadow2); }
} }
@layer base.fonts { @layer base.fonts {
:root { :root {
font-family: JunicodeVF, serif; font-family: JunicodeVF, serif;
font-feature-settings: --base-features:
"ccmp", "calt", "liga", "loca", "rlig", "kern", "mark", "mkmk"; "ccmp", "calt", "liga", "loca", "rlig", "kern", "mark", "mkmk",
"ss09", "cv69" 6; /* nice */
font-feature-settings: var(--base-features);
font-variation-settings: "ENLA" 25; font-variation-settings: "ENLA" 25;
font-stretch: 125%; font-stretch: 125%;
font-weight: 450; 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 { @layer base.other {
a { a {
color: inherit; color: inherit;
@ -111,19 +86,35 @@
font-style: italic; font-style: italic;
} }
dl { margin: 0 1em; } .amp {
dt { font-weight: bold; } font-size: 75%;
dd { margin: 0; } font-weight: 550;
@media (width >= 70rem) {
dl {
display: grid;
grid-template-columns: auto 1fr;
column-gap: 1em;
} }
dt { grid-column-start: 1; } small { font-stretch: 100%; }
dd { grid-column-start: 2; } }
@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;
}
} }
} }
@ -173,7 +164,7 @@
} }
@layer images { @layer images {
#velzek-img-holder { #char-img-holder {
display: grid; display: grid;
grid-template: "i"; grid-template: "i";
align-items: center; align-items: center;
@ -184,37 +175,36 @@
margin: auto; margin: auto;
} }
img { .mainfig {
filter: var(--shadow2);
max-width: 100%; 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) { @media (width >= 70rem) {
figure { .mainfig {
width: 480px; width: 480px;
float: right; float: right;
margin: 0 calc(0px - var(--protrude)) 1em 1em; margin: 0 calc(0px - var(--protrude)) 1em 1em;
img { width: 100%; } img { width: 100%; }
} }
#velzek-pic { section:nth-of-type(even) .mainfig { rotate: -1deg; }
shape-outside: polygon(100% 0%, 13% 0%, 13% 25%, 0% 27%, section:nth-of-type(odd) .mainfig { rotate: 1.5deg; }
0% 51%, 18% 60%, 21% 100%, 100% 100%); #char-pic { rotate: 0deg; }
}
#ekkel-pic { rotate: -2deg; }
#suveesha-pic { rotate: 3deg; }
} }
} }
@layer switcher { @layer switcher {
#velzek-pic img { #pic1, #pic2 { transition: all ease 200ms 175ms; }
transition: all ease 200ms 175ms;
}
:root:has(#switch:checked) #clothes-pic, :root:has(#switch:checked) #pic1,
:root:not(:has(#switch:checked)) #armour-pic { :root:not(:has(#switch:checked)) #pic2 {
opacity: 0; opacity: 0;
transition-delay: 0ms; transition-delay: 0ms;
pointer-events: none; pointer-events: none;
@ -225,7 +215,7 @@
position: absolute; position: absolute;
} }
#velzek-pic label { #char-pic label {
display: block; display: block;
width: min-content; width: min-content;
margin: 0 auto 1em; margin: 0 auto 1em;

38
dnd/bio.css Normal file
View file

@ -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; }
}

71
dnd/index.css Normal file
View file

@ -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;
}
}

83
dnd/index.html Normal file
View file

@ -0,0 +1,83 @@
<!doctype html>
<html lang=en>
<meta charset=utf-8>
<title>d&amp;d chars</title>
<link rel=stylesheet href=index.css>
<meta name=viewport content='width=device-width; initial-scale=0.75'>
<header>
<h1>d<span class=amp>&amp;</span>d chars</h1>
<p> blorbos from my tabletop
</header>
<section class=char id=velzek>
<figure class=portrait>
<a href=velzek>
<img src=velzek.webp srcset='velzek2x.webp 2x'>
</a>
</figure>
<h2>
<a href=velzek>velzek hawthorne</a>
</h2>
<p>
anthropologist observing the humanoid society in marikest, when a suspicious
sequence of events plunge the city into chaos.
(2024)
</section>
<section class=char id=marigold>
<figure class=portrait>
<a href=marigold>
<img src=marigold.webp srcset='marigold2x.webp 2x'>
</a>
</figure>
<h2>
<a href=marigold><small>(call me)</small> marigold</a>
</h2>
<p>
disgraced acolyte of bahamut sent out into the world to atone for her
mistakes.
(2024)
</section>
<section class=char id=nex>
<figure class=portrait>
<a href=nex> <img src=nex.webp srcset='nex2x.webp 2x'> </a>
</figure>
<h2>
<a href=nex>nex</a>
</h2>
<p>
went to hell and back to stop the world from disintegrating. you know,
normal stuff.
(202024)
</section>
<section class=char id=kezda>
<figure class=portrait>
<a href=kezda> <img src=kezda.webp srcset='kezda2x.webp 2x'> </a>
</figure>
<h2>
<a href=kezda>kezda</a>
</h2>
<p>
little lizard who saw magic one time and never stopped thinking about it
<br>
(2019; <small>game abandoned but i still like this lil gremlin</small>)
</section>
<footer>
<a href=.. aria-label=back></a>
</footer>

BIN
dnd/kezda.webp (Stored with Git LFS) Normal file

Binary file not shown.

28
dnd/kezda/index.html Normal file
View file

@ -0,0 +1,28 @@
<!doctype html>
<html lang=en>
<meta charset=utf-8>
<title>kezda</title>
<link rel=stylesheet href=style.css>
<link rel=icon href=icon.webp>
<meta name=viewport content='width=device-width; initial-scale=0.75'>
<header>
<h1 id=top>kezda</h1>
</header>
<section id=info>
<h2>basic info</h2>
<figure id=char-pic class=mainfig>
<img src=kezda.webp srcset='kezda2x.webp 2x'
alt='kezda with their familar poking out of a hole in their hat'>
</figure>
a lil creature
</section>
<footer>
<a href=.. aria-label=back></a>
</footer>

BIN
dnd/kezda/kezda.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/kezda/kezda2x.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/kezda/src/kezda-full.kra (Stored with Git LFS) Normal file

Binary file not shown.

3
dnd/kezda/style.css Normal file
View file

@ -0,0 +1,3 @@
@import url(../bio.css);

BIN
dnd/kezda2x.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/marigold.webp (Stored with Git LFS) Normal file

Binary file not shown.

35
dnd/marigold/index.html Normal file
View file

@ -0,0 +1,35 @@
<!doctype html>
<html lang=en>
<meta charset=utf-8>
<title>marigold</title>
<link rel=stylesheet href=style.css>
<link rel=icon href=icon.webp>
<meta name=viewport content='width=device-width; initial-scale=0.75'>
<header>
<h1 id=top>marigold</h1>
</header>
<section id=info>
<h2>basic info</h2>
<table class=stats>
<caption> warlock of bahamut
<tbody>
<tr> <th> height <td> 198 cm (6ʹ 6ʺ)
<tr> <th> weight <td> 121 kg (267 ℔)
<tr> <th> age <td> 37
</table>
<p>
lets get it out of the way. her actual name is, um, lets see here…
<span class=ipa>[ˈqχḁʂx̩kɬ̩ːχ]</span>. which is why she goes by marigold
among the humanoids.
</section>
<footer>
<a href=.. aria-label=back></a>
</footer>

6
dnd/marigold/style.css Normal file
View file

@ -0,0 +1,6 @@
@import url(../bio.css);
.ipa {
font-feature-settings: "ss03" 1;
font-variation-settings: "ENLA" 0;
}

BIN
dnd/marigold2x.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/nex.kra-autosave.kra (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/nex.webp (Stored with Git LFS) Normal file

Binary file not shown.

22
dnd/nex/index.html Normal file
View file

@ -0,0 +1,22 @@
<!doctype html>
<html lang=en>
<meta charset=utf-8>
<title>nex</title>
<link rel=stylesheet href=../bio.css>
<link rel=icon href=icon.webp>
<meta name=viewport content='width=device-width; initial-scale=0.75'>
<header>
<h1 id=top>nex</h1>
</header>
<section id=info>
<h2>basic info</h2>
</section>
<footer>
<a href=.. aria-label=back></a>
</footer>

BIN
dnd/nex2x.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/src/kezda.kra (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/src/marigold.kra (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/src/nex.kra (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/src/velzek.kra (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek/armour.s.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek/armour.s2x.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek/clothes.s.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek/clothes.s2x.webp (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -8,14 +8,6 @@
<meta name=viewport content='width=device-width; initial-scale=0.75'> <meta name=viewport content='width=device-width; initial-scale=0.75'>
<meta property=og:type content=article>
<meta property=og:title content="velzek hawthorne">
<meta property=og:description content="kobold cleric and anthropologist">
<meta property=og:url content="https://niss.website/velzek/">
<meta property=og:image content="https://niss.website/velzek/icon.png">
<meta name=twitter:card content=summary_large_image>
<meta name=twitter:image content="https://niss.website/velzek/clothes.webp">
<header> <header>
<h1 id=top>velzek hawthorne</h1> <h1 id=top>velzek hawthorne</h1>
</header> </header>
@ -23,36 +15,33 @@
<section id=info> <section id=info>
<h2>basic info</h2> <h2>basic info</h2>
<figure id=velzek-pic> <figure id=char-pic class=mainfig>
<input type=checkbox id=switch> <input type=checkbox id=switch>
<label for=switch> switch </label> <label for=switch> switch </label>
<div id=velzek-img-holder> <div id=char-img-holder>
<a href=clothes.webp> <a href=clothes.webp id=pic1>
<img id=clothes-pic <img src=clothes.s.webp srcset='clothes.s2x.webp 2x'
src=clothes_small.webp srcset='clothes_small2x.webp 2x'
alt='velzek the kobold in green priest garments'> alt='velzek the kobold in green priest garments'>
</a> </a>
<a href=armour.webp> <a href=armour.webp id=pic2>
<img id=armour-pic <img src=armour.s.webp srcset='armour.s2x.webp 2x'
src=armour_small.webp srcset='armour_small2x.webp 2x'
alt='velzek the kobold in metal armour holding a mace'> alt='velzek the kobold in metal armour holding a mace'>
</a> </a>
</div> </div>
</figure> </figure>
<p> <table class=stats>
<strong>cleric of <a href=#suveesha>suveesha</a></strong> <caption>
and anthropologist cleric of <a href=#suveesha>suveesha</a>
<span class=and>and anthropologist</span>
<dl> <tbody>
<dt>height <dd> 74 cm (2ʹ 5ʺ) <tr> <th> height <td> 74 cm (2ʹ 5ʺ)
<dt>weight <dd> 18 kg (40 ℔) <tr> <th> weight <td> 18 kg (40 ℔)
<dt>age <dd> 32 <tr> <th> age <td> 32
<dt>year of birth <dd> 1187 <tr> <th> year of birth <td> 1187
<dt>alignment <dd> neutral good </table>
</dl>
<p> <p>
<dfn>velzek</dfn> is a kobold from a community called <dfn>velzek</dfn> 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, eight years ago, she and four other kobolds arrived at the temple of berei,
next to the green on the south border of marikest. next to the green on the south border of marikest.
<p> <table>
her companions are: <caption> velzeks companions
<tbody>
<dl> <tr> <th> yarva bitterbrush <td> 26, he/him
<dt> yarva bitterbrush <dd> 26, he/him <tr> <th> keshku æstivæ <td> 28, she/her
<!-- purshia tridentata --> <tr> <th> volek ruba <td> 23, she/her
<dt> keshku aestivae <dd> 28, she/her <tr> <th> tokil arceuthus <td> 25, he/him
<!-- æstīvus: summer, but also wheat is triticum aestivum --> </table>
<dt> volek ruba <dd> 23, she/her <!--
<!-- rubus: blackberry/raspberry --> purshia tridentata
<dt> tokil arceuthus <dd> 25, he/him æstīvus: summer, but also wheat is triticum aestivum
<!-- ἄρκευθος: juniper --> rubus: blackberry/raspberry
</dl> ἄρκευθος: juniper
-->
<p> <p>
until arriving in marikest, the concept of a surname was totally unknown to 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 the kobolds, so they all invented names based on plants for themselves once
they became needed. they became needed. in the city theyve been under the mentorship of a
halfling named <dfn>bobbie fairchild</dfn>.
<p> <p>
the purpose of the kobolds arrival in marikest is to study outside the purpose of the kobolds arrival in marikest is to study outside
@ -91,9 +82,9 @@
<section id=ekkel> <section id=ekkel>
<h2>ekkel</h2> <h2>ekkel</h2>
<figure id=ekkel-pic> <figure id=ekkel-pic class=mainfig>
<a href=map_k_full.webp> <a href=map_k_full.webp>
<img src=map_k.webp class=bordered <img src=map_k.s.webp srcset='map_k.s2x.webp 2x' class=bordered
alt="ekkel is north of the ruins of avaros dungeon" alt="ekkel is north of the ruins of avaros dungeon"
title="location of ekkel burrow"> title="location of ekkel burrow">
</a> </a>
@ -104,7 +95,7 @@
the windswept wall. despite the historical friction between kobolds and the windswept wall. despite the historical friction between kobolds and
humanoids, ekkel has enjoyed peace for decades, due to its location far away 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 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 dont consciously keep track of closer kinship
bonds. bonds.
<p> <p>
@ -126,8 +117,10 @@
<section id=suveesha> <section id=suveesha>
<h2>suveesha</h2> <h2>suveesha</h2>
<figure id=suveesha-pic> <figure id=suveesha-pic class=mainfig>
<a href=suveesha.webp><img src=suveesha.webp class=bordered></a> <a href=suveesha.webp>
<img src=suveesha.s.webp srcset='suveesha.s2x.webp 2x' class=bordered>
</a>
</figure> </figure>
<p> <p>
@ -146,10 +139,10 @@
<p> <p>
while amongst the humanoids, velzek and the others have been instructed to while amongst the humanoids, velzek and the others have been instructed to
adopt bereis symbols for familiarity: a bundle of wheat, a sickle, and a adopt bereis more familiar symbols: a bundle of wheat, a sickle, and a
rising sun. rising sun.
</section> </section>
<footer> <footer>
<a href=/index.html aria-label=back></a> <a href=.. aria-label=back></a>
</footer> </footer>

BIN
dnd/velzek/map_k.s.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek/map_k.s2x.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek/src/map_k_full.kra (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek/src/suveesha.kra (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek/src/velzek.kra (Stored with Git LFS) Normal file

Binary file not shown.

8
dnd/velzek/style.css Normal file
View file

@ -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%);
}
}

BIN
dnd/velzek/suveesha.s.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek/suveesha.s2x.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dnd/velzek2x.webp (Stored with Git LFS) Normal file

Binary file not shown.

25
rainbow-quox/edit.svg Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
width="15" height="15" viewBox="0 0 100 91.21">
<defs>
<linearGradient id="gradient" y1="100%" y2="0%">
<stop offset="20%" stop-color="hsl(60 90% 95%)" />
<stop offset="100%" stop-color="hsl(60 80% 90%)" />
</linearGradient>
<linearGradient id="fade">
<stop offset="25%" stop-color="white" />
<stop offset="87%" stop-color="black" />
</linearGradient>
<mask id="mask">
<rect fill="url(#fade)" x="30" y="80" width="70" height="20" />
</mask>
</defs>
<g fill="url(#gradient)">
<path id="pencil" d="M 70,0 l -70,70 v 21.21 h 21.21 l 70,-70 z" />
<path id="line" mask="url(#mask)" d="M 30,91.21 h 70 v -10 h -60 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 784 B

View file

@ -13,11 +13,11 @@ export class HistoryItem {
} }
asHtml(): HTMLButtonElement { asHtml(): HTMLButtonElement {
const { lines: bg, outer, belly1: belly, fins1: fins } = this.rgb; const { lines, outer, belly1: belly, fins1: fins } = this.rgb;
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">
<rect x=-10 y=-10 width=140 height=120 fill="${bg.css()}" /> <rect x=-10 y=-10 width=140 height=120 fill="${lines.css()}" />
<path fill="${fins.css()}" d="M 60,0 h -57.73 v 100 z"> <path fill="${fins.css()}" d="M 60,0 h -57.73 v 100 z">
<title>fin colour: ${fins.css()}</title> <title>fin colour: ${fins.css()}</title>
</path> </path>
@ -53,21 +53,23 @@ export class History {
add(name: string): void { this.items.push(name); } add(name: string): void { this.items.push(name); }
*iterNames(maxLength?: number | null): Iterable<string> { *iterNames(maxLength: number = 100): Iterable<string> {
let seen = new Set<string>; let seen = new Set<string>;
let done = 0; let done = 0;
if (maxLength === undefined) maxLength = 100;
for (let i = this.items.length - 1; i >= 0; i--) { 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]!; const name = this.items[i]!;
if (!name || seen.has(name)) continue; if (!name || seen.has(name)) continue;
seen.add(name); done++;
yield name; yield name;
seen.add(name);
done++;
} }
} }
*iterItems(maxLength?: number | null): Iterable<HistoryItem> { // pass a negative number to iterate over all
*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 oklch = Color.colors(new Color.Rand(name), Color.KNOWN[name]);
const rgbs = Color.toRgbs(oklch); const rgbs = Color.toRgbs(oklch);
@ -77,37 +79,26 @@ export class History {
} }
static validate(x: unknown): History | undefined { static validate(x: unknown): History | undefined {
if (!Array.isArray(x)) return; if (Array.isArray(x) && x.every(i => typeof i == 'string'))
if (!x.every(i => typeof i === 'string')) return;
return new History(x); 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; const storage = persist ? localStorage : sessionStorage;
storage.setItem('history', JSON.stringify(this)); storage.setItem('history', JSON.stringify(this));
} }
// if the json was invalid, return it // if no history exists, or it's invalid, just start a new one
// if no history exists just start a new one static load(): History {
static load(): History | string {
const json = const json =
sessionStorage.getItem('history') ?? sessionStorage.getItem('history') ??
localStorage.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 if (json === null) return new History;
static loadOrClear(): History {
const h = History.load(); return History.validate(JSON.parse(json)) ?? new History;
return h instanceof History ? h : new History;
} }
addSave(name: string, persist = true): void { addSave(name: string, persist = true): void {
@ -115,14 +106,9 @@ export class History {
this.save(persist); this.save(persist);
} }
prune(maxLength?: number | null) { prune(maxLength?: number): void {
let keep = []; let keep = [];
for (let name of this.iterNames(maxLength)) keep.push(name); for (let name of this.iterNames(maxLength)) keep.push(name);
this.items = keep.reverse(); this.items = keep.reverse();
} }
pruneSave(maxLength?: number | null, persist = true) {
this.prune(maxLength);
this.save(persist);
}
} }

View file

@ -0,0 +1,161 @@
import * as Color from './color.js';
async function loadBitmap(url: string): Promise<ImageBitmap> {
const img0 = new Image;
const img: Promise<ImageBitmapSource> = 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<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 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<A>(f: (l: Layer) => A): Record<Layer, A> {
return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record<Layer, A>;
}
export async function makeLayerInfoAsync<A>(f: (l: Layer) => Promise<A>):
Promise<Record<Layer, A>> {
let list = await Promise.all(allLayers.map(l => f(l).then(res => [l, res])));
return Object.fromEntries(list);
}
export function loadLayers(dir: string): Promise<Record<Layer, ImageData>> {
let buf = makeBufferIfLocks(WIDTH, HEIGHT);
return makeLayerInfoAsync(l => loadImageData(`./${dir}/${l}.webp`, buf));
}
export type Position = [x: number, y: number];
export type Positions = Record<Layer, Position>;
export async function loadPos(dir: string): Promise<Positions> {
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<Layer, [ImageData, Position]>;
export type Data = {
front: SideData, back: SideData,
frontImage?: ImageData, backImage?: ImageData,
};
export type ComposedData = Required<Data>;
export async function loadData(): Promise<Data> {
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<ImageData> {
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<ComposedData> {
let { front, back } = data;
data.frontImage ??= await composeLayers(front);
data.backImage ??= await composeLayers(back);
return data as ComposedData;
function composeLayers(sdata: SideData): Promise<ImageData> {
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);
}

View file

@ -0,0 +1,64 @@
import { Rgb, Rgbs, rgb } from './color.js';
import { Layer } from './layer.js';
export type Color =
Exclude<Layer, 'eyeshine' | 'stroke' | 'static'>
| '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<Record<Color, string>> = {
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<Color, Layer>;
export const STATIC_COLS: Record<StaticColor, Rgb> = {
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<Record<Color, Rgb>>;
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' });
}

View file

@ -1,177 +1,16 @@
import * as Color from './color.js'; import * as Color from './color.js';
import { History } from './history.js'; import { History } from './history.js';
import * as Layer from './layer.js';
import * as Palette from './palette.js';
async function loadBitmap(url: string): Promise<ImageBitmap> { function message(msg: string, size = 100) {
const img0 = new Image;
const img: Promise<ImageBitmapSource> = 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<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);
}
function loadImageData(url: string, buf?: Buffer): Promise<ImageData> {
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<A>(f: (l: Layer) => A): Record<Layer, A> {
return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record<Layer, A>;
}
async function makeLayerInfoAsync<A>(f: (l: Layer) => Promise<A>):
Promise<Record<Layer, A>> {
let list = await Promise.all(allLayers.map(l => f(l).then(res => [l, res])));
return Object.fromEntries(list);
}
function loadLayers(dir: string): Promise<Record<Layer, ImageData>> {
let buf = makeBufferIfLocks(WIDTH, HEIGHT);
return makeLayerInfoAsync(l => loadImageData(`./${dir}/${l}.webp`, buf));
}
type Position = [x: number, y: number];
type Positions = Record<Layer, Position>;
async function loadPos(dir: string): Promise<Positions> {
return (await fetch(`./${dir}/pos.json`)).json();
}
type Side = 'front' | 'back';
function swapSide(s: Side): Side {
return s == 'front' ? 'back' : 'front';
}
type SideData = Record<Layer, [ImageData, Position]>;
type LayerData = {
front: SideData, back: SideData,
frontImage?: ImageData, backImage?: ImageData,
};
type ComposedData = Required<LayerData>;
async function loadData(): Promise<LayerData> {
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<ImageData> {
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<ComposedData> {
let { front, back } = data;
data.frontImage ??= await composeLayers(front);
data.backImage ??= await composeLayers(back);
return data as ComposedData;
function composeLayers(sdata: SideData): Promise<ImageData> {
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) {
const ctx = getCanvasCtx('main'); const ctx = getCanvasCtx('main');
const size = error ? 30 : 100;
ctx.save(); 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.font = `bold ${size}px Muller, sans-serif`;
ctx.textAlign = 'center'; 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(); ctx.restore();
} }
@ -187,19 +26,19 @@ function updateUrl(seed: string): void {
type ApplyStateOpts = { type ApplyStateOpts = {
seed: string, seed: string,
side?: Side, side?: Layer.Side,
firstLoad?: boolean, firstLoad?: boolean,
buf?: Buffer, buf?: Layer.Buffer,
history?: History, history?: History,
done?: Done, done?: Done,
}; };
async function async function
applyState(data: LayerData, opts: ApplyStateOpts): Promise<string> { applyState(data: Layer.Data, opts: ApplyStateOpts): Promise<string> {
let { side, seed, firstLoad, buf, history, done } = opts; let { side, seed, firstLoad, buf, history, done } = opts;
side ??= 'front'; side ??= 'front';
firstLoad ??= false; firstLoad ??= false;
buf ??= new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!; buf ??= Layer.makeBuffer();
done ??= () => {}; done ??= () => {};
let rand = new Color.Rand(seed); let rand = new Color.Rand(seed);
@ -208,7 +47,7 @@ applyState(data: LayerData, opts: ApplyStateOpts): Promise<string> {
const rgb = Color.toRgbs(oklch); const rgb = Color.toRgbs(oklch);
const newSeed = rand.alphaNum(); const newSeed = rand.alphaNum();
await recolorLayers(data, rgb); await Layer.recolorAll(data, rgb);
updateBg(oklch); updateBg(oklch);
updateSvgs(oklch, rgb); updateSvgs(oklch, rgb);
@ -216,7 +55,7 @@ applyState(data: LayerData, opts: ApplyStateOpts): Promise<string> {
updateUrl(seed); updateUrl(seed);
if (firstLoad) { if (firstLoad) {
await instantUpdateImage(side, await ensureComposed(buf, data)); await instantUpdateImage(side, await Layer.ensureComposed(buf, data));
done(); done();
} else { } else {
await animateUpdateImage(buf, side, data, done); await animateUpdateImage(buf, side, data, done);
@ -235,7 +74,7 @@ function getCanvasCtx(id: CanvasId) {
} }
async function async function
instantUpdateImage(side: Side, data: ComposedData) { instantUpdateImage(side: Layer.Side, data: Layer.ComposedData) {
getCanvasCtx('main').putImageData(data[`${side}Image`], 0, 0); getCanvasCtx('main').putImageData(data[`${side}Image`], 0, 0);
} }
@ -244,9 +83,10 @@ type Done = () => void;
const noAnim = matchMedia('(prefers-reduced-motion: reduce)'); const noAnim = matchMedia('(prefers-reduced-motion: reduce)');
async function 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) { if (noAnim.matches) {
instantUpdateImage(side, await ensureComposed(buf, data)); instantUpdateImage(side, await Layer.ensureComposed(buf, data));
done(); done();
return; return;
} }
@ -257,11 +97,11 @@ animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) {
const aux = getCanvasCtx('aux'); const aux = getCanvasCtx('aux');
document.documentElement.dataset.running = 'reroll'; document.documentElement.dataset.running = 'reroll';
const cdata = await ensureComposed(buf, data); const cdata = await Layer.ensureComposed(buf, data);
redraw(aux, buf, cdata, side); Layer.redraw(aux, buf, cdata, side);
aux.canvas.addEventListener('animationend', async () => { aux.canvas.addEventListener('animationend', async () => {
await redraw(main, buf, cdata, side); await Layer.redraw(main, buf, cdata, side);
aux.canvas.style.removeProperty('animation'); aux.canvas.style.removeProperty('animation');
delete document.documentElement.dataset.running; delete document.documentElement.dataset.running;
done(); done();
@ -273,7 +113,8 @@ animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) {
} }
async function 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) { if (noAnim.matches) {
instantUpdateImage(newSide, data); instantUpdateImage(newSide, data);
done(); done();
@ -286,10 +127,10 @@ animateSwapImage(buf: Buffer, newSide: Side, data: ComposedData, done: Done) {
const aux = getCanvasCtx('aux'); const aux = getCanvasCtx('aux');
document.documentElement.dataset.running = 'swap'; document.documentElement.dataset.running = 'swap';
await redraw(aux, buf, data, newSide); await Layer.redraw(aux, buf, data, newSide);
aux.canvas.addEventListener('animationend', async () => { 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.putImageData(image, 0, 0);
main.canvas.style.removeProperty('animation'); 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<ApplyStateOpts, 'seed' | 'history'>) { opts: Omit<ApplyStateOpts, 'seed' | 'history'>) {
const list = document.getElementById('history-items'); const list = document.getElementById('history-items');
if (!list) return; if (!list) return;
@ -369,7 +210,7 @@ function showHistory(history: History, data: LayerData,
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'); let 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';
@ -377,18 +218,7 @@ function closeHistory() {
function download(seed: string) { function download(seed: string) {
const colors = Color.toRgbs(Color.colors(new Color.Rand(seed))); const colors = Color.toRgbs(Color.colors(new Color.Rand(seed)));
const blob = Palette.make(seed, colors);
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' });
// there must be a better way to push out a file than // there must be a better way to push out a file than
// this autohotkey-ass nonsense // this autohotkey-ass nonsense
@ -403,15 +233,15 @@ function download(seed: string) {
async function setup() { async function setup() {
message('loading layers…'); message('loading layers…');
let data = await loadData().catch(e => { message(e, true); throw e }); let data = await Layer.loadData().catch(e => { message(e, 30); throw e });
let history = History.loadOrClear(); let history = History.load();
let buf = new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!; let buf = Layer.makeBuffer();
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, buf, history, firstLoad: true });
let side: Side = 'front'; let side: Layer.Side = 'front';
const reroll = document.getElementById('reroll')!; const reroll = document.getElementById('reroll')!;
const swap = document.getElementById('swap')!; const swap = document.getElementById('swap')!;
@ -473,8 +303,8 @@ async function setup() {
} }
function runSwap() { function runSwap() {
run(async k => { run(async k => {
side = swapSide(side); side = Layer.swapSide(side);
const cdata = await ensureComposed(buf, data); const cdata = await Layer.ensureComposed(buf, data);
await animateSwapImage(buf, side, cdata, k); await animateSwapImage(buf, side, cdata, k);
}); });
} }

View file

@ -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 <button>
font: 700 20pt var(--font);
padding: 0.2rem 0.5rem;
background-blend-mode: hard-light;
border: 3px solid oklch(0.2 0.05 var(--hue));
}
@mixin button {
@include box-base;
background: $box-texture, $button-bg;
color: $button-fg;
}
@mixin box {
@include box-base;
background: $box-texture, $box-bg;
color: $box-fg;
}
@mixin image-button {
@include button;
padding: 5px;
> * { display: block; }
}
@mixin nested-image-button {
@include image-button;
border: 2px solid $button-fg;
}
$arrowhead:
conic-gradient(from -124deg at 100% 50%,
currentcolor, currentcolor 68deg, transparent 68deg);
@mixin arrow-button {
@include button;
&::before {
content: '';
display: inline-block;
height: 1.25ex; aspect-ratio: 2/3;
background: $arrowhead;
margin-right: 0.5ex;
}
}

BIN
velzek/armour_small.webp (Stored with Git LFS)

Binary file not shown.

BIN
velzek/armour_small2x.webp (Stored with Git LFS)

Binary file not shown.

BIN
velzek/clothes_small.webp (Stored with Git LFS)

Binary file not shown.

BIN
velzek/clothes_small2x.webp (Stored with Git LFS)

Binary file not shown.

BIN
velzek/map_k.webp (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/armour.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/armour_small.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/armour_small2x.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/clothes.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/clothes_small.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/clothes_small2x.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/icon.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/map_k.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/map_k_full.kra (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/map_k_full.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/suveesha.kra (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/suveesha.png (Stored with Git LFS)

Binary file not shown.

BIN
velzek/src/velzek icon.kra (Stored with Git LFS)

Binary file not shown.