+
+
+
+
+
diff --git a/rainbow-quox/kra/back.kra b/rainbow-quox/kra/back.kra
index a01a414..7bb3f4d 100644
--- a/rainbow-quox/kra/back.kra
+++ b/rainbow-quox/kra/back.kra
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ec8adde036f86be12b8517ff1cdc227ad3dcaaf9e0a0e9e186d060d503fac573
-size 11558980
+oid sha256:75dc67e9a6a3a7807ad39f3b564b841c0a5335ed1ceae6a750ea748d300e3c33
+size 10674968
diff --git a/rainbow-quox/kra/front.kra b/rainbow-quox/kra/front.kra
index 7fd8554..2dfbb5f 100644
--- a/rainbow-quox/kra/front.kra
+++ b/rainbow-quox/kra/front.kra
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:579ca5af9a79a8577f62013418f58c729c646cfda9f2fd2bed54688b59de2ddd
-size 11346742
+oid sha256:047b1eb6e703a2c8d67d6ea3a83b942a8636da501b924d8d6a261f3c2f5f1544
+size 11122381
diff --git a/rainbow-quox/make-palette/cabal.project b/rainbow-quox/make-palette/cabal.project
new file mode 100644
index 0000000..9a93c76
--- /dev/null
+++ b/rainbow-quox/make-palette/cabal.project
@@ -0,0 +1,9 @@
+packages: ./make-palette.cabal
+
+source-repository-package
+ type: git
+ location: https://git.rhiannon.website/rhi/svg-builder
+ tag: 9c09fcea4ac316dd5e0709b40f85952047070bf1
+
+shared: False
+executable-dynamic: False
diff --git a/rainbow-quox/make-palette/make-palette.cabal b/rainbow-quox/make-palette/make-palette.cabal
new file mode 100644
index 0000000..0ab338e
--- /dev/null
+++ b/rainbow-quox/make-palette/make-palette.cabal
@@ -0,0 +1,12 @@
+cabal-version: 3.0
+
+name: make-palette
+version: 0
+
+executable make-palette
+ hs-source-dirs: .
+ build-depends: base, text, svg-builder
+ default-language: GHC2024
+ default-extensions: OverloadedStrings, PatternSynonyms
+ main-is: make-palette.hs
+ ghc-options: -Wall
diff --git a/rainbow-quox/make-palette/make-palette.hs b/rainbow-quox/make-palette/make-palette.hs
new file mode 100644
index 0000000..1634793
--- /dev/null
+++ b/rainbow-quox/make-palette/make-palette.hs
@@ -0,0 +1,147 @@
+import Graphics.Svg
+import Data.Text (Text)
+import Data.Text qualified as Text
+import Data.Maybe
+import System.Environment
+import Data.String (IsString (..))
+
+main :: IO ()
+main = do
+ [out] <- getArgs
+ renderToFile out document
+
+
+document :: Element
+document =
+ svg11_ (stylesheet "style/palette.css" <> foldMap makeItem (zip [0..] items))
+ `with` [Width_ <<- width, Height_ <<- "179",
+ ViewBox_ <<- viewBox]
+ where
+ width = tshow $ 53 + 100 * length items
+ viewBox = "-53 -1 " <> width <> " 179"
+
+
+items :: [Swatch]
+items =
+ ["outer" `WithMinor` "vitiligo1",
+ "spines" `WithUse` "vitiligo1",
+ "stripes" `As` "socks" `WithMinor` "cuffs",
+ "belly1" `WithMinor` "vitiligo3",
+ "belly2" `WithMinor` "vitiligo2",
+ "fins1" `WithMinor` "vitiligo4",
+ "fins2" `WithUse` "vitiligo4",
+ "fins3" `WithUse` "vitiligo4",
+ Only "masks", Only "claws", Only "eyes"
+ ]
+
+data Layer = Layer Text (Maybe Text)
+instance IsString Layer where fromString l = Layer (fromString l) Nothing
+pattern As :: Text -> Text -> Layer
+pattern txt `As` disp = Layer txt (Just disp)
+
+data Swatch =
+ Only Layer
+ | WithMinor Layer Layer
+ | WithUse Layer Text
+
+makeItem :: (Int, Swatch) -> Element
+makeItem (i, swatch) = case swatch of
+ Only (Layer l d) -> makeOnly l d i
+ WithMinor (Layer l1 d1) (Layer l2 d2) -> makeMinor l1 d1 l2 d2 i
+ WithUse (Layer l1 d1) l2 -> makeUse l1 d1 l2 i
+
+data SwatchCount = One | Two
+
+data SwatchPos = SOnly | SFirst | SSecond
+
+data RectType = RMaj | RMin | ROnly
+
+
+stylesheet :: Text -> Element
+stylesheet path =
+ makeElementNoEnd "link" `with`
+ [makeAttribute "xmlns" "http://www.w3.org/1999/xhtml",
+ makeAttribute "rel" "stylesheet",
+ makeAttribute "href" path,
+ Type_ <<- "text/css"]
+
+makeOnly :: Text -> Maybe Text -> Int -> Element
+makeOnly layer display index =
+ itemGroup layer index
+ [makeRect layer ROnly,
+ makeTextBg layer,
+ makeName (fromMaybe layer display) One,
+ makeHex layer SOnly]
+
+makeMinor :: Text -> Maybe Text -> Text -> Maybe Text -> Int -> Element
+makeMinor layer1 display1 layer2 _display2 index =
+ itemGroup layer1 index
+ [makeRect layer2 RMin,
+ makeRect layer1 RMaj,
+ makeTextBg layer1,
+ makeName (fromMaybe layer1 display1) Two,
+ makeHex layer1 SFirst,
+ makeHex layer2 SSecond]
+
+makeUse :: Text -> Maybe Text -> Text -> Int -> Element
+makeUse layer1 display1 layer2 index =
+ itemGroup layer1 index
+ [use_ [makeAttribute "href" $ "#s-" <> layer2],
+ makeRect layer1 RMaj,
+ makeTextBg layer1,
+ makeName (fromMaybe layer1 display1) Two,
+ makeHex layer1 SFirst,
+ use_ [makeAttribute "href" $ "#c-" <> layer2]]
+
+itemGroup :: Text -> Int -> [Element] -> Element
+itemGroup layer index elt =
+ g_ [Class_ <<- "item", Id_ <<- "i-" <> layer,
+ Transform_ <<- itemTranslate index]
+ (mconcat elt)
+
+makeRect :: Text -> RectType -> Element
+makeRect layer typ =
+ rect_ [Class_ <<- classes, Id_ <<- "s-" <> layer,
+ Fill_ <<- "pink",
+ X_ <<- "0", Y_ <<- y,
+ Width_ <<- "80", Height_ <<- height]
+ where
+ classes = "swatch " <>
+ case typ of RMaj -> "maj"; RMin -> "min"; ROnly -> "only"
+ height = case typ of RMaj -> "50"; RMin -> "25"; ROnly -> "80"
+ y = case typ of RMaj -> "30"; RMin -> "0"; ROnly -> "0"
+
+makeTextBg :: Text -> Element
+makeTextBg layer =
+ polygon_ [Class_ <<- "text-bg", Id_ <<- "p-" <> layer,
+ Fill_ <<- "pink",
+ points [(0,85), (80,85), (28,175), (-52,175)]]
+
+makeName :: Text -> SwatchCount -> Element
+makeName display count =
+ text_ [Class_ <<- "name", Text_anchor_ <<- "end",
+ X_ <<- tshow x, Y_ <<- "100", textTransform x] $
+ toElement display
+ where x = case count of One -> 22; Two -> 18
+
+makeHex :: Text -> SwatchPos -> Element
+makeHex layer count =
+ text_ [Class_ <<- "hex", Id_ <<- "c-" <> layer, Text_anchor_ <<- "end",
+ X_ <<- tshow x, Y_ <<- "100", textTransform x] $
+ "#000000"
+ where x = case count of SOnly -> 51; SFirst -> 42; SSecond -> 64
+
+textTransform :: Int -> Attribute
+textTransform x =
+ Transform_ <<- rotateAround @Double (-60) (fromIntegral x) 100
+
+
+itemTranslate :: Int -> Text
+itemTranslate i = translate @Double (fromIntegral $ i * 100) 0
+
+points :: [(Int, Int)] -> Attribute
+points = (Points_ <<-) . Text.unwords .
+ map (\(x, y) -> Text.intercalate "," [tshow x, tshow y])
+
+tshow :: Show a => a -> Text
+tshow = Text.pack . show
diff --git a/rainbow-quox/script/color.ts b/rainbow-quox/script/color.ts
new file mode 100644
index 0000000..1cd1d39
--- /dev/null
+++ b/rainbow-quox/script/color.ts
@@ -0,0 +1,512 @@
+import * as R from './rand.js';
+
+const max = Math.max;
+const min = Math.min;
+
+export type Luma = number;
+export type Chroma = number;
+export type Hue = number;
+export type Alpha = number;
+
+export type HueDistance = number;
+
+const MAXL: Luma = 0.9;
+const MINL: Luma = 0.4;
+const MINL_LIGHT: Luma = 0.7;
+const MAXL_DARK: Luma = 0.65;
+
+const MINC_LIGHT: Chroma = 0.08;
+const MAXC_LIGHT: Chroma = 0.1;
+const MINC_DARK: Chroma = 0.12;
+const MAXC_DARK: Chroma = 0.175;
+
+// max spread for a sequence of analogous colors. unless that would put them
+// too close together
+const MAXH_WIDTH: HueDistance = 80;
+
+// minimum distance between adjacent analogous colors
+const MINH_SEP: HueDistance = 5;
+
+// size of the wedge a "complementary" color can be in
+const MAXH_COMPL: HueDistance = 40;
+
+// size of the wedge a "triadic" color can be in
+const MAXH_TRIAD: HueDistance = 25;
+
+
+type LD = 'light' | 'dark';
+
+export namespace Oklch {
+ export type Channel = 'l' | 'c' | 'h';
+ export type Channels = Record