Compare commits
2 commits
898c7753a4
...
dff263856c
Author | SHA1 | Date | |
---|---|---|---|
dff263856c | |||
cad73b232d |
71 changed files with 844 additions and 402 deletions
7
Makefile
7
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 = \
|
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)
|
||||||
|
|
|
@ -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) {
|
small { font-stretch: 100%; }
|
||||||
dl {
|
}
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
@layer base.headings {
|
||||||
column-gap: 1em;
|
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 {
|
@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
38
dnd/bio.css
Normal 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
71
dnd/index.css
Normal 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
83
dnd/index.html
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang=en>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
<title>d&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>&</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.
|
||||||
|
(2020–24)
|
||||||
|
</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
BIN
dnd/kezda.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
28
dnd/kezda/index.html
Normal file
28
dnd/kezda/index.html
Normal 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
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
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
BIN
dnd/kezda/src/kezda-full.kra
(Stored with Git LFS)
Normal file
Binary file not shown.
3
dnd/kezda/style.css
Normal file
3
dnd/kezda/style.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@import url(../bio.css);
|
||||||
|
|
||||||
|
|
BIN
dnd/kezda2x.webp
(Stored with Git LFS)
Normal file
BIN
dnd/kezda2x.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
dnd/marigold.webp
(Stored with Git LFS)
Normal file
BIN
dnd/marigold.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
35
dnd/marigold/index.html
Normal file
35
dnd/marigold/index.html
Normal 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>
|
||||||
|
let’s get it out of the way. her actual name is, um, let’s 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
6
dnd/marigold/style.css
Normal 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
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
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
BIN
dnd/nex.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
22
dnd/nex/index.html
Normal file
22
dnd/nex/index.html
Normal 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
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
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
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
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
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
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
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
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
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
BIN
dnd/velzek/clothes.s2x.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
|
@ -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> velzek’s 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 they’ve 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 avaro’s dungeon"
|
alt="ekkel is north of the ruins of avaro’s 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 don’t 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 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.
|
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
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
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
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
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
BIN
dnd/velzek/src/velzek.kra
(Stored with Git LFS)
Normal file
Binary file not shown.
8
dnd/velzek/style.css
Normal file
8
dnd/velzek/style.css
Normal 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
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
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
BIN
dnd/velzek2x.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
25
rainbow-quox/edit.svg
Normal file
25
rainbow-quox/edit.svg
Normal 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 |
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
161
rainbow-quox/script/layer.ts
Normal file
161
rainbow-quox/script/layer.ts
Normal 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);
|
||||||
|
}
|
64
rainbow-quox/script/palette.ts
Normal file
64
rainbow-quox/script/palette.ts
Normal 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' });
|
||||||
|
}
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
71
rainbow-quox/style/defs.scss
Normal file
71
rainbow-quox/style/defs.scss
Normal 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)
BIN
velzek/armour_small.webp
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/armour_small2x.webp
(Stored with Git LFS)
BIN
velzek/armour_small2x.webp
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/clothes_small.webp
(Stored with Git LFS)
BIN
velzek/clothes_small.webp
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/clothes_small2x.webp
(Stored with Git LFS)
BIN
velzek/clothes_small2x.webp
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/map_k.webp
(Stored with Git LFS)
BIN
velzek/map_k.webp
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/armour.png
(Stored with Git LFS)
BIN
velzek/src/armour.png
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/armour_small.png
(Stored with Git LFS)
BIN
velzek/src/armour_small.png
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/armour_small2x.png
(Stored with Git LFS)
BIN
velzek/src/armour_small2x.png
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/clothes.png
(Stored with Git LFS)
BIN
velzek/src/clothes.png
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/clothes_small.png
(Stored with Git LFS)
BIN
velzek/src/clothes_small.png
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/clothes_small2x.png
(Stored with Git LFS)
BIN
velzek/src/clothes_small2x.png
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/icon.png
(Stored with Git LFS)
BIN
velzek/src/icon.png
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/map_k.png
(Stored with Git LFS)
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)
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)
BIN
velzek/src/map_k_full.png
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/suveesha.kra
(Stored with Git LFS)
BIN
velzek/src/suveesha.kra
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/suveesha.png
(Stored with Git LFS)
BIN
velzek/src/suveesha.png
(Stored with Git LFS)
Binary file not shown.
BIN
velzek/src/velzek icon.kra
(Stored with Git LFS)
BIN
velzek/src/velzek icon.kra
(Stored with Git LFS)
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue