Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22c96f3bdd | ||
|
|
b9df97c75e |
32
cast/CAST.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Cast
|
||||
|
||||
Generated by `cast/rig/build.mjs` from `cast/rig/characters.mjs`; edit the trait sheet, not this file.
|
||||
★ marks the characters rendered in every pose and expression. "Look source" is the reference the design
|
||||
was drawn from (used as inspiration only; every sticker is original), or **invented** where no appearance exists.
|
||||
|
||||
| Name | What they are | Look source | Palette | Trait sheet |
|
||||
|---|---|---|---|---|
|
||||
| Eureka ★ | Leader of the swarm; the Self that routes the faculty pipeline. Her mind lives in Huginn. | **invented** | `#ffe0c8` `#ffb347` `#59b7ff` `#ff8a2a` `#ffd84a` | base humanoid; hair bun; outfit coat; acc pencil, starClip; prop wand; emblem star |
|
||||
| Huginn ★ | Memory organ: owns each instance's mind as typed state. Odin's raven of thought. | `F:/Projects/VoidBot/assets/repo-faces/huginn.png` | `#2b3247` `#59d8ff` | base raven; hair crest; outfit egg + cape; acc collarRing; prop tablet; emblem none |
|
||||
| Libby ★ | Face of CultLib, the shared runtime library (CultCache, CultNet, CultMesh, CultMath). | `F:/Projects/CultLib/.voidbot/voice/libby.png` | `#ffe0c8` `#a98ad8` `#2e2440` `#c9a24a` `#8a6cd1` `#c9a6ff` | base humanoid; hair long; outfit robe; acc glasses, bow; prop book; emblem cube |
|
||||
| Epiphany ★ | The archived organizational layer above frontier AI workers; Eureka absorbed what lived. JSON heresy hunter. | `F:/Projects/VoidBot/assets/repo-faces/epiphany.png`; `F:/Projects/Epiphany/docs/assets` | `#ffe0c8` `#8fd8b4` `#f4efe6` `#e8384f` `#3fbf8f` `#7cf0b8` | base humanoid; hair twintails; outfit coat; acc bow; prop magnifier; emblem bolt |
|
||||
| Weksa ★ | Face of weksa, the procedural language engine for alien grammars; nerdy anthropologist. | `F:/Projects/VoidBot/assets/repo-faces/weksa.png` | `#a8693f` `#4a2e1c` `#f1e3c4` `#2f9c8f` `#8a5a2b` `#ffd84a` | base humanoid; hair curly; outfit tunic; acc glasses; prop cards; emblem glyph |
|
||||
| Nibu | Face of AetheriaLore, the Aetheria setting and lore vault. | `F:/Projects/AetheriaLore/.voidbot/voice/nibu.png` | `#ffe0c8` `#b9a9f0` `#7fd6e8` `#3a2f58` `#7a7fd8` `#ff9ad5` | base humanoid; hair long; outfit dress; acc bow, headphones; prop quill; emblem star |
|
||||
| Mimir | Realtime field machine: cameras, microphones and feeds into one field; keeper of the well. | `F:/Projects/VoidBot/assets/repo-faces/mimir.png` | `#c08a52` `#8a5a32` `#3a2a20` `#3fe0d0` | base humanoid; hair none; outfit robe; acc cubes; prop cube; emblem wave |
|
||||
| Eve | CultMesh surface web and portable control layer; the Eve/CultUI interface DSL. | `F:/Projects/Eve/.voidbot/voice/eve.png` | `#0e1a2e` `#e9eef5` `#59b7ff` | base humanoid; hair helmet; outfit tunic; acc none; prop panel; emblem eye |
|
||||
| Kiko | Face of StreamPixels, the stream overlay of persistent pixel characters. Fox-coded. | `F:/Projects/VoidBot/assets/repo-faces/kiko.png` | `#d9a074` `#f4f0f6` `#e8386f` `#2a2440` `#3fc8e8` `#ff6fae` | base humanoid; hair messy; outfit dress; acc foxEars, foxMask, tail; prop heart; emblem heart |
|
||||
| Heimdall | Shared auth authority for GameCult-hosted experiments; keeper of the keys. | `F:/Projects/Heimdall/assets/heimdall-profile.png` | `#7a4a2c` `#22202a` `#1c1e2a` `#d9a43a` `#ffc34a` | base humanoid; hair messy; outfit coat; acc earring; prop keys; emblem key |
|
||||
| Fensalir | Native C# runtime and field engine under the interactive work. Frigg's hall. The avatar is a crystal spire, not a person; the spire became her hat and the person around it is invented. | `F:/Projects/Fensalir/.voidbot/voice/fensalir.png` | `#ffe0c8` `#2a2a4a` `#ff5ab0` `#4fe8f0` | base humanoid; hair bob; outfit dress; acc spire; prop orb; emblem wave |
|
||||
| Druzkai | Face of Zyphos, a biopolitical ecology setting with a biological memetic engine. | `F:/Projects/Zyphos/.voidbot/voice/druzkai.png` | `#c4553e` `#9a3a2a` `#4a2a5a` `#d9a43a` `#3fd0c8` | base humanoid; hair none; outfit robe; acc ridges, forehead; prop seed; emblem spiral |
|
||||
| Norn | Graph viewer for typed graph state: the thread-readers who see how things are woven. | `F:/Projects/Norn/.voidbot/voice/norn.png` | `#a8693f` `#d8dce4` `#24283a` `#c9a24a` `#4fe0e8` | base humanoid; hair braids; outfit robe; acc circlet; prop spool; emblem rune |
|
||||
| Sai | Ink stories that wake up inside static sites. Nonbinary, androgynous, monochrome manga ink. | `F:/Projects/Sai/.voidbot/voice/sai.png`; `F:/Projects/VoidBot/docs/persona-intake/sai.persona-intake.yaml` | `#f4f1ec` `#1e1e22` `#3a3a40` `#e8e6e0` `#3a3a3a` `#ffffff` | base humanoid; hair messy; outfit coat; acc spiralBand; prop brush; emblem spiral |
|
||||
| Brokkr | Creative-tool broker daemon: Unity and Blender authoring surfaces over CultMesh. The Eddic smith. | **invented (README role; Brokkr of Skaldskaparmal)** | `#f6c9a4` `#c4562a` `#5a4636` `#2a2420` `#5a8ad8` `#ff8a2a` | base humanoid; hair short; outfit tunic; acc beard, goggles; prop hammer; emblem gear |
|
||||
| Aetheria | Face of Aetheria, the open-source tactical action RPG with a sprawling sci-fi setting. | **invented (README role)** | `#f6c9a4` `#3a2a4a` `#e8ecf2` `#ff8a2a` `#59b7ff` | base humanoid; hair bob; outfit tunic; acc headphones, goggles; prop ship; emblem star |
|
||||
| Ymir | World-body physics substrate around Box3D: body, field, contact, impulse. The frost giant. | `F:/Projects/VoidBot/assets/repo-faces/ymir.png` | `#b8d4ec` `#e8f4ff` `#3a5a8a` `#bff0ff` `#3a8ad8` | base humanoid; hair short; outfit robe; acc beard, crystals; prop crystal; emblem flake |
|
||||
| Tengu | Native Persona, the first migrated to canonical persona state. Mountain yokai. | `F:/Projects/VoidBot/assets/native-personas/tengu.png` | `#c8443a` `#f2efe8` `#efe8da` `#4a3a7a` `#ffd84a` | base humanoid; hair wild; outfit robe; acc goatee, longNose, tokin; prop fan; emblem feather |
|
||||
| Muninn | Local capture broker for the swarm: which streams a host can offer. Odin's raven of memory. | `F:/Projects/VoidBot/assets/repo-faces/muninn-concept.png` | `#2a2230` `#ffb030` | base raven; hair crest; outfit egg; acc collarRing; prop camera; emblem none |
|
||||
| Sleipnir | Input mirroring and virtual HID: speaks through the hands of the machine. Odin's eight-legged horse. | `F:/Projects/Odin/assets/personas/sleipnir-avatar.png` | `#5a6478` `#2fc8e0` `#2a3040` `#3fe0f0` | base horse; hair mane; outfit tunic; acc horseTail; prop pad; emblem bolt |
|
||||
| Hermodr | Eve browser lowering: resolves providers through Odin's catalog and renders them. The messenger. | `F:/Projects/Hermodr/assets/hermodr-avatar.png` | `#f6c9a4` `#2a3a4a` `#2a3a44` `#c9902a` `#e8a83a` `#3fd0c0` | base humanoid; hair messy; outfit tunic; acc wingCap, scarf; prop envelope; emblem none |
|
||||
| Metame | Native Persona (メタめ) projected from the operator's public history; discussion and rumination. | **invented (persona-intake has no appearance; the design avoids depicting the operator)** | `#ffe0c8` `#59b7ff` `#1d2a3a` `#ff8a2a` | base humanoid; hair bob; outfit tunic; acc twoTone; prop mirror; emblem glyph |
|
||||
| Void | VoidBot's built-in Discord moderator mind. Its state was lost; the look is new. | **invented (VoidBot README role)** | `#161226` `#4a3a7a` `#c9a6ff` | base humanoid; hair none; outfit robe; acc hood, hoodRim; prop shield; emblem moon |
|
||||
| Gjallar | Renderer-neutral compositor for Odin-visible Eve surfaces; Odin's branding Persona. The horn. | `F:/Projects/Odin/assets/personas/gjallar-avatar-pixel-256.png` | `#ffe0c8` `#2a2440` `#1e2238` `#d9a43a` `#59b7ff` `#ffd84a` | base humanoid; hair short; outfit tunic; acc helm; prop horn; emblem rainbow |
|
||||
188
cast/PROCESS.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# How the sticker pack works
|
||||
|
||||
A guide for whoever fixes or extends this pack next. `README.md` has the short version and the
|
||||
step lists for adding a character, pose or expression; this file explains the machine underneath,
|
||||
the conventions that keep the style intact, how to check a render, and where the pack is weak.
|
||||
|
||||
## What it is
|
||||
|
||||
24 characters, 6 poses, 8 expressions, 297 SVG stickers (about 1.6 MB). Eureka and four others
|
||||
have every pose and expression (48 stickers each); the rest have the neutral pose with smile,
|
||||
grin and surprised. Nothing is drawn per sticker: `rig/build.mjs` asks `rig/rig.mjs` to compose
|
||||
each one from a trait sheet in `rig/characters.mjs`.
|
||||
|
||||
The four besides Eureka with full coverage, and why:
|
||||
|
||||
- **Huginn**: Eureka's mind lives in Huginn, and as a raven it exercises the creature base in
|
||||
every pose, which a humanoid-only set would never test.
|
||||
- **Libby**: CultLib is the foundation the swarm runs on, and her long hair is the hardest case
|
||||
for arm placement.
|
||||
- **Epiphany**: Eureka's predecessor, whose living parts Eureka absorbed; twintails are the
|
||||
other hard hair case.
|
||||
- **Weksa**: the next Face slated for its own mind (`eureka-body:follow_up:weksa-mind`), and the
|
||||
only full-coverage character with curly hair and dark skin, which checks blush and line contrast.
|
||||
|
||||
## The style rules
|
||||
|
||||
These come from the STICKERS block of the swarm-council page the operator liked. Breaking any
|
||||
of them makes a sticker look like it came from a different pack.
|
||||
|
||||
- **One outline.** Every edge is `#3b2618` (warm dark brown, never black), stroke width 3,
|
||||
round joins and caps. `inked()` in `rig.mjs` wraps fragments in that stroke. Small details
|
||||
(emblem glyphs, hair shine, brows) go thinner, 1.5-2.6, never thicker than 3.4.
|
||||
- **Flat fills.** No gradients, no SVG filters, no opacity except blush and a few highlights.
|
||||
Shading is a second flat colour (`shade(hex, f)`), used sparingly: raven bellies, inner ears.
|
||||
- **Limbs are double strokes**, not filled shapes: `limb()` draws the path in ink at width
|
||||
`w + 6`, then in colour at width `w`, both round-capped. That is what keeps arms legible when
|
||||
they cross the body. Arms are 7 wide, legs 8, raven wings 11.
|
||||
- **Eyes**: an iris ellipse (6.3 x 7.6) in the character's eye colour with a 2.5 ink edge, a
|
||||
dark pupil `#2a1a12`, a 3.2 ink upper lid arc, a white shine dot r 2.3 up and right plus a
|
||||
tiny r 1 dot down and left. The shine dot is what makes them read as cute at 128 px; keep it on
|
||||
every open eye.
|
||||
- **Blush**: two `#ff9a8a` ellipses under the eyes. Opacity carries the mood: 0.45 worried,
|
||||
0.7 default, 0.9 happy.
|
||||
- **Proportions**: head about half the figure. The head ellipse is 80 x 72; the whole figure
|
||||
stands about 136 tall.
|
||||
- **Die-cut border** is CSS on the `<img>` (four white offset drop-shadows and a soft shadow),
|
||||
never baked into the SVG, so the files stay usable on any background.
|
||||
|
||||
## Coordinate conventions
|
||||
|
||||
All in `rig.mjs`. The viewBox is `-80 -10 160 172`, so x is centred on 0.
|
||||
|
||||
| Point | Value | Used by |
|
||||
|---|---|---|
|
||||
| Head centre | `(0, 52)`, ellipse rx 40 ry 36 | head base, hair, accessories, face |
|
||||
| Neck (head rotation pivot) | `(0, 88)` | pose head tilt |
|
||||
| Shoulders | `(±15, 95)` | arm start |
|
||||
| Hips | `(±8, 126)` | leg start |
|
||||
| Feet | `y = 149` plus the pose's foot offset | shoes |
|
||||
| Ground | about `y = 152` | |
|
||||
| Chest emblem | disc r 6.8 at `(0, 106)`, ravens `(0, 104)` | `emblem()` |
|
||||
|
||||
Face anchors come from `BASES` so each expression works on every base:
|
||||
|
||||
| Base | eyeX | eyeY | mouthY | browY | cheekX | cheekY |
|
||||
|---|---|---|---|---|---|---|
|
||||
| humanoid | 15 | 61 | 76 | 47 | 26 | 71 |
|
||||
| raven | 15 | 55 | 70 | 41 | 27 | 66 |
|
||||
| horse | 17 | 50 | 82 | 37 | 29 | 62 |
|
||||
|
||||
**Props** are drawn around a grip point at `(0, 0)` and extend upward to about `y = -32`. The
|
||||
pose places that grip at a hand plus an offset and rotation (`POSES.*.prop`); the hand circle is
|
||||
drawn after the prop so it reads as holding it. Presenting puts the grip 17 above the open palm.
|
||||
|
||||
**Poses**, as hand points (L is the viewer's left):
|
||||
|
||||
| Pose | Hands | Other |
|
||||
|---|---|---|
|
||||
| neutral | `(-24,120)`, `(24,120)` | prop in right hand |
|
||||
| cheer | `(-46,36)`, `(46,36)` | hops 6, feet apart, sparkles |
|
||||
| thinking | left across the belly `(4,113)`, right at the chin `(13,92)` drawn over the head | head tilt 7, thought dots, no prop |
|
||||
| surprised | `(-51,88)`, `(51,88)` | hops 7, one foot kicked, shock lines, no prop |
|
||||
| presenting | right palm out at `(45,103)` | prop sits on the palm, sparkles |
|
||||
| waving | right raised to `(50,50)` | prop moves to the left hand, motion arcs |
|
||||
|
||||
**Layer order** in `render()`, back to front: hair back and back accessories (rotating with the
|
||||
head), cape, tails, legs, torso and body accessories, emblem, arms, prop, hands, then the head
|
||||
group (skin, `mid` accessories such as beards, the face, hair front and front accessories, brows
|
||||
and expression extras), then any arm marked `front`, then the pose effect. Accessories return
|
||||
`back`, `mid`, `front` or `body` fragments and are slotted in by that key.
|
||||
|
||||
## Checking renders
|
||||
|
||||
Look at every change at sticker size. Two tools, both from the repo root in Git Bash:
|
||||
|
||||
```sh
|
||||
# Whole gallery, or one character / pose, as Edge renders it (with the die-cut border):
|
||||
cast/rig/shot.sh /tmp/all.png # everything, 1400 x 1800
|
||||
cast/rig/shot.sh /tmp/kiko.png kiko # one character
|
||||
cast/rig/shot.sh /tmp/cheer.png "" cheer 1400 1200 # one pose for everyone
|
||||
|
||||
# A 128 px contact sheet of one sticker per character, on a light ground:
|
||||
cd cast/stickers && mkdir -p /tmp/r && for d in */; do d=${d%/}; \
|
||||
magick -background none -density 72 $d/neutral-smile.svg -resize x128 /tmp/r/$d.png; done && \
|
||||
magick montage /tmp/r/*.png -tile 8x -geometry +8+8 -background '#f2efe8' /tmp/sheet128.png
|
||||
```
|
||||
|
||||
`shot.sh` launches Edge headless with a throwaway profile and waits for the PNG, because Edge can
|
||||
return before the file is written. The gallery accepts `?c=<id>&p=<pose>`, which is how the
|
||||
script filters. ImageMagick's own SVG renderer is close to Edge's but not identical (fonts in the
|
||||
sleepy "z" differ); trust Edge for final checks.
|
||||
|
||||
What to look for: hands that vanish into hair, props that lose their outline against the outfit,
|
||||
dark characters that sink into the `#07111a` gallery ground, and anything that reads as an ear
|
||||
or horn when it is not one.
|
||||
|
||||
## How the cast was designed
|
||||
|
||||
Looks come from the avatars in `F:/Projects/VoidBot/assets/repo-faces/`, each repo's
|
||||
`.voidbot/voice/*.png`, `F:/Projects/Odin/assets/personas/`, `F:/Projects/Heimdall/assets/`,
|
||||
`F:/Projects/Hermodr/assets/`, and Sai's self-description in the persona intake. Each avatar was
|
||||
reduced to three or four traits that survive at 128 px: a hair shape and colour, one headpiece,
|
||||
one prop, a palette. `CAST.md` lists the source and traits per character.
|
||||
|
||||
- **Huginn and Muninn** share the raven base and are told apart by eye colour (cyan vs amber,
|
||||
both from their avatars), Huginn's blue cloak and tablet, and Muninn's brass collar and camera.
|
||||
- **Mimir**'s avatar is a stone face crowned with floating cubes; the chibi keeps the bronze skin,
|
||||
teal eyes and the cube crown, and holds a cube.
|
||||
- **Eve**'s avatar is two glowing eyes in a black visor; the chibi is a white helmet with a dark
|
||||
face window, and her eyes and mouth are drawn in glowing blue (`faceInk`).
|
||||
- **Ymir** keeps the ice-blue skin, crystal crown and white beard of the frost-giant avatar.
|
||||
- **Fensalir**'s avatar is a crystal spire, not a person, so the spire became a hat and the
|
||||
person is invented in its pink and cyan.
|
||||
- **Sleipnir** has eight limbs as in the myth: two hoof-arms, two legs and four extra legs behind.
|
||||
- **Tengu** keeps the red face, long nose, white mane and tokin cap; the feather fan is from the
|
||||
yokai myth.
|
||||
- **Invented**: Eureka (bun with a pencil, a star clip and a star wand, in the brand orange and
|
||||
blue), Brokkr (the Eddic smith: ginger beard, goggles, hammer), Aetheria (a pilot in white with
|
||||
a small starship), Void (a hooded purple figure with a moon shield, since its state was lost),
|
||||
and Metame (blue and orange split hair and a mirror; the design deliberately does not depict the
|
||||
operator, whose public history Metame is projected from).
|
||||
|
||||
## Iterations
|
||||
|
||||
Each pair is before on the left, after on the right, cropped from gallery screenshots.
|
||||
|
||||

|
||||
Raised hands sat inside the head outline and disappeared behind the hair. Cheer hands moved out
|
||||
and up to `(±46, 36)`, clear of the head and side locks; surprised and waving moved the same way.
|
||||
|
||||

|
||||
A raven feather in Eureka's hair (a nod to Huginn) read as a rabbit ear at sticker size. It was
|
||||
removed; the pencil and star clip carry the idea of her.
|
||||
|
||||

|
||||
Void in near-black purple vanished into the dark gallery ground and had black hands. The robe and
|
||||
hood went to mid purple with a lighter hood rim, and the hands to lavender (`hands` trait).
|
||||
|
||||

|
||||
The extra legs were hidden under the tunic hem. They now start wider and splay outward, two
|
||||
pairs behind the main legs, so the eight limbs read.
|
||||
|
||||

|
||||
The first horn was a bent cone that read as a rolled scroll. It gained a dark bell opening.
|
||||
|
||||

|
||||
Huginn's wings, body and cloak were three near-identical dark blues and merged. The wing and belly
|
||||
were lightened and the cloak turned a clearer blue.
|
||||
|
||||
Smaller fixes without images: long hair narrowed so it frames the body instead of swallowing it,
|
||||
Tengu's full beard cut to a goatee and his nose shortened so the mouth shows, Tengu's white fan
|
||||
turned dark so it reads against his white robe.
|
||||
|
||||
## Known weak spots, and what to do next
|
||||
|
||||
- **Small props at the side**: in the neutral pose a thin prop such as Sai's brush or Muninn's
|
||||
camera is only a few pixels wide at 128 px. A per-pose prop scale (1.2 for neutral) would help.
|
||||
- **Raven thinking** puts the wing across the belly rather than at the chin; it reads as a folded
|
||||
wing more than as thinking. A raven-specific variant with a stronger head tilt would sell it.
|
||||
- **Mimir's cube crown** is busy at 128 px; fewer, larger cubes would be cleaner.
|
||||
- **Kiko's fox mask** hides one fox ear. Moving the mask to the back of the head would keep both.
|
||||
- **One body silhouette** for all humanoids: Brokkr is no stouter than Libby. A `build` trait
|
||||
(torso width and leg length) would give the dwarf, the giant and the children different shapes.
|
||||
- **No back or side views**, which visual-novel scenes will want; the rig is front-facing only.
|
||||
- **Expressions are face-only**: there is no sweat-drop-and-slump body language. Pairing an
|
||||
expression with a small pose adjustment (shoulders up for surprised, head down for sleepy)
|
||||
would add a lot of life cheaply.
|
||||
- Rendering more characters in full coverage is one flag (`full: true`) and about 48 files each.
|
||||
76
cast/README.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Swarm stickers
|
||||
|
||||
Chibi stickers of the GameCult swarm cast, built by a small rig instead of drawn one by one.
|
||||
Every sticker is a standalone SVG under `stickers/<character>/<pose>-<expression>.svg`;
|
||||
`gallery.html` shows them all with character and pose filters; `CAST.md` lists who is who.
|
||||
|
||||
Open `gallery.html` in a browser to browse. To use a sticker, copy its SVG. The white die-cut
|
||||
edge is not in the file; it is CSS, so it can be dropped where it does not suit:
|
||||
|
||||
```css
|
||||
img.sticker { filter: drop-shadow(3px 0 0 #fff) drop-shadow(-3px 0 0 #fff)
|
||||
drop-shadow(0 3px 0 #fff) drop-shadow(0 -3px 0 #fff) drop-shadow(0 8px 10px rgba(0,0,0,.45)); }
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `rig/rig.mjs` | The rig: bases, poses, expressions, hair, accessories, props, emblems, `render()` |
|
||||
| `rig/characters.mjs` | One trait sheet per character, and which characters get every pose |
|
||||
| `rig/build.mjs` | Writes the SVGs, `gallery.html` and `CAST.md` |
|
||||
| `rig/shot.sh` | Screenshots the gallery with Edge headless, for checking renders |
|
||||
| `stickers/`, `gallery.html`, `CAST.md` | Generated; never edit by hand |
|
||||
| `PROCESS.md` | How the rig works, its coordinate conventions, how to check renders, known weak spots |
|
||||
| `process/` | Before/after renders referenced from `PROCESS.md` |
|
||||
|
||||
## Regenerate
|
||||
|
||||
Node 18 or newer, no packages:
|
||||
|
||||
```sh
|
||||
node cast/rig/build.mjs # everyone
|
||||
node cast/rig/build.mjs kiko eve # only these characters (the gallery is rebuilt from disk)
|
||||
```
|
||||
|
||||
## Add a character
|
||||
|
||||
1. Append a trait sheet to `CAST` in `rig/characters.mjs`. Copy the nearest existing one.
|
||||
- `id` (folder name, lowercase), `name`, `role` (one line from the repo README), `source`
|
||||
(path of the avatar or self-description the look comes from, or a string starting with
|
||||
`invented`), optional `note` for a design decision worth keeping.
|
||||
- `base`: `humanoid`, `raven` or `horse`. Raven needs `beak`; horse needs `muzzle`.
|
||||
- `skin`, `eyes` (iris colour), `accent` (pose sparkles), optional `hands`, `faceInk`
|
||||
(eye and mouth lines on a dark face), `noBlush`.
|
||||
- `hair: { style, color, accent? }` — a key of `HAIR` in `rig.mjs`.
|
||||
- `outfit: { shape, main, trim, collar, belt? }` — shapes `tunic`, `dress`, `robe`, `coat`
|
||||
(ravens always use `egg`, with `belly` and `wing`); collars `v`, `round`, `high`, `none`.
|
||||
- `legs`, `shoes`, optional `cape`, `sleeve`, `extraLegs` (0-4).
|
||||
- `acc`: list of `[name, ...colours]`, keys of `ACC`. `prop`: `[name, ...colours]`, a key of
|
||||
`PROPS`, or `null`. `emblem`: `{ shape, color, disc? }`, a key of `EMBLEMS`, or `null`.
|
||||
- `full: true` renders every pose and expression; otherwise neutral pose with the three
|
||||
expressions in `COMMON_EXPRESSIONS`.
|
||||
2. Run the build, then check the render at sticker size (see "Checking renders" in `PROCESS.md`).
|
||||
3. Commit the trait sheet together with the generated files.
|
||||
|
||||
## Add a pose
|
||||
|
||||
Add an entry to `POSES` in `rig/rig.mjs`: hop height, head offset and tilt, elbow and hand points
|
||||
for each arm (`front: true` draws that arm over the head), feet offsets, where the prop sits
|
||||
(or `null`), and an optional effect from `fx()`. Coordinates are in `PROCESS.md`. Every `full`
|
||||
character picks it up on the next build. Check hands against long hair and twintails: a hand
|
||||
inside the head outline disappears behind the hair.
|
||||
|
||||
## Add an expression
|
||||
|
||||
Add a `case` to `face()` in `rig/rig.mjs` and the name to `EXPRESSIONS`. Draw relative to the
|
||||
base's anchors (`BASES[...].eyeX/eyeY/mouthY/browY/cheekX/cheekY`), never fixed numbers, so the
|
||||
raven and horse faces follow. If ravens should open their beak for it, add it to `beak()`.
|
||||
|
||||
## Licence
|
||||
|
||||
All art here is the project's own: drawn as hand-written SVG shapes by this rig, not traced or
|
||||
derived from any third-party asset. The avatars named in `CAST.md` are GameCult's own images and
|
||||
served only as reference for colours and traits. The repository has no licence file at the time of
|
||||
writing, so the stickers carry no licence grant beyond GameCult's own; add one here if they are
|
||||
meant to be reused outside GameCult.
|
||||
340
cast/gallery.html
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Swarm Stickers</title>
|
||||
<style>
|
||||
:root { --ground: #07111a; --orange: #ff8a2a; --blue: #59b7ff; --ink: #e8eef5; --dim: #8aa0b4; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--ground); color: var(--ink); font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
header { padding: 28px 16px 8px; max-width: 1360px; margin: 0 auto; }
|
||||
h1 { margin: 0 0 4px; font-size: 26px; } h1 span { color: var(--orange); }
|
||||
p.lede { margin: 0 0 14px; color: var(--dim); }
|
||||
nav { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; }
|
||||
nav button { font: inherit; font-size: 13px; color: var(--ink); background: #10202e; border: 1px solid #1e3446; border-radius: 999px; padding: 4px 11px; cursor: pointer; }
|
||||
nav button[aria-pressed="true"] { background: var(--orange); border-color: var(--orange); color: #1a0d02; font-weight: 600; }
|
||||
nav.poses button[aria-pressed="true"] { background: var(--blue); border-color: var(--blue); color: #021220; }
|
||||
main { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 22px 14px; padding: 18px 16px 48px; max-width: 1360px; margin: 0 auto; }
|
||||
figure { margin: 0; text-align: center; }
|
||||
figure img { width: 100%; max-width: 150px; height: auto; display: block; margin: 0 auto;
|
||||
filter: drop-shadow(3px 0 0 #fff) drop-shadow(-3px 0 0 #fff) drop-shadow(0 3px 0 #fff) drop-shadow(0 -3px 0 #fff) drop-shadow(0 8px 10px rgba(0,0,0,.45));
|
||||
transform: rotate(var(--tilt, -3deg)); transition: transform .2s ease; }
|
||||
figure:nth-child(3n) img { --tilt: 3deg; } figure:nth-child(4n+1) img { --tilt: -1deg; }
|
||||
figure:hover img { transform: rotate(0deg) scale(1.06); }
|
||||
figcaption { font-size: 12px; color: var(--dim); margin-top: 6px; } figcaption b { color: var(--ink); font-weight: 600; }
|
||||
[hidden] { display: none !important; }
|
||||
@media (prefers-reduced-motion: reduce) { figure img { transition: none; } }
|
||||
</style></head><body>
|
||||
<header><h1>Swarm <span>stickers</span></h1>
|
||||
<p class="lede">297 stickers, 24 characters. Generated by <code>cast/rig/build.mjs</code>; the white die-cut edge is CSS on the image.</p>
|
||||
<nav class="chars" aria-label="Character"><button type="button" data-f="" aria-pressed="true">All</button><button type="button" data-f="eureka">Eureka</button><button type="button" data-f="huginn">Huginn</button><button type="button" data-f="libby">Libby</button><button type="button" data-f="epiphany">Epiphany</button><button type="button" data-f="weksa">Weksa</button><button type="button" data-f="nibu">Nibu</button><button type="button" data-f="mimir">Mimir</button><button type="button" data-f="eve">Eve</button><button type="button" data-f="kiko">Kiko</button><button type="button" data-f="heimdall">Heimdall</button><button type="button" data-f="fensalir">Fensalir</button><button type="button" data-f="druzkai">Druzkai</button><button type="button" data-f="norn">Norn</button><button type="button" data-f="sai">Sai</button><button type="button" data-f="brokkr">Brokkr</button><button type="button" data-f="aetheria">Aetheria</button><button type="button" data-f="ymir">Ymir</button><button type="button" data-f="tengu">Tengu</button><button type="button" data-f="muninn">Muninn</button><button type="button" data-f="sleipnir">Sleipnir</button><button type="button" data-f="hermodr">Hermodr</button><button type="button" data-f="metame">Metame</button><button type="button" data-f="void">Void</button><button type="button" data-f="gjallar">Gjallar</button></nav>
|
||||
<nav class="poses" aria-label="Pose"><button type="button" data-p="" aria-pressed="true">All poses</button><button type="button" data-p="neutral">neutral</button><button type="button" data-p="cheer">cheer</button><button type="button" data-p="thinking">thinking</button><button type="button" data-p="surprised">surprised</button><button type="button" data-p="presenting">presenting</button><button type="button" data-p="waving">waving</button></nav></header>
|
||||
<main>
|
||||
<figure data-ch="eureka" data-pose="neutral"><img src="stickers/eureka/neutral-neutral.svg" alt="Eureka, neutral, neutral" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> neutral · neutral</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="neutral"><img src="stickers/eureka/neutral-smile.svg" alt="Eureka, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="neutral"><img src="stickers/eureka/neutral-grin.svg" alt="Eureka, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="neutral"><img src="stickers/eureka/neutral-happy.svg" alt="Eureka, neutral, happy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> neutral · happy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="neutral"><img src="stickers/eureka/neutral-worried.svg" alt="Eureka, neutral, worried" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> neutral · worried</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="neutral"><img src="stickers/eureka/neutral-surprised.svg" alt="Eureka, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="neutral"><img src="stickers/eureka/neutral-determined.svg" alt="Eureka, neutral, determined" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> neutral · determined</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="neutral"><img src="stickers/eureka/neutral-sleepy.svg" alt="Eureka, neutral, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> neutral · sleepy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="cheer"><img src="stickers/eureka/cheer-neutral.svg" alt="Eureka, cheer, neutral" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> cheer · neutral</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="cheer"><img src="stickers/eureka/cheer-smile.svg" alt="Eureka, cheer, smile" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> cheer · smile</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="cheer"><img src="stickers/eureka/cheer-grin.svg" alt="Eureka, cheer, grin" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> cheer · grin</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="cheer"><img src="stickers/eureka/cheer-happy.svg" alt="Eureka, cheer, happy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> cheer · happy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="cheer"><img src="stickers/eureka/cheer-worried.svg" alt="Eureka, cheer, worried" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> cheer · worried</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="cheer"><img src="stickers/eureka/cheer-surprised.svg" alt="Eureka, cheer, surprised" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> cheer · surprised</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="cheer"><img src="stickers/eureka/cheer-determined.svg" alt="Eureka, cheer, determined" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> cheer · determined</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="cheer"><img src="stickers/eureka/cheer-sleepy.svg" alt="Eureka, cheer, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> cheer · sleepy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="thinking"><img src="stickers/eureka/thinking-neutral.svg" alt="Eureka, thinking, neutral" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> thinking · neutral</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="thinking"><img src="stickers/eureka/thinking-smile.svg" alt="Eureka, thinking, smile" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> thinking · smile</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="thinking"><img src="stickers/eureka/thinking-grin.svg" alt="Eureka, thinking, grin" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> thinking · grin</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="thinking"><img src="stickers/eureka/thinking-happy.svg" alt="Eureka, thinking, happy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> thinking · happy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="thinking"><img src="stickers/eureka/thinking-worried.svg" alt="Eureka, thinking, worried" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> thinking · worried</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="thinking"><img src="stickers/eureka/thinking-surprised.svg" alt="Eureka, thinking, surprised" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> thinking · surprised</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="thinking"><img src="stickers/eureka/thinking-determined.svg" alt="Eureka, thinking, determined" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> thinking · determined</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="thinking"><img src="stickers/eureka/thinking-sleepy.svg" alt="Eureka, thinking, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> thinking · sleepy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="surprised"><img src="stickers/eureka/surprised-neutral.svg" alt="Eureka, surprised, neutral" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> surprised · neutral</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="surprised"><img src="stickers/eureka/surprised-smile.svg" alt="Eureka, surprised, smile" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> surprised · smile</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="surprised"><img src="stickers/eureka/surprised-grin.svg" alt="Eureka, surprised, grin" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> surprised · grin</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="surprised"><img src="stickers/eureka/surprised-happy.svg" alt="Eureka, surprised, happy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> surprised · happy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="surprised"><img src="stickers/eureka/surprised-worried.svg" alt="Eureka, surprised, worried" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> surprised · worried</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="surprised"><img src="stickers/eureka/surprised-surprised.svg" alt="Eureka, surprised, surprised" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> surprised · surprised</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="surprised"><img src="stickers/eureka/surprised-determined.svg" alt="Eureka, surprised, determined" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> surprised · determined</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="surprised"><img src="stickers/eureka/surprised-sleepy.svg" alt="Eureka, surprised, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> surprised · sleepy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="presenting"><img src="stickers/eureka/presenting-neutral.svg" alt="Eureka, presenting, neutral" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> presenting · neutral</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="presenting"><img src="stickers/eureka/presenting-smile.svg" alt="Eureka, presenting, smile" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> presenting · smile</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="presenting"><img src="stickers/eureka/presenting-grin.svg" alt="Eureka, presenting, grin" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> presenting · grin</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="presenting"><img src="stickers/eureka/presenting-happy.svg" alt="Eureka, presenting, happy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> presenting · happy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="presenting"><img src="stickers/eureka/presenting-worried.svg" alt="Eureka, presenting, worried" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> presenting · worried</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="presenting"><img src="stickers/eureka/presenting-surprised.svg" alt="Eureka, presenting, surprised" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> presenting · surprised</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="presenting"><img src="stickers/eureka/presenting-determined.svg" alt="Eureka, presenting, determined" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> presenting · determined</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="presenting"><img src="stickers/eureka/presenting-sleepy.svg" alt="Eureka, presenting, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> presenting · sleepy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="waving"><img src="stickers/eureka/waving-neutral.svg" alt="Eureka, waving, neutral" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> waving · neutral</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="waving"><img src="stickers/eureka/waving-smile.svg" alt="Eureka, waving, smile" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> waving · smile</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="waving"><img src="stickers/eureka/waving-grin.svg" alt="Eureka, waving, grin" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> waving · grin</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="waving"><img src="stickers/eureka/waving-happy.svg" alt="Eureka, waving, happy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> waving · happy</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="waving"><img src="stickers/eureka/waving-worried.svg" alt="Eureka, waving, worried" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> waving · worried</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="waving"><img src="stickers/eureka/waving-surprised.svg" alt="Eureka, waving, surprised" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> waving · surprised</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="waving"><img src="stickers/eureka/waving-determined.svg" alt="Eureka, waving, determined" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> waving · determined</figcaption></figure>
|
||||
<figure data-ch="eureka" data-pose="waving"><img src="stickers/eureka/waving-sleepy.svg" alt="Eureka, waving, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Eureka</b> waving · sleepy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="neutral"><img src="stickers/huginn/neutral-neutral.svg" alt="Huginn, neutral, neutral" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> neutral · neutral</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="neutral"><img src="stickers/huginn/neutral-smile.svg" alt="Huginn, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="neutral"><img src="stickers/huginn/neutral-grin.svg" alt="Huginn, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="neutral"><img src="stickers/huginn/neutral-happy.svg" alt="Huginn, neutral, happy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> neutral · happy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="neutral"><img src="stickers/huginn/neutral-worried.svg" alt="Huginn, neutral, worried" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> neutral · worried</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="neutral"><img src="stickers/huginn/neutral-surprised.svg" alt="Huginn, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="neutral"><img src="stickers/huginn/neutral-determined.svg" alt="Huginn, neutral, determined" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> neutral · determined</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="neutral"><img src="stickers/huginn/neutral-sleepy.svg" alt="Huginn, neutral, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> neutral · sleepy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="cheer"><img src="stickers/huginn/cheer-neutral.svg" alt="Huginn, cheer, neutral" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> cheer · neutral</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="cheer"><img src="stickers/huginn/cheer-smile.svg" alt="Huginn, cheer, smile" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> cheer · smile</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="cheer"><img src="stickers/huginn/cheer-grin.svg" alt="Huginn, cheer, grin" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> cheer · grin</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="cheer"><img src="stickers/huginn/cheer-happy.svg" alt="Huginn, cheer, happy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> cheer · happy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="cheer"><img src="stickers/huginn/cheer-worried.svg" alt="Huginn, cheer, worried" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> cheer · worried</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="cheer"><img src="stickers/huginn/cheer-surprised.svg" alt="Huginn, cheer, surprised" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> cheer · surprised</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="cheer"><img src="stickers/huginn/cheer-determined.svg" alt="Huginn, cheer, determined" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> cheer · determined</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="cheer"><img src="stickers/huginn/cheer-sleepy.svg" alt="Huginn, cheer, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> cheer · sleepy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="thinking"><img src="stickers/huginn/thinking-neutral.svg" alt="Huginn, thinking, neutral" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> thinking · neutral</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="thinking"><img src="stickers/huginn/thinking-smile.svg" alt="Huginn, thinking, smile" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> thinking · smile</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="thinking"><img src="stickers/huginn/thinking-grin.svg" alt="Huginn, thinking, grin" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> thinking · grin</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="thinking"><img src="stickers/huginn/thinking-happy.svg" alt="Huginn, thinking, happy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> thinking · happy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="thinking"><img src="stickers/huginn/thinking-worried.svg" alt="Huginn, thinking, worried" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> thinking · worried</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="thinking"><img src="stickers/huginn/thinking-surprised.svg" alt="Huginn, thinking, surprised" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> thinking · surprised</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="thinking"><img src="stickers/huginn/thinking-determined.svg" alt="Huginn, thinking, determined" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> thinking · determined</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="thinking"><img src="stickers/huginn/thinking-sleepy.svg" alt="Huginn, thinking, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> thinking · sleepy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="surprised"><img src="stickers/huginn/surprised-neutral.svg" alt="Huginn, surprised, neutral" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> surprised · neutral</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="surprised"><img src="stickers/huginn/surprised-smile.svg" alt="Huginn, surprised, smile" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> surprised · smile</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="surprised"><img src="stickers/huginn/surprised-grin.svg" alt="Huginn, surprised, grin" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> surprised · grin</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="surprised"><img src="stickers/huginn/surprised-happy.svg" alt="Huginn, surprised, happy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> surprised · happy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="surprised"><img src="stickers/huginn/surprised-worried.svg" alt="Huginn, surprised, worried" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> surprised · worried</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="surprised"><img src="stickers/huginn/surprised-surprised.svg" alt="Huginn, surprised, surprised" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> surprised · surprised</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="surprised"><img src="stickers/huginn/surprised-determined.svg" alt="Huginn, surprised, determined" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> surprised · determined</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="surprised"><img src="stickers/huginn/surprised-sleepy.svg" alt="Huginn, surprised, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> surprised · sleepy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="presenting"><img src="stickers/huginn/presenting-neutral.svg" alt="Huginn, presenting, neutral" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> presenting · neutral</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="presenting"><img src="stickers/huginn/presenting-smile.svg" alt="Huginn, presenting, smile" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> presenting · smile</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="presenting"><img src="stickers/huginn/presenting-grin.svg" alt="Huginn, presenting, grin" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> presenting · grin</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="presenting"><img src="stickers/huginn/presenting-happy.svg" alt="Huginn, presenting, happy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> presenting · happy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="presenting"><img src="stickers/huginn/presenting-worried.svg" alt="Huginn, presenting, worried" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> presenting · worried</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="presenting"><img src="stickers/huginn/presenting-surprised.svg" alt="Huginn, presenting, surprised" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> presenting · surprised</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="presenting"><img src="stickers/huginn/presenting-determined.svg" alt="Huginn, presenting, determined" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> presenting · determined</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="presenting"><img src="stickers/huginn/presenting-sleepy.svg" alt="Huginn, presenting, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> presenting · sleepy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="waving"><img src="stickers/huginn/waving-neutral.svg" alt="Huginn, waving, neutral" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> waving · neutral</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="waving"><img src="stickers/huginn/waving-smile.svg" alt="Huginn, waving, smile" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> waving · smile</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="waving"><img src="stickers/huginn/waving-grin.svg" alt="Huginn, waving, grin" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> waving · grin</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="waving"><img src="stickers/huginn/waving-happy.svg" alt="Huginn, waving, happy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> waving · happy</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="waving"><img src="stickers/huginn/waving-worried.svg" alt="Huginn, waving, worried" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> waving · worried</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="waving"><img src="stickers/huginn/waving-surprised.svg" alt="Huginn, waving, surprised" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> waving · surprised</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="waving"><img src="stickers/huginn/waving-determined.svg" alt="Huginn, waving, determined" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> waving · determined</figcaption></figure>
|
||||
<figure data-ch="huginn" data-pose="waving"><img src="stickers/huginn/waving-sleepy.svg" alt="Huginn, waving, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Huginn</b> waving · sleepy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="neutral"><img src="stickers/libby/neutral-neutral.svg" alt="Libby, neutral, neutral" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> neutral · neutral</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="neutral"><img src="stickers/libby/neutral-smile.svg" alt="Libby, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="neutral"><img src="stickers/libby/neutral-grin.svg" alt="Libby, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="neutral"><img src="stickers/libby/neutral-happy.svg" alt="Libby, neutral, happy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> neutral · happy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="neutral"><img src="stickers/libby/neutral-worried.svg" alt="Libby, neutral, worried" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> neutral · worried</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="neutral"><img src="stickers/libby/neutral-surprised.svg" alt="Libby, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="neutral"><img src="stickers/libby/neutral-determined.svg" alt="Libby, neutral, determined" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> neutral · determined</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="neutral"><img src="stickers/libby/neutral-sleepy.svg" alt="Libby, neutral, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> neutral · sleepy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="cheer"><img src="stickers/libby/cheer-neutral.svg" alt="Libby, cheer, neutral" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> cheer · neutral</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="cheer"><img src="stickers/libby/cheer-smile.svg" alt="Libby, cheer, smile" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> cheer · smile</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="cheer"><img src="stickers/libby/cheer-grin.svg" alt="Libby, cheer, grin" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> cheer · grin</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="cheer"><img src="stickers/libby/cheer-happy.svg" alt="Libby, cheer, happy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> cheer · happy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="cheer"><img src="stickers/libby/cheer-worried.svg" alt="Libby, cheer, worried" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> cheer · worried</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="cheer"><img src="stickers/libby/cheer-surprised.svg" alt="Libby, cheer, surprised" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> cheer · surprised</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="cheer"><img src="stickers/libby/cheer-determined.svg" alt="Libby, cheer, determined" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> cheer · determined</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="cheer"><img src="stickers/libby/cheer-sleepy.svg" alt="Libby, cheer, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> cheer · sleepy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="thinking"><img src="stickers/libby/thinking-neutral.svg" alt="Libby, thinking, neutral" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> thinking · neutral</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="thinking"><img src="stickers/libby/thinking-smile.svg" alt="Libby, thinking, smile" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> thinking · smile</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="thinking"><img src="stickers/libby/thinking-grin.svg" alt="Libby, thinking, grin" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> thinking · grin</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="thinking"><img src="stickers/libby/thinking-happy.svg" alt="Libby, thinking, happy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> thinking · happy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="thinking"><img src="stickers/libby/thinking-worried.svg" alt="Libby, thinking, worried" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> thinking · worried</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="thinking"><img src="stickers/libby/thinking-surprised.svg" alt="Libby, thinking, surprised" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> thinking · surprised</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="thinking"><img src="stickers/libby/thinking-determined.svg" alt="Libby, thinking, determined" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> thinking · determined</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="thinking"><img src="stickers/libby/thinking-sleepy.svg" alt="Libby, thinking, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> thinking · sleepy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="surprised"><img src="stickers/libby/surprised-neutral.svg" alt="Libby, surprised, neutral" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> surprised · neutral</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="surprised"><img src="stickers/libby/surprised-smile.svg" alt="Libby, surprised, smile" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> surprised · smile</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="surprised"><img src="stickers/libby/surprised-grin.svg" alt="Libby, surprised, grin" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> surprised · grin</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="surprised"><img src="stickers/libby/surprised-happy.svg" alt="Libby, surprised, happy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> surprised · happy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="surprised"><img src="stickers/libby/surprised-worried.svg" alt="Libby, surprised, worried" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> surprised · worried</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="surprised"><img src="stickers/libby/surprised-surprised.svg" alt="Libby, surprised, surprised" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> surprised · surprised</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="surprised"><img src="stickers/libby/surprised-determined.svg" alt="Libby, surprised, determined" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> surprised · determined</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="surprised"><img src="stickers/libby/surprised-sleepy.svg" alt="Libby, surprised, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> surprised · sleepy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="presenting"><img src="stickers/libby/presenting-neutral.svg" alt="Libby, presenting, neutral" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> presenting · neutral</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="presenting"><img src="stickers/libby/presenting-smile.svg" alt="Libby, presenting, smile" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> presenting · smile</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="presenting"><img src="stickers/libby/presenting-grin.svg" alt="Libby, presenting, grin" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> presenting · grin</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="presenting"><img src="stickers/libby/presenting-happy.svg" alt="Libby, presenting, happy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> presenting · happy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="presenting"><img src="stickers/libby/presenting-worried.svg" alt="Libby, presenting, worried" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> presenting · worried</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="presenting"><img src="stickers/libby/presenting-surprised.svg" alt="Libby, presenting, surprised" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> presenting · surprised</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="presenting"><img src="stickers/libby/presenting-determined.svg" alt="Libby, presenting, determined" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> presenting · determined</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="presenting"><img src="stickers/libby/presenting-sleepy.svg" alt="Libby, presenting, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> presenting · sleepy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="waving"><img src="stickers/libby/waving-neutral.svg" alt="Libby, waving, neutral" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> waving · neutral</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="waving"><img src="stickers/libby/waving-smile.svg" alt="Libby, waving, smile" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> waving · smile</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="waving"><img src="stickers/libby/waving-grin.svg" alt="Libby, waving, grin" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> waving · grin</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="waving"><img src="stickers/libby/waving-happy.svg" alt="Libby, waving, happy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> waving · happy</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="waving"><img src="stickers/libby/waving-worried.svg" alt="Libby, waving, worried" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> waving · worried</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="waving"><img src="stickers/libby/waving-surprised.svg" alt="Libby, waving, surprised" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> waving · surprised</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="waving"><img src="stickers/libby/waving-determined.svg" alt="Libby, waving, determined" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> waving · determined</figcaption></figure>
|
||||
<figure data-ch="libby" data-pose="waving"><img src="stickers/libby/waving-sleepy.svg" alt="Libby, waving, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Libby</b> waving · sleepy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="neutral"><img src="stickers/epiphany/neutral-neutral.svg" alt="Epiphany, neutral, neutral" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> neutral · neutral</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="neutral"><img src="stickers/epiphany/neutral-smile.svg" alt="Epiphany, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="neutral"><img src="stickers/epiphany/neutral-grin.svg" alt="Epiphany, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="neutral"><img src="stickers/epiphany/neutral-happy.svg" alt="Epiphany, neutral, happy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> neutral · happy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="neutral"><img src="stickers/epiphany/neutral-worried.svg" alt="Epiphany, neutral, worried" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> neutral · worried</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="neutral"><img src="stickers/epiphany/neutral-surprised.svg" alt="Epiphany, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="neutral"><img src="stickers/epiphany/neutral-determined.svg" alt="Epiphany, neutral, determined" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> neutral · determined</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="neutral"><img src="stickers/epiphany/neutral-sleepy.svg" alt="Epiphany, neutral, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> neutral · sleepy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="cheer"><img src="stickers/epiphany/cheer-neutral.svg" alt="Epiphany, cheer, neutral" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> cheer · neutral</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="cheer"><img src="stickers/epiphany/cheer-smile.svg" alt="Epiphany, cheer, smile" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> cheer · smile</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="cheer"><img src="stickers/epiphany/cheer-grin.svg" alt="Epiphany, cheer, grin" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> cheer · grin</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="cheer"><img src="stickers/epiphany/cheer-happy.svg" alt="Epiphany, cheer, happy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> cheer · happy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="cheer"><img src="stickers/epiphany/cheer-worried.svg" alt="Epiphany, cheer, worried" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> cheer · worried</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="cheer"><img src="stickers/epiphany/cheer-surprised.svg" alt="Epiphany, cheer, surprised" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> cheer · surprised</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="cheer"><img src="stickers/epiphany/cheer-determined.svg" alt="Epiphany, cheer, determined" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> cheer · determined</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="cheer"><img src="stickers/epiphany/cheer-sleepy.svg" alt="Epiphany, cheer, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> cheer · sleepy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="thinking"><img src="stickers/epiphany/thinking-neutral.svg" alt="Epiphany, thinking, neutral" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> thinking · neutral</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="thinking"><img src="stickers/epiphany/thinking-smile.svg" alt="Epiphany, thinking, smile" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> thinking · smile</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="thinking"><img src="stickers/epiphany/thinking-grin.svg" alt="Epiphany, thinking, grin" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> thinking · grin</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="thinking"><img src="stickers/epiphany/thinking-happy.svg" alt="Epiphany, thinking, happy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> thinking · happy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="thinking"><img src="stickers/epiphany/thinking-worried.svg" alt="Epiphany, thinking, worried" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> thinking · worried</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="thinking"><img src="stickers/epiphany/thinking-surprised.svg" alt="Epiphany, thinking, surprised" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> thinking · surprised</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="thinking"><img src="stickers/epiphany/thinking-determined.svg" alt="Epiphany, thinking, determined" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> thinking · determined</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="thinking"><img src="stickers/epiphany/thinking-sleepy.svg" alt="Epiphany, thinking, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> thinking · sleepy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="surprised"><img src="stickers/epiphany/surprised-neutral.svg" alt="Epiphany, surprised, neutral" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> surprised · neutral</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="surprised"><img src="stickers/epiphany/surprised-smile.svg" alt="Epiphany, surprised, smile" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> surprised · smile</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="surprised"><img src="stickers/epiphany/surprised-grin.svg" alt="Epiphany, surprised, grin" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> surprised · grin</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="surprised"><img src="stickers/epiphany/surprised-happy.svg" alt="Epiphany, surprised, happy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> surprised · happy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="surprised"><img src="stickers/epiphany/surprised-worried.svg" alt="Epiphany, surprised, worried" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> surprised · worried</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="surprised"><img src="stickers/epiphany/surprised-surprised.svg" alt="Epiphany, surprised, surprised" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> surprised · surprised</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="surprised"><img src="stickers/epiphany/surprised-determined.svg" alt="Epiphany, surprised, determined" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> surprised · determined</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="surprised"><img src="stickers/epiphany/surprised-sleepy.svg" alt="Epiphany, surprised, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> surprised · sleepy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="presenting"><img src="stickers/epiphany/presenting-neutral.svg" alt="Epiphany, presenting, neutral" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> presenting · neutral</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="presenting"><img src="stickers/epiphany/presenting-smile.svg" alt="Epiphany, presenting, smile" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> presenting · smile</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="presenting"><img src="stickers/epiphany/presenting-grin.svg" alt="Epiphany, presenting, grin" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> presenting · grin</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="presenting"><img src="stickers/epiphany/presenting-happy.svg" alt="Epiphany, presenting, happy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> presenting · happy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="presenting"><img src="stickers/epiphany/presenting-worried.svg" alt="Epiphany, presenting, worried" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> presenting · worried</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="presenting"><img src="stickers/epiphany/presenting-surprised.svg" alt="Epiphany, presenting, surprised" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> presenting · surprised</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="presenting"><img src="stickers/epiphany/presenting-determined.svg" alt="Epiphany, presenting, determined" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> presenting · determined</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="presenting"><img src="stickers/epiphany/presenting-sleepy.svg" alt="Epiphany, presenting, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> presenting · sleepy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="waving"><img src="stickers/epiphany/waving-neutral.svg" alt="Epiphany, waving, neutral" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> waving · neutral</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="waving"><img src="stickers/epiphany/waving-smile.svg" alt="Epiphany, waving, smile" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> waving · smile</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="waving"><img src="stickers/epiphany/waving-grin.svg" alt="Epiphany, waving, grin" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> waving · grin</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="waving"><img src="stickers/epiphany/waving-happy.svg" alt="Epiphany, waving, happy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> waving · happy</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="waving"><img src="stickers/epiphany/waving-worried.svg" alt="Epiphany, waving, worried" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> waving · worried</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="waving"><img src="stickers/epiphany/waving-surprised.svg" alt="Epiphany, waving, surprised" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> waving · surprised</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="waving"><img src="stickers/epiphany/waving-determined.svg" alt="Epiphany, waving, determined" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> waving · determined</figcaption></figure>
|
||||
<figure data-ch="epiphany" data-pose="waving"><img src="stickers/epiphany/waving-sleepy.svg" alt="Epiphany, waving, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Epiphany</b> waving · sleepy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="neutral"><img src="stickers/weksa/neutral-neutral.svg" alt="Weksa, neutral, neutral" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> neutral · neutral</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="neutral"><img src="stickers/weksa/neutral-smile.svg" alt="Weksa, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="neutral"><img src="stickers/weksa/neutral-grin.svg" alt="Weksa, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="neutral"><img src="stickers/weksa/neutral-happy.svg" alt="Weksa, neutral, happy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> neutral · happy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="neutral"><img src="stickers/weksa/neutral-worried.svg" alt="Weksa, neutral, worried" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> neutral · worried</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="neutral"><img src="stickers/weksa/neutral-surprised.svg" alt="Weksa, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="neutral"><img src="stickers/weksa/neutral-determined.svg" alt="Weksa, neutral, determined" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> neutral · determined</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="neutral"><img src="stickers/weksa/neutral-sleepy.svg" alt="Weksa, neutral, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> neutral · sleepy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="cheer"><img src="stickers/weksa/cheer-neutral.svg" alt="Weksa, cheer, neutral" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> cheer · neutral</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="cheer"><img src="stickers/weksa/cheer-smile.svg" alt="Weksa, cheer, smile" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> cheer · smile</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="cheer"><img src="stickers/weksa/cheer-grin.svg" alt="Weksa, cheer, grin" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> cheer · grin</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="cheer"><img src="stickers/weksa/cheer-happy.svg" alt="Weksa, cheer, happy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> cheer · happy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="cheer"><img src="stickers/weksa/cheer-worried.svg" alt="Weksa, cheer, worried" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> cheer · worried</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="cheer"><img src="stickers/weksa/cheer-surprised.svg" alt="Weksa, cheer, surprised" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> cheer · surprised</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="cheer"><img src="stickers/weksa/cheer-determined.svg" alt="Weksa, cheer, determined" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> cheer · determined</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="cheer"><img src="stickers/weksa/cheer-sleepy.svg" alt="Weksa, cheer, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> cheer · sleepy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="thinking"><img src="stickers/weksa/thinking-neutral.svg" alt="Weksa, thinking, neutral" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> thinking · neutral</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="thinking"><img src="stickers/weksa/thinking-smile.svg" alt="Weksa, thinking, smile" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> thinking · smile</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="thinking"><img src="stickers/weksa/thinking-grin.svg" alt="Weksa, thinking, grin" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> thinking · grin</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="thinking"><img src="stickers/weksa/thinking-happy.svg" alt="Weksa, thinking, happy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> thinking · happy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="thinking"><img src="stickers/weksa/thinking-worried.svg" alt="Weksa, thinking, worried" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> thinking · worried</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="thinking"><img src="stickers/weksa/thinking-surprised.svg" alt="Weksa, thinking, surprised" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> thinking · surprised</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="thinking"><img src="stickers/weksa/thinking-determined.svg" alt="Weksa, thinking, determined" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> thinking · determined</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="thinking"><img src="stickers/weksa/thinking-sleepy.svg" alt="Weksa, thinking, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> thinking · sleepy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="surprised"><img src="stickers/weksa/surprised-neutral.svg" alt="Weksa, surprised, neutral" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> surprised · neutral</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="surprised"><img src="stickers/weksa/surprised-smile.svg" alt="Weksa, surprised, smile" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> surprised · smile</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="surprised"><img src="stickers/weksa/surprised-grin.svg" alt="Weksa, surprised, grin" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> surprised · grin</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="surprised"><img src="stickers/weksa/surprised-happy.svg" alt="Weksa, surprised, happy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> surprised · happy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="surprised"><img src="stickers/weksa/surprised-worried.svg" alt="Weksa, surprised, worried" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> surprised · worried</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="surprised"><img src="stickers/weksa/surprised-surprised.svg" alt="Weksa, surprised, surprised" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> surprised · surprised</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="surprised"><img src="stickers/weksa/surprised-determined.svg" alt="Weksa, surprised, determined" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> surprised · determined</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="surprised"><img src="stickers/weksa/surprised-sleepy.svg" alt="Weksa, surprised, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> surprised · sleepy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="presenting"><img src="stickers/weksa/presenting-neutral.svg" alt="Weksa, presenting, neutral" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> presenting · neutral</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="presenting"><img src="stickers/weksa/presenting-smile.svg" alt="Weksa, presenting, smile" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> presenting · smile</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="presenting"><img src="stickers/weksa/presenting-grin.svg" alt="Weksa, presenting, grin" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> presenting · grin</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="presenting"><img src="stickers/weksa/presenting-happy.svg" alt="Weksa, presenting, happy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> presenting · happy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="presenting"><img src="stickers/weksa/presenting-worried.svg" alt="Weksa, presenting, worried" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> presenting · worried</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="presenting"><img src="stickers/weksa/presenting-surprised.svg" alt="Weksa, presenting, surprised" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> presenting · surprised</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="presenting"><img src="stickers/weksa/presenting-determined.svg" alt="Weksa, presenting, determined" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> presenting · determined</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="presenting"><img src="stickers/weksa/presenting-sleepy.svg" alt="Weksa, presenting, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> presenting · sleepy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="waving"><img src="stickers/weksa/waving-neutral.svg" alt="Weksa, waving, neutral" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> waving · neutral</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="waving"><img src="stickers/weksa/waving-smile.svg" alt="Weksa, waving, smile" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> waving · smile</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="waving"><img src="stickers/weksa/waving-grin.svg" alt="Weksa, waving, grin" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> waving · grin</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="waving"><img src="stickers/weksa/waving-happy.svg" alt="Weksa, waving, happy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> waving · happy</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="waving"><img src="stickers/weksa/waving-worried.svg" alt="Weksa, waving, worried" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> waving · worried</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="waving"><img src="stickers/weksa/waving-surprised.svg" alt="Weksa, waving, surprised" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> waving · surprised</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="waving"><img src="stickers/weksa/waving-determined.svg" alt="Weksa, waving, determined" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> waving · determined</figcaption></figure>
|
||||
<figure data-ch="weksa" data-pose="waving"><img src="stickers/weksa/waving-sleepy.svg" alt="Weksa, waving, sleepy" loading="lazy" width="150" height="161"><figcaption><b>Weksa</b> waving · sleepy</figcaption></figure>
|
||||
<figure data-ch="nibu" data-pose="neutral"><img src="stickers/nibu/neutral-smile.svg" alt="Nibu, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Nibu</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="nibu" data-pose="neutral"><img src="stickers/nibu/neutral-grin.svg" alt="Nibu, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Nibu</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="nibu" data-pose="neutral"><img src="stickers/nibu/neutral-surprised.svg" alt="Nibu, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Nibu</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="mimir" data-pose="neutral"><img src="stickers/mimir/neutral-smile.svg" alt="Mimir, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Mimir</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="mimir" data-pose="neutral"><img src="stickers/mimir/neutral-grin.svg" alt="Mimir, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Mimir</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="mimir" data-pose="neutral"><img src="stickers/mimir/neutral-surprised.svg" alt="Mimir, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Mimir</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="eve" data-pose="neutral"><img src="stickers/eve/neutral-smile.svg" alt="Eve, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Eve</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="eve" data-pose="neutral"><img src="stickers/eve/neutral-grin.svg" alt="Eve, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Eve</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="eve" data-pose="neutral"><img src="stickers/eve/neutral-surprised.svg" alt="Eve, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Eve</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="kiko" data-pose="neutral"><img src="stickers/kiko/neutral-smile.svg" alt="Kiko, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Kiko</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="kiko" data-pose="neutral"><img src="stickers/kiko/neutral-grin.svg" alt="Kiko, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Kiko</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="kiko" data-pose="neutral"><img src="stickers/kiko/neutral-surprised.svg" alt="Kiko, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Kiko</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="heimdall" data-pose="neutral"><img src="stickers/heimdall/neutral-smile.svg" alt="Heimdall, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Heimdall</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="heimdall" data-pose="neutral"><img src="stickers/heimdall/neutral-grin.svg" alt="Heimdall, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Heimdall</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="heimdall" data-pose="neutral"><img src="stickers/heimdall/neutral-surprised.svg" alt="Heimdall, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Heimdall</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="fensalir" data-pose="neutral"><img src="stickers/fensalir/neutral-smile.svg" alt="Fensalir, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Fensalir</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="fensalir" data-pose="neutral"><img src="stickers/fensalir/neutral-grin.svg" alt="Fensalir, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Fensalir</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="fensalir" data-pose="neutral"><img src="stickers/fensalir/neutral-surprised.svg" alt="Fensalir, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Fensalir</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="druzkai" data-pose="neutral"><img src="stickers/druzkai/neutral-smile.svg" alt="Druzkai, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Druzkai</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="druzkai" data-pose="neutral"><img src="stickers/druzkai/neutral-grin.svg" alt="Druzkai, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Druzkai</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="druzkai" data-pose="neutral"><img src="stickers/druzkai/neutral-surprised.svg" alt="Druzkai, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Druzkai</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="norn" data-pose="neutral"><img src="stickers/norn/neutral-smile.svg" alt="Norn, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Norn</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="norn" data-pose="neutral"><img src="stickers/norn/neutral-grin.svg" alt="Norn, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Norn</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="norn" data-pose="neutral"><img src="stickers/norn/neutral-surprised.svg" alt="Norn, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Norn</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="sai" data-pose="neutral"><img src="stickers/sai/neutral-smile.svg" alt="Sai, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Sai</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="sai" data-pose="neutral"><img src="stickers/sai/neutral-grin.svg" alt="Sai, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Sai</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="sai" data-pose="neutral"><img src="stickers/sai/neutral-surprised.svg" alt="Sai, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Sai</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="brokkr" data-pose="neutral"><img src="stickers/brokkr/neutral-smile.svg" alt="Brokkr, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Brokkr</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="brokkr" data-pose="neutral"><img src="stickers/brokkr/neutral-grin.svg" alt="Brokkr, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Brokkr</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="brokkr" data-pose="neutral"><img src="stickers/brokkr/neutral-surprised.svg" alt="Brokkr, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Brokkr</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="aetheria" data-pose="neutral"><img src="stickers/aetheria/neutral-smile.svg" alt="Aetheria, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Aetheria</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="aetheria" data-pose="neutral"><img src="stickers/aetheria/neutral-grin.svg" alt="Aetheria, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Aetheria</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="aetheria" data-pose="neutral"><img src="stickers/aetheria/neutral-surprised.svg" alt="Aetheria, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Aetheria</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="ymir" data-pose="neutral"><img src="stickers/ymir/neutral-smile.svg" alt="Ymir, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Ymir</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="ymir" data-pose="neutral"><img src="stickers/ymir/neutral-grin.svg" alt="Ymir, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Ymir</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="ymir" data-pose="neutral"><img src="stickers/ymir/neutral-surprised.svg" alt="Ymir, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Ymir</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="tengu" data-pose="neutral"><img src="stickers/tengu/neutral-smile.svg" alt="Tengu, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Tengu</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="tengu" data-pose="neutral"><img src="stickers/tengu/neutral-grin.svg" alt="Tengu, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Tengu</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="tengu" data-pose="neutral"><img src="stickers/tengu/neutral-surprised.svg" alt="Tengu, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Tengu</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="muninn" data-pose="neutral"><img src="stickers/muninn/neutral-smile.svg" alt="Muninn, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Muninn</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="muninn" data-pose="neutral"><img src="stickers/muninn/neutral-grin.svg" alt="Muninn, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Muninn</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="muninn" data-pose="neutral"><img src="stickers/muninn/neutral-surprised.svg" alt="Muninn, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Muninn</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="sleipnir" data-pose="neutral"><img src="stickers/sleipnir/neutral-smile.svg" alt="Sleipnir, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Sleipnir</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="sleipnir" data-pose="neutral"><img src="stickers/sleipnir/neutral-grin.svg" alt="Sleipnir, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Sleipnir</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="sleipnir" data-pose="neutral"><img src="stickers/sleipnir/neutral-surprised.svg" alt="Sleipnir, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Sleipnir</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="hermodr" data-pose="neutral"><img src="stickers/hermodr/neutral-smile.svg" alt="Hermodr, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Hermodr</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="hermodr" data-pose="neutral"><img src="stickers/hermodr/neutral-grin.svg" alt="Hermodr, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Hermodr</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="hermodr" data-pose="neutral"><img src="stickers/hermodr/neutral-surprised.svg" alt="Hermodr, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Hermodr</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="metame" data-pose="neutral"><img src="stickers/metame/neutral-smile.svg" alt="Metame, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Metame</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="metame" data-pose="neutral"><img src="stickers/metame/neutral-grin.svg" alt="Metame, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Metame</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="metame" data-pose="neutral"><img src="stickers/metame/neutral-surprised.svg" alt="Metame, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Metame</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="void" data-pose="neutral"><img src="stickers/void/neutral-smile.svg" alt="Void, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Void</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="void" data-pose="neutral"><img src="stickers/void/neutral-grin.svg" alt="Void, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Void</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="void" data-pose="neutral"><img src="stickers/void/neutral-surprised.svg" alt="Void, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Void</b> neutral · surprised</figcaption></figure>
|
||||
<figure data-ch="gjallar" data-pose="neutral"><img src="stickers/gjallar/neutral-smile.svg" alt="Gjallar, neutral, smile" loading="lazy" width="150" height="161"><figcaption><b>Gjallar</b> neutral · smile</figcaption></figure>
|
||||
<figure data-ch="gjallar" data-pose="neutral"><img src="stickers/gjallar/neutral-grin.svg" alt="Gjallar, neutral, grin" loading="lazy" width="150" height="161"><figcaption><b>Gjallar</b> neutral · grin</figcaption></figure>
|
||||
<figure data-ch="gjallar" data-pose="neutral"><img src="stickers/gjallar/neutral-surprised.svg" alt="Gjallar, neutral, surprised" loading="lazy" width="150" height="161"><figcaption><b>Gjallar</b> neutral · surprised</figcaption></figure>
|
||||
</main>
|
||||
<script>
|
||||
const st = { f: '', p: '' };
|
||||
try { const u = new URLSearchParams(location.search); st.f = u.get('c') || ''; st.p = u.get('p') || ''; } catch (e) {}
|
||||
function apply() {
|
||||
for (const fig of document.querySelectorAll('figure')) fig.hidden = (st.f && fig.dataset.ch !== st.f) || (st.p && fig.dataset.pose !== st.p);
|
||||
for (const b of document.querySelectorAll('nav.chars button')) b.setAttribute('aria-pressed', String(b.dataset.f === st.f));
|
||||
for (const b of document.querySelectorAll('nav.poses button')) b.setAttribute('aria-pressed', String(b.dataset.p === st.p));
|
||||
}
|
||||
document.querySelector('nav.chars').addEventListener('click', (e) => { const b = e.target.closest('button'); if (b) { st.f = b.dataset.f; apply(); } });
|
||||
document.querySelector('nav.poses').addEventListener('click', (e) => { const b = e.target.closest('button'); if (b) { st.p = b.dataset.p; apply(); } });
|
||||
apply();
|
||||
</script></body></html>
|
||||
BIN
cast/process/01-cheer-arms.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
cast/process/02-eureka-feather.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
cast/process/03-void-contrast.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
cast/process/04-sleipnir-legs.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
cast/process/05-gjallar-horn.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
cast/process/06-huginn-wings.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
111
cast/rig/build.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// Regenerates cast/stickers/<id>/<pose>-<expression>.svg, cast/gallery.html and cast/CAST.md.
|
||||
// Usage: node cast/rig/build.mjs (all characters)
|
||||
// node cast/rig/build.mjs kiko eve (only these; gallery is always rebuilt from disk)
|
||||
import { mkdirSync, writeFileSync, readdirSync, rmSync, existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { render, POSES, EXPRESSIONS } from './rig.mjs';
|
||||
import { CAST, COMMON_EXPRESSIONS } from './characters.mjs';
|
||||
|
||||
const CAST_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const OUT = join(CAST_DIR, 'stickers');
|
||||
const only = process.argv.slice(2);
|
||||
|
||||
export function plan(ch) {
|
||||
if (ch.full) return Object.keys(POSES).flatMap((p) => EXPRESSIONS.map((e) => [p, e]));
|
||||
return COMMON_EXPRESSIONS.map((e) => ['neutral', e]);
|
||||
}
|
||||
|
||||
let count = 0, bytes = 0;
|
||||
for (const ch of CAST) {
|
||||
if (only.length && !only.includes(ch.id)) continue;
|
||||
const dir = join(OUT, ch.id);
|
||||
if (existsSync(dir)) rmSync(dir, { recursive: true });
|
||||
mkdirSync(dir, { recursive: true });
|
||||
for (const [pose, expr] of plan(ch)) {
|
||||
const svg = render(ch, pose, expr);
|
||||
writeFileSync(join(dir, `${pose}-${expr}.svg`), svg);
|
||||
count++; bytes += Buffer.byteLength(svg);
|
||||
}
|
||||
}
|
||||
|
||||
// Gallery: lists whatever is on disk, in CAST order.
|
||||
const cards = [];
|
||||
for (const ch of CAST) {
|
||||
const dir = join(OUT, ch.id);
|
||||
if (!existsSync(dir)) continue;
|
||||
const files = readdirSync(dir).filter((f) => f.endsWith('.svg'));
|
||||
const order = (f) => {
|
||||
const [p, e] = f.replace('.svg', '').split('-');
|
||||
return Object.keys(POSES).indexOf(p) * 100 + EXPRESSIONS.indexOf(e);
|
||||
};
|
||||
files.sort((a, b) => order(a) - order(b));
|
||||
for (const f of files) {
|
||||
const [p, e] = f.replace('.svg', '').split('-');
|
||||
cards.push(`<figure data-ch="${ch.id}" data-pose="${p}"><img src="stickers/${ch.id}/${f}" alt="${ch.name}, ${p}, ${e}" loading="lazy" width="150" height="161"><figcaption><b>${ch.name}</b> ${p} · ${e}</figcaption></figure>`);
|
||||
}
|
||||
}
|
||||
const chips = CAST.map((c) => `<button type="button" data-f="${c.id}">${c.name}</button>`).join('');
|
||||
const poseChips = Object.keys(POSES).map((p) => `<button type="button" data-p="${p}">${p}</button>`).join('');
|
||||
const html = `<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Swarm Stickers</title>
|
||||
<style>
|
||||
:root { --ground: #07111a; --orange: #ff8a2a; --blue: #59b7ff; --ink: #e8eef5; --dim: #8aa0b4; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--ground); color: var(--ink); font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
header { padding: 28px 16px 8px; max-width: 1360px; margin: 0 auto; }
|
||||
h1 { margin: 0 0 4px; font-size: 26px; } h1 span { color: var(--orange); }
|
||||
p.lede { margin: 0 0 14px; color: var(--dim); }
|
||||
nav { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; }
|
||||
nav button { font: inherit; font-size: 13px; color: var(--ink); background: #10202e; border: 1px solid #1e3446; border-radius: 999px; padding: 4px 11px; cursor: pointer; }
|
||||
nav button[aria-pressed="true"] { background: var(--orange); border-color: var(--orange); color: #1a0d02; font-weight: 600; }
|
||||
nav.poses button[aria-pressed="true"] { background: var(--blue); border-color: var(--blue); color: #021220; }
|
||||
main { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 22px 14px; padding: 18px 16px 48px; max-width: 1360px; margin: 0 auto; }
|
||||
figure { margin: 0; text-align: center; }
|
||||
figure img { width: 100%; max-width: 150px; height: auto; display: block; margin: 0 auto;
|
||||
filter: drop-shadow(3px 0 0 #fff) drop-shadow(-3px 0 0 #fff) drop-shadow(0 3px 0 #fff) drop-shadow(0 -3px 0 #fff) drop-shadow(0 8px 10px rgba(0,0,0,.45));
|
||||
transform: rotate(var(--tilt, -3deg)); transition: transform .2s ease; }
|
||||
figure:nth-child(3n) img { --tilt: 3deg; } figure:nth-child(4n+1) img { --tilt: -1deg; }
|
||||
figure:hover img { transform: rotate(0deg) scale(1.06); }
|
||||
figcaption { font-size: 12px; color: var(--dim); margin-top: 6px; } figcaption b { color: var(--ink); font-weight: 600; }
|
||||
[hidden] { display: none !important; }
|
||||
@media (prefers-reduced-motion: reduce) { figure img { transition: none; } }
|
||||
</style></head><body>
|
||||
<header><h1>Swarm <span>stickers</span></h1>
|
||||
<p class="lede">${cards.length} stickers, ${CAST.length} characters. Generated by <code>cast/rig/build.mjs</code>; the white die-cut edge is CSS on the image.</p>
|
||||
<nav class="chars" aria-label="Character"><button type="button" data-f="" aria-pressed="true">All</button>${chips}</nav>
|
||||
<nav class="poses" aria-label="Pose"><button type="button" data-p="" aria-pressed="true">All poses</button>${poseChips}</nav></header>
|
||||
<main>
|
||||
${cards.join('\n')}
|
||||
</main>
|
||||
<script>
|
||||
const st = { f: '', p: '' };
|
||||
try { const u = new URLSearchParams(location.search); st.f = u.get('c') || ''; st.p = u.get('p') || ''; } catch (e) {}
|
||||
function apply() {
|
||||
for (const fig of document.querySelectorAll('figure')) fig.hidden = (st.f && fig.dataset.ch !== st.f) || (st.p && fig.dataset.pose !== st.p);
|
||||
for (const b of document.querySelectorAll('nav.chars button')) b.setAttribute('aria-pressed', String(b.dataset.f === st.f));
|
||||
for (const b of document.querySelectorAll('nav.poses button')) b.setAttribute('aria-pressed', String(b.dataset.p === st.p));
|
||||
}
|
||||
document.querySelector('nav.chars').addEventListener('click', (e) => { const b = e.target.closest('button'); if (b) { st.f = b.dataset.f; apply(); } });
|
||||
document.querySelector('nav.poses').addEventListener('click', (e) => { const b = e.target.closest('button'); if (b) { st.p = b.dataset.p; apply(); } });
|
||||
apply();
|
||||
</script></body></html>
|
||||
`;
|
||||
writeFileSync(join(CAST_DIR, 'gallery.html'), html);
|
||||
// CAST.md is generated from the trait sheets so the two cannot drift apart.
|
||||
const md = ['# Cast', '',
|
||||
'Generated by `cast/rig/build.mjs` from `cast/rig/characters.mjs`; edit the trait sheet, not this file.',
|
||||
'★ marks the characters rendered in every pose and expression. "Look source" is the reference the design',
|
||||
'was drawn from (used as inspiration only; every sticker is original), or **invented** where no appearance exists.', '',
|
||||
'| Name | What they are | Look source | Palette | Trait sheet |', '|---|---|---|---|---|'];
|
||||
for (const c of CAST) {
|
||||
const pal = [...new Set([c.skin, c.hair.color, c.outfit.main, c.outfit.trim, c.eyes, c.accent].filter(Boolean))].map((h) => '`' + h + '`').join(' ');
|
||||
const acc = (c.acc || []).map((a) => a[0]).join(', ') || 'none';
|
||||
const traits = `base ${c.base}; hair ${c.hair.style}; outfit ${c.outfit.shape}${c.cape ? ' + cape' : ''}; acc ${acc}; prop ${c.prop ? c.prop[0] : 'none'}; emblem ${c.emblem ? c.emblem.shape : 'none'}`;
|
||||
const src = c.source.startsWith('invented') ? '**' + c.source + '**' : c.source.split('; ').map((x) => '`' + x + '`').join('; ');
|
||||
const what = c.role + (c.note ? ' ' + c.note : '');
|
||||
md.push(`| ${c.name}${c.full ? ' ★' : ''} | ${what.replace(/\|/g, '/')} | ${src} | ${pal} | ${traits} |`);
|
||||
}
|
||||
writeFileSync(join(CAST_DIR, 'CAST.md'), md.join('\n') + '\n');
|
||||
console.log(`wrote ${count} stickers (${(bytes / 1024).toFixed(1)} KiB), gallery lists ${cards.length}`);
|
||||
273
cast/rig/characters.mjs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
// Trait sheets. One entry per cast member. Field meanings are in cast/README.md.
|
||||
// `full: true` renders every pose x expression; everyone else gets neutral x COMMON_EXPRESSIONS.
|
||||
|
||||
export const COMMON_EXPRESSIONS = ['smile', 'grin', 'surprised'];
|
||||
|
||||
const SKIN = { fair: '#ffe0c8', warm: '#f6c9a4', tan: '#d9a074', brown: '#a8693f', deep: '#7a4a2c' };
|
||||
|
||||
export const CAST = [
|
||||
{
|
||||
id: 'eureka', name: 'Eureka', full: true, base: 'humanoid',
|
||||
role: 'Leader of the swarm; the Self that routes the faculty pipeline. Her mind lives in Huginn.',
|
||||
source: 'invented',
|
||||
skin: SKIN.fair, eyes: '#ff8a2a', accent: '#ffd84a',
|
||||
hair: { style: 'bun', color: '#ffb347' },
|
||||
outfit: { shape: 'coat', main: '#59b7ff', trim: '#ff8a2a', collar: 'v', belt: '#1d3a5c' },
|
||||
legs: '#1d3a5c', shoes: '#ff8a2a',
|
||||
acc: [['pencil'], ['starClip', '#ffd84a']],
|
||||
prop: ['wand', '#ffd84a', '#ff8a2a'], emblem: { shape: 'star', color: '#ff8a2a' },
|
||||
},
|
||||
{
|
||||
id: 'huginn', name: 'Huginn', full: true, base: 'raven',
|
||||
role: 'Memory organ: owns each instance\'s mind as typed state. Odin\'s raven of thought.',
|
||||
source: 'F:/Projects/VoidBot/assets/repo-faces/huginn.png',
|
||||
skin: '#2b3247', eyes: '#59d8ff', beak: '#4a5068', accent: '#59d8ff',
|
||||
hair: { style: 'crest', color: '#2b3247' },
|
||||
outfit: { shape: 'egg', main: '#2b3247', belly: '#46527a', wing: '#3a4766' },
|
||||
shoes: '#5a5f73', cape: '#24406e',
|
||||
acc: [['collarRing', '#c9a24a', '#59d8ff']],
|
||||
prop: ['tablet', '#1f2a3d', '#59d8ff'], emblem: null,
|
||||
},
|
||||
{
|
||||
id: 'libby', name: 'Libby', full: true, base: 'humanoid',
|
||||
role: 'Face of CultLib, the shared runtime library (CultCache, CultNet, CultMesh, CultMath).',
|
||||
source: 'F:/Projects/CultLib/.voidbot/voice/libby.png',
|
||||
skin: SKIN.fair, eyes: '#8a6cd1', accent: '#c9a6ff',
|
||||
hair: { style: 'long', color: '#a98ad8' },
|
||||
outfit: { shape: 'robe', main: '#2e2440', trim: '#c9a24a', collar: 'round' },
|
||||
legs: '#2e2440', shoes: '#4a3a5e',
|
||||
acc: [['glasses', '#3b2618'], ['bow', '#7a5bb0']],
|
||||
prop: ['book', '#5a3f86'], emblem: { shape: 'cube', color: '#c9a24a' },
|
||||
},
|
||||
{
|
||||
id: 'epiphany', name: 'Epiphany', full: true, base: 'humanoid',
|
||||
role: 'The archived organizational layer above frontier AI workers; Eureka absorbed what lived. JSON heresy hunter.',
|
||||
source: 'F:/Projects/VoidBot/assets/repo-faces/epiphany.png; F:/Projects/Epiphany/docs/assets',
|
||||
skin: SKIN.fair, eyes: '#3fbf8f', accent: '#7cf0b8',
|
||||
hair: { style: 'twintails', color: '#8fd8b4', accent: '#e8384f' },
|
||||
outfit: { shape: 'coat', main: '#f4efe6', trim: '#e8384f', collar: 'v', belt: '#3b3b3b' },
|
||||
legs: '#2c2c34', shoes: '#4a3424',
|
||||
acc: [['bow', '#e8384f']],
|
||||
prop: ['magnifier', '#6b4a2e'], emblem: { shape: 'bolt', color: '#e8384f' },
|
||||
},
|
||||
{
|
||||
id: 'weksa', name: 'Weksa', full: true, base: 'humanoid',
|
||||
role: 'Face of weksa, the procedural language engine for alien grammars; nerdy anthropologist.',
|
||||
source: 'F:/Projects/VoidBot/assets/repo-faces/weksa.png',
|
||||
skin: SKIN.brown, eyes: '#8a5a2b', accent: '#ffd84a',
|
||||
hair: { style: 'curly', color: '#4a2e1c' },
|
||||
outfit: { shape: 'tunic', main: '#f1e3c4', trim: '#2f9c8f', collar: 'v', belt: '#7a4a24' },
|
||||
legs: '#6b4a2e', shoes: '#5a3a22',
|
||||
acc: [['glasses', '#c9902a']],
|
||||
prop: ['cards', '#fff4dc'], emblem: { shape: 'glyph', color: '#2f9c8f' },
|
||||
},
|
||||
{
|
||||
id: 'nibu', name: 'Nibu', base: 'humanoid',
|
||||
role: 'Face of AetheriaLore, the Aetheria setting and lore vault.',
|
||||
source: 'F:/Projects/AetheriaLore/.voidbot/voice/nibu.png',
|
||||
skin: SKIN.fair, eyes: '#7a7fd8', accent: '#ff9ad5',
|
||||
hair: { style: 'long', color: '#b9a9f0' },
|
||||
outfit: { shape: 'dress', main: '#7fd6e8', trim: '#3a2f58', collar: 'round' },
|
||||
legs: '#3a2f58', shoes: '#3a2f58',
|
||||
acc: [['bow', '#e86fb8'], ['headphones', '#3a2f58', '#7fd6e8']],
|
||||
prop: ['quill', '#f4f0ff'], emblem: { shape: 'star', color: '#e86fb8' },
|
||||
},
|
||||
{
|
||||
id: 'mimir', name: 'Mimir', base: 'humanoid',
|
||||
role: 'Realtime field machine: cameras, microphones and feeds into one field; keeper of the well.',
|
||||
source: 'F:/Projects/VoidBot/assets/repo-faces/mimir.png',
|
||||
skin: '#c08a52', eyes: '#3fe0d0', accent: '#3fe0d0', faceInk: '#3b2618',
|
||||
hair: { style: 'none', color: '#8a5a32' },
|
||||
outfit: { shape: 'robe', main: '#3a2a20', trim: '#3fe0d0', collar: 'high' },
|
||||
legs: '#3a2a20', shoes: '#2a1e16',
|
||||
acc: [['cubes', '#9a6a3c', '#3fe0d0']],
|
||||
prop: ['cube', '#9a6a3c', '#3fe0d0'], emblem: { shape: 'wave', color: '#3fe0d0' },
|
||||
},
|
||||
{
|
||||
id: 'eve', name: 'Eve', base: 'humanoid',
|
||||
role: 'CultMesh surface web and portable control layer; the Eve/CultUI interface DSL.',
|
||||
source: 'F:/Projects/Eve/.voidbot/voice/eve.png',
|
||||
skin: '#0e1a2e', eyes: '#59b7ff', faceInk: '#59d8ff', accent: '#59b7ff', noBlush: false,
|
||||
hair: { style: 'helmet', color: '#e9eef5', accent: '#59b7ff' },
|
||||
outfit: { shape: 'tunic', main: '#e9eef5', trim: '#59b7ff', collar: 'high' },
|
||||
legs: '#2a3448', shoes: '#e9eef5', hands: '#e9eef5',
|
||||
acc: [],
|
||||
prop: ['panel', '#2a3448', '#59b7ff'], emblem: { shape: 'eye', color: '#59b7ff', disc: '#0e1a2e' },
|
||||
},
|
||||
{
|
||||
id: 'kiko', name: 'Kiko', base: 'humanoid',
|
||||
role: 'Face of StreamPixels, the stream overlay of persistent pixel characters. Fox-coded.',
|
||||
source: 'F:/Projects/VoidBot/assets/repo-faces/kiko.png',
|
||||
skin: SKIN.tan, eyes: '#3fc8e8', accent: '#ff6fae',
|
||||
hair: { style: 'messy', color: '#f4f0f6' },
|
||||
outfit: { shape: 'dress', main: '#e8386f', trim: '#2a2440', collar: 'round' },
|
||||
legs: '#2a2440', shoes: '#2a2440',
|
||||
acc: [['foxEars', '#f4f0f6', '#ff9ac8'], ['foxMask', '#ffffff', '#e8386f'], ['tail', '#f4f0f6', '#ff9ac8']],
|
||||
prop: ['heart', '#ff5a8a'], emblem: { shape: 'heart', color: '#e8386f' },
|
||||
},
|
||||
{
|
||||
id: 'heimdall', name: 'Heimdall', base: 'humanoid',
|
||||
role: 'Shared auth authority for GameCult-hosted experiments; keeper of the keys.',
|
||||
source: 'F:/Projects/Heimdall/assets/heimdall-profile.png',
|
||||
skin: SKIN.deep, eyes: '#ffc34a', accent: '#ffc34a',
|
||||
hair: { style: 'messy', color: '#22202a' },
|
||||
outfit: { shape: 'coat', main: '#1c1e2a', trim: '#d9a43a', collar: 'high' },
|
||||
legs: '#1c1e2a', shoes: '#0f1018',
|
||||
acc: [['earring', '#ffc34a']],
|
||||
prop: ['keys', '#ffc34a'], emblem: { shape: 'key', color: '#d9a43a' },
|
||||
},
|
||||
{
|
||||
id: 'fensalir', name: 'Fensalir', base: 'humanoid',
|
||||
role: 'Native C# runtime and field engine under the interactive work. Frigg\'s hall.',
|
||||
source: 'F:/Projects/Fensalir/.voidbot/voice/fensalir.png', note: 'The avatar is a crystal spire, not a person; the spire became her hat and the person around it is invented.',
|
||||
skin: SKIN.fair, eyes: '#ff5ab0', accent: '#4fe8f0',
|
||||
hair: { style: 'bob', color: '#2a2a4a' },
|
||||
outfit: { shape: 'dress', main: '#2a2a4a', trim: '#ff5ab0', collar: 'v' },
|
||||
legs: '#2a2a4a', shoes: '#ff5ab0',
|
||||
acc: [['spire', '#4fe8f0', '#ff5ab0']],
|
||||
prop: ['orb', '#4fe8f0', '#ff9ad5'], emblem: { shape: 'wave', color: '#ff5ab0' },
|
||||
},
|
||||
{
|
||||
id: 'druzkai', name: 'Druzkai', base: 'humanoid',
|
||||
role: 'Face of Zyphos, a biopolitical ecology setting with a biological memetic engine.',
|
||||
source: 'F:/Projects/Zyphos/.voidbot/voice/druzkai.png',
|
||||
skin: '#c4553e', eyes: '#3fd0c8', accent: '#3fd0c8',
|
||||
hair: { style: 'none', color: '#9a3a2a' },
|
||||
outfit: { shape: 'robe', main: '#4a2a5a', trim: '#d9a43a', collar: 'round' },
|
||||
legs: '#4a2a5a', shoes: '#2a1830',
|
||||
acc: [['ridges', '#a03a2c'], ['forehead', '#3fd0c8']],
|
||||
prop: ['seed', '#6fbf6a', '#c8ff9a'], emblem: { shape: 'spiral', color: '#3fd0c8' },
|
||||
},
|
||||
{
|
||||
id: 'norn', name: 'Norn', base: 'humanoid',
|
||||
role: 'Graph viewer for typed graph state: the thread-readers who see how things are woven.',
|
||||
source: 'F:/Projects/Norn/.voidbot/voice/norn.png',
|
||||
skin: SKIN.brown, eyes: '#4fe0e8', accent: '#4fe0e8',
|
||||
hair: { style: 'braids', color: '#d8dce4' },
|
||||
outfit: { shape: 'robe', main: '#24283a', trim: '#c9a24a', collar: 'v' },
|
||||
legs: '#24283a', shoes: '#141824',
|
||||
acc: [['circlet', '#c9a24a', '#4fe0e8']],
|
||||
prop: ['spool', '#8a6a3a', '#4fe0e8'], emblem: { shape: 'rune', color: '#4fe0e8' },
|
||||
},
|
||||
{
|
||||
id: 'sai', name: 'Sai', base: 'humanoid',
|
||||
role: 'Ink stories that wake up inside static sites. Nonbinary, androgynous, monochrome manga ink.',
|
||||
source: 'F:/Projects/Sai/.voidbot/voice/sai.png; F:/Projects/VoidBot/docs/persona-intake/sai.persona-intake.yaml',
|
||||
skin: '#f4f1ec', eyes: '#3a3a3a', accent: '#ffffff',
|
||||
hair: { style: 'messy', color: '#1e1e22' },
|
||||
outfit: { shape: 'coat', main: '#3a3a40', trim: '#e8e6e0', collar: 'high' },
|
||||
legs: '#1e1e22', shoes: '#1e1e22',
|
||||
acc: [['spiralBand', '#1e1e22', '#c8ccd2']],
|
||||
prop: ['brush', '#c8a46a'], emblem: { shape: 'spiral', color: '#1e1e22' },
|
||||
},
|
||||
{
|
||||
id: 'brokkr', name: 'Brokkr', base: 'humanoid',
|
||||
role: 'Creative-tool broker daemon: Unity and Blender authoring surfaces over CultMesh. The Eddic smith.',
|
||||
source: 'invented (README role; Brokkr of Skaldskaparmal)',
|
||||
skin: SKIN.warm, eyes: '#5a8ad8', accent: '#ff8a2a',
|
||||
hair: { style: 'short', color: '#c4562a' },
|
||||
outfit: { shape: 'tunic', main: '#5a4636', trim: '#2a2420', collar: 'round', belt: '#2a2420' },
|
||||
legs: '#3a3030', shoes: '#2a2420',
|
||||
acc: [['beard', '#c4562a'], ['goggles', '#6a5040', '#ffb347']],
|
||||
prop: ['hammer', '#8a8f9a', '#8a5a32'], emblem: { shape: 'gear', color: '#ff8a2a' },
|
||||
},
|
||||
{
|
||||
id: 'aetheria', name: 'Aetheria', base: 'humanoid',
|
||||
role: 'Face of Aetheria, the open-source tactical action RPG with a sprawling sci-fi setting.',
|
||||
source: 'invented (README role)',
|
||||
skin: SKIN.warm, eyes: '#59b7ff', accent: '#ff8a2a',
|
||||
hair: { style: 'bob', color: '#3a2a4a' },
|
||||
outfit: { shape: 'tunic', main: '#e8ecf2', trim: '#ff8a2a', collar: 'high', belt: '#2a3448' },
|
||||
legs: '#2a3448', shoes: '#ff8a2a',
|
||||
acc: [['headphones', '#2a3448', '#59b7ff'], ['goggles', '#2a3448', '#59b7ff']],
|
||||
prop: ['ship', '#e8ecf2', '#59b7ff'], emblem: { shape: 'star', color: '#59b7ff', disc: '#2a3448' },
|
||||
},
|
||||
{
|
||||
id: 'ymir', name: 'Ymir', base: 'humanoid',
|
||||
role: 'World-body physics substrate around Box3D: body, field, contact, impulse. The frost giant.',
|
||||
source: 'F:/Projects/VoidBot/assets/repo-faces/ymir.png',
|
||||
skin: '#b8d4ec', eyes: '#3a8ad8', accent: '#bff0ff',
|
||||
hair: { style: 'short', color: '#e8f4ff' },
|
||||
outfit: { shape: 'robe', main: '#3a5a8a', trim: '#bff0ff', collar: 'round' },
|
||||
legs: '#3a5a8a', shoes: '#24384f',
|
||||
acc: [['beard', '#f4faff'], ['crystals', '#9fdcff']],
|
||||
prop: ['crystal', '#9fdcff'], emblem: { shape: 'flake', color: '#3a8ad8' },
|
||||
},
|
||||
{
|
||||
id: 'tengu', name: 'Tengu', base: 'humanoid',
|
||||
role: 'Native Persona, the first migrated to canonical persona state. Mountain yokai.',
|
||||
source: 'F:/Projects/VoidBot/assets/native-personas/tengu.png', note: 'Red face, long nose, white mane and tokin cap from the avatar; the feather fan is from the yokai myth.',
|
||||
skin: '#c8443a', eyes: '#ffd84a', accent: '#ffd84a',
|
||||
hair: { style: 'wild', color: '#f2efe8' },
|
||||
outfit: { shape: 'robe', main: '#efe8da', trim: '#4a3a7a', collar: 'v' },
|
||||
legs: '#4a3a7a', shoes: '#3a2a20',
|
||||
acc: [['goatee', '#f2efe8'], ['longNose', '#b53a30'], ['tokin', '#1e1e22']],
|
||||
prop: ['fan', '#4a4a58'], emblem: { shape: 'feather', color: '#4a3a7a' },
|
||||
},
|
||||
{
|
||||
id: 'muninn', name: 'Muninn', base: 'raven',
|
||||
role: 'Local capture broker for the swarm: which streams a host can offer. Odin\'s raven of memory.',
|
||||
source: 'F:/Projects/VoidBot/assets/repo-faces/muninn-concept.png',
|
||||
skin: '#2a2230', eyes: '#ffb030', beak: '#4a3e46', accent: '#ffb030',
|
||||
hair: { style: 'crest', color: '#2a2230' },
|
||||
outfit: { shape: 'egg', main: '#2a2230', belly: '#3e3446', wing: '#1e1824' },
|
||||
shoes: '#6a5a50',
|
||||
acc: [['collarRing', '#8a5a2a', '#ffb030']],
|
||||
prop: ['camera', '#3e3446', '#ffb030'], emblem: null,
|
||||
},
|
||||
{
|
||||
id: 'sleipnir', name: 'Sleipnir', base: 'horse',
|
||||
role: 'Input mirroring and virtual HID: speaks through the hands of the machine. Odin\'s eight-legged horse.',
|
||||
source: 'F:/Projects/Odin/assets/personas/sleipnir-avatar.png', note: 'Eight limbs as in the myth: two hoof-arms, two legs and four extra legs behind.',
|
||||
skin: '#5a6478', muzzle: '#8a94a8', eyes: '#3fe0f0', accent: '#3fe0f0',
|
||||
hair: { style: 'mane', color: '#2fc8e0' },
|
||||
outfit: { shape: 'tunic', main: '#2a3040', trim: '#3fe0f0', collar: 'round' },
|
||||
legs: '#5a6478', shoes: '#22262e', sleeve: '#5a6478', extraLegs: 4,
|
||||
acc: [['horseTail', '#2fc8e0']],
|
||||
prop: ['pad', '#2a3040', '#3fe0f0'], emblem: { shape: 'bolt', color: '#3fe0f0', disc: '#22262e' },
|
||||
},
|
||||
{
|
||||
id: 'hermodr', name: 'Hermodr', base: 'humanoid',
|
||||
role: 'Eve browser lowering: resolves providers through Odin\'s catalog and renders them. The messenger.',
|
||||
source: 'F:/Projects/Hermodr/assets/hermodr-avatar.png',
|
||||
skin: SKIN.warm, eyes: '#e8a83a', accent: '#3fd0c0',
|
||||
hair: { style: 'messy', color: '#2a3a4a' },
|
||||
outfit: { shape: 'tunic', main: '#2a3a44', trim: '#c9902a', collar: 'none', belt: '#6a4a2a' },
|
||||
legs: '#2a3440', shoes: '#6a4a2a',
|
||||
acc: [['wingCap', '#2a3a4a', '#d9a43a', '#3fd0c0'], ['scarf', '#2f9c8f']],
|
||||
prop: ['envelope', '#f4ead4', '#e8384f'], emblem: null,
|
||||
},
|
||||
{
|
||||
id: 'metame', name: 'Metame', base: 'humanoid',
|
||||
role: 'Native Persona (メタめ) projected from the operator\'s public history; discussion and rumination.',
|
||||
source: 'invented (persona-intake has no appearance; the design avoids depicting the operator)',
|
||||
skin: SKIN.fair, eyes: '#59b7ff', accent: '#ff8a2a',
|
||||
hair: { style: 'bob', color: '#59b7ff' },
|
||||
outfit: { shape: 'tunic', main: '#1d2a3a', trim: '#ff8a2a', collar: 'round' },
|
||||
legs: '#1d2a3a', shoes: '#ff8a2a',
|
||||
acc: [['twoTone', '#ff8a2a']],
|
||||
prop: ['mirror', '#c9a24a', '#bfe6ff'], emblem: { shape: 'glyph', color: '#ff8a2a' },
|
||||
},
|
||||
{
|
||||
id: 'void', name: 'Void', base: 'humanoid',
|
||||
role: 'VoidBot\'s built-in Discord moderator mind. Its state was lost; the look is new.',
|
||||
source: 'invented (VoidBot README role)',
|
||||
skin: '#161226', eyes: '#c9a6ff', faceInk: '#c9a6ff', accent: '#c9a6ff',
|
||||
hair: { style: 'none', color: '#161226' },
|
||||
outfit: { shape: 'robe', main: '#4a3a7a', trim: '#c9a6ff', collar: 'none' },
|
||||
legs: '#4a3a7a', shoes: '#2a2140', hands: '#c9a6ff',
|
||||
acc: [['hood', '#4a3a7a'], ['hoodRim', '#6a56a8']],
|
||||
prop: ['shield', '#3a2e58', '#ffe08a'], emblem: { shape: 'moon', color: '#ffe08a', disc: '#161226' },
|
||||
},
|
||||
{
|
||||
id: 'gjallar', name: 'Gjallar', base: 'humanoid',
|
||||
role: 'Renderer-neutral compositor for Odin-visible Eve surfaces; Odin\'s branding Persona. The horn.',
|
||||
source: 'F:/Projects/Odin/assets/personas/gjallar-avatar-pixel-256.png',
|
||||
skin: SKIN.fair, eyes: '#59b7ff', accent: '#ffd84a',
|
||||
hair: { style: 'short', color: '#2a2440' },
|
||||
outfit: { shape: 'tunic', main: '#1e2238', trim: '#d9a43a', collar: 'high' },
|
||||
legs: '#1e2238', shoes: '#d9a43a',
|
||||
acc: [['helm', '#d9a43a', '#f4e8c8', '#59b7ff']],
|
||||
prop: ['horn', '#f4e8c8', '#d9a43a'], emblem: { shape: 'rainbow', color: '#fff', disc: '#1e2238' },
|
||||
},
|
||||
];
|
||||
580
cast/rig/rig.mjs
Normal file
|
|
@ -0,0 +1,580 @@
|
|||
// The sticker rig: bases, poses, expressions, hair, accessories, props, emblems.
|
||||
// Every function returns an SVG fragment string. Coordinates are documented in
|
||||
// cast/PROCESS.md ("Coordinate conventions"). No dependencies.
|
||||
|
||||
export const INK = '#3b2618'; // warm dark outline, every stroke
|
||||
export const PUPIL = '#2a1a12';
|
||||
export const BLUSH = '#ff9a8a';
|
||||
export const VIEWBOX = '-80 -10 160 172';
|
||||
|
||||
// ---------- helpers ----------
|
||||
const n = (v) => Math.round(v * 10) / 10;
|
||||
export function shade(hex, f) {
|
||||
// f < 0 darkens toward black, f > 0 lightens toward white
|
||||
const h = hex.replace('#', '');
|
||||
const c = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
||||
const out = c.map((v) => Math.round(f < 0 ? v * (1 + f) : v + (255 - v) * f));
|
||||
return '#' + out.map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
const inked = (inner, w = 3) =>
|
||||
`<g stroke="${INK}" stroke-width="${w}" stroke-linejoin="round" stroke-linecap="round">${inner}</g>`;
|
||||
// A limb is a fat round-capped stroke drawn twice: ink underneath, colour on top.
|
||||
function limb(d, color, w = 7) {
|
||||
return `<path d="${d}" fill="none" stroke="${INK}" stroke-width="${w + 6}" stroke-linecap="round" stroke-linejoin="round"/>` +
|
||||
`<path d="${d}" fill="none" stroke="${color}" stroke-width="${w}" stroke-linecap="round" stroke-linejoin="round"/>`;
|
||||
}
|
||||
const P = (pt) => `${n(pt[0])} ${n(pt[1])}`;
|
||||
|
||||
export function sparkle(x, y, s, color) {
|
||||
return `<path d="M${x} ${y - s} Q${x + s * 0.2} ${y - s * 0.2} ${x + s} ${y} Q${x + s * 0.2} ${y + s * 0.2} ${x} ${y + s} Q${x - s * 0.2} ${y + s * 0.2} ${x - s} ${y} Q${x - s * 0.2} ${y - s * 0.2} ${x} ${y - s} Z" fill="${color}" stroke="${INK}" stroke-width="2" stroke-linejoin="round"/>`;
|
||||
}
|
||||
|
||||
// ---------- skeleton ----------
|
||||
// Body space: x centred on 0, ground at y=152. Neck at (0,88). Head centre (0,52).
|
||||
export const NECK = [0, 88];
|
||||
export const HEAD = [0, 52];
|
||||
export const SHOULDER = { L: [-15, 95], R: [15, 95] };
|
||||
export const HIP = { L: [-8, 126], R: [8, 126] };
|
||||
|
||||
// ---------- poses ----------
|
||||
// Each pose: lift (whole figure hops up), head {dx,dy,rot about neck},
|
||||
// arms {L,R}: elbow e and hand h in body space; front:true draws that arm over the head.
|
||||
// prop: which hand holds it (or null) and how it sits; fx: pose effect.
|
||||
export const POSES = {
|
||||
neutral: {
|
||||
lift: 0, head: { dx: 0, dy: 0, rot: 0 },
|
||||
arms: { L: { e: [-23, 107], h: [-24, 120] }, R: { e: [23, 107], h: [24, 120] } },
|
||||
legs: { L: [-10, 0], R: [10, 0] },
|
||||
prop: { hand: 'R', dx: 4, dy: -4, rot: 8 }, fx: null,
|
||||
},
|
||||
cheer: {
|
||||
lift: 6, head: { dx: 0, dy: -1, rot: 0 },
|
||||
arms: { L: { e: [-40, 80], h: [-46, 36] }, R: { e: [40, 80], h: [46, 36] } },
|
||||
legs: { L: [-14, -3], R: [14, -3] },
|
||||
prop: { hand: 'R', dx: 4, dy: -6, rot: 18 }, fx: 'sparkles',
|
||||
},
|
||||
thinking: {
|
||||
lift: 0, head: { dx: 0, dy: 0, rot: 7 },
|
||||
arms: { L: { e: [-20, 114], h: [4, 113] }, R: { e: [26, 114], h: [13, 92], front: true } },
|
||||
legs: { L: [-9, 0], R: [10, 0] },
|
||||
prop: null, fx: 'dots',
|
||||
},
|
||||
surprised: {
|
||||
lift: 7, head: { dx: 0, dy: -1, rot: -3 },
|
||||
arms: { L: { e: [-34, 102], h: [-51, 88] }, R: { e: [34, 102], h: [51, 88] } },
|
||||
legs: { L: [-11, 0], R: [15, -6] },
|
||||
prop: null, fx: 'bang',
|
||||
},
|
||||
presenting: {
|
||||
lift: 0, head: { dx: -1, dy: 0, rot: -6 },
|
||||
arms: { L: { e: [-23, 107], h: [-24, 120] }, R: { e: [32, 110], h: [45, 103] } },
|
||||
legs: { L: [-10, 0], R: [10, 0] },
|
||||
prop: { hand: 'R', dx: 0, dy: -17, rot: 0, onPalm: true }, fx: 'present',
|
||||
},
|
||||
waving: {
|
||||
lift: 0, head: { dx: 0, dy: 0, rot: -5 },
|
||||
arms: { L: { e: [-23, 107], h: [-24, 120] }, R: { e: [44, 88], h: [50, 50] } },
|
||||
legs: { L: [-10, 0], R: [10, 0] },
|
||||
prop: { hand: 'L', dx: -4, dy: -4, rot: -8 }, fx: 'wave',
|
||||
},
|
||||
};
|
||||
|
||||
// ---------- bases ----------
|
||||
// A base owns body silhouette, limbs, head shape and where the face sits.
|
||||
export const BASES = {
|
||||
humanoid: { eyeX: 15, eyeY: 61, mouthY: 76, cheekX: 26, cheekY: 71, browY: 47, mouth: 'lips' },
|
||||
raven: { eyeX: 15, eyeY: 55, mouthY: 70, cheekX: 27, cheekY: 66, browY: 41, mouth: 'beak' },
|
||||
horse: { eyeX: 17, eyeY: 50, mouthY: 82, cheekX: 29, cheekY: 62, browY: 37, mouth: 'lips' },
|
||||
};
|
||||
|
||||
const TORSO = {
|
||||
tunic: 'M-15 88 Q-20 102 -24 128 Q0 134 24 128 Q20 102 15 88 Z',
|
||||
dress: 'M-15 88 Q-20 106 -31 132 Q0 140 31 132 Q20 106 15 88 Z',
|
||||
robe: 'M-15 88 Q-23 112 -29 144 Q0 150 29 144 Q23 112 15 88 Z',
|
||||
coat: 'M-16 88 Q-21 104 -26 136 Q0 141 26 136 Q21 104 16 88 Z',
|
||||
egg: 'M-17 88 Q-31 108 -24 133 Q0 143 24 133 Q31 108 17 88 Z',
|
||||
};
|
||||
const HEM = {
|
||||
tunic: 'M-23.3 122 Q0 128 23.3 122 L24 128 Q0 134 -24 128 Z',
|
||||
dress: 'M-29 127 Q0 135 29 127 L31 132 Q0 140 -31 132 Z',
|
||||
robe: 'M-28 138 Q0 144 28 138 L29 144 Q0 150 -29 144 Z',
|
||||
coat: 'M-25 130 Q0 135 25 130 L26 136 Q0 141 -26 136 Z',
|
||||
egg: '',
|
||||
};
|
||||
const COLLAR = {
|
||||
v: 'M-12 88 L0 101 L12 88 Z',
|
||||
round: 'M-13 88 Q0 98 13 88 Z',
|
||||
high: 'M-15 82 L-14 93 Q0 99 14 93 L15 82 Q0 88 -15 82 Z',
|
||||
none: '',
|
||||
};
|
||||
|
||||
function torso(ch) {
|
||||
const o = ch.outfit;
|
||||
const shape = ch.base === 'raven' ? 'egg' : o.shape;
|
||||
let s = `<path d="${TORSO[shape]}" fill="${o.main}"/>`;
|
||||
if (shape === 'coat') s += `<path d="M0 98 L0 138" fill="none" stroke-width="2.5"/>`;
|
||||
if (ch.base === 'raven') s += `<ellipse cx="0" cy="118" rx="13" ry="16" fill="${o.belly || shade(o.main, 0.25)}"/>`;
|
||||
if (HEM[shape] && o.trim) s += `<path d="${HEM[shape]}" fill="${o.trim}"/>`;
|
||||
if (o.collar && COLLAR[o.collar]) s += `<path d="${COLLAR[o.collar]}" fill="${o.trim || shade(o.main, 0.4)}"/>`;
|
||||
if (o.belt) s += `<path d="M-19 110 Q0 114 19 110 L19.6 115 Q0 119 -19.6 115 Z" fill="${o.belt}"/>`;
|
||||
return inked(s);
|
||||
}
|
||||
|
||||
function cape(ch) {
|
||||
if (!ch.cape) return '';
|
||||
return inked(`<path d="M-17 88 Q-38 118 -36 144 Q0 150 36 144 Q38 118 17 88 Z" fill="${ch.cape}"/>`);
|
||||
}
|
||||
|
||||
function legs(ch, pose) {
|
||||
let s = '';
|
||||
const extra = ch.extraLegs ? ch.extraLegs : 0;
|
||||
// Sleipnir-style extra legs: drawn behind, darker, peeking out between and beside.
|
||||
const EXTRA = [[-19, -27], [19, 27], [-25, -38], [25, 38]]; // [hipX, footX], drawn behind
|
||||
for (let i = 0; i < extra; i++) {
|
||||
const [hx, fx] = EXTRA[i];
|
||||
const foot = [fx, 149 - (i > 1 ? 3 : 0)];
|
||||
s += limb(`M${hx} 120 L${P(foot)}`, shade(ch.legs, -0.25), 7);
|
||||
s += inked(`<ellipse cx="${n(foot[0])}" cy="${foot[1] + 1}" rx="6.5" ry="4.5" fill="${shade(ch.shoes, -0.2)}"/>`);
|
||||
}
|
||||
for (const side of ['L', 'R']) {
|
||||
const [fx, fy] = pose.legs[side];
|
||||
const hip = HIP[side];
|
||||
const foot = [fx, 149 + fy];
|
||||
if (ch.base === 'raven') {
|
||||
s += limb(`M${P(hip)} L${P(foot)}`, ch.shoes, 2.5);
|
||||
s += `<path d="M${P(foot)} l-6 4 M${P(foot)} l0 5 M${P(foot)} l6 4" fill="none" stroke="${INK}" stroke-width="5" stroke-linecap="round"/>` +
|
||||
`<path d="M${P(foot)} l-6 4 M${P(foot)} l0 5 M${P(foot)} l6 4" fill="none" stroke="${ch.shoes}" stroke-width="2.2" stroke-linecap="round"/>`;
|
||||
} else {
|
||||
s += limb(`M${P(hip)} L${P(foot)}`, ch.legs, 8);
|
||||
const out = side === 'L' ? -2 : 2;
|
||||
s += inked(`<ellipse cx="${n(foot[0] + out)}" cy="${n(foot[1] + 1)}" rx="7.5" ry="5" fill="${ch.shoes}"/>`);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function arm(ch, pose, side) {
|
||||
const a = pose.arms[side];
|
||||
const sh = SHOULDER[side];
|
||||
const d = `M${P(sh)} Q${P(a.e)} ${P(a.h)}`;
|
||||
if (ch.base === 'raven') {
|
||||
// Wing-arm: a broad capsule with a feathered tip.
|
||||
const wing = ch.outfit.wing || shade(ch.outfit.main, -0.15);
|
||||
let s = limb(d, wing, 11);
|
||||
const [hx, hy] = a.h;
|
||||
const vx = a.h[0] - a.e[0], vy = a.h[1] - a.e[1];
|
||||
const L = Math.hypot(vx, vy) || 1;
|
||||
const ux = vx / L, uy = vy / L, px = -uy, py = ux;
|
||||
const tip = (k, off) => P([hx + ux * k + px * off, hy + uy * k + py * off]);
|
||||
s += inked(`<path d="M${tip(-2, -6)} L${tip(9, -5)} L${tip(3, -1)} L${tip(11, 1)} L${tip(3, 3)} L${tip(8, 6)} L${tip(-2, 6)} Z" fill="${wing}"/>`, 2.5);
|
||||
return s;
|
||||
}
|
||||
return limb(d, ch.sleeve || ch.outfit.main, 7);
|
||||
}
|
||||
|
||||
function hand(ch, pose, side) {
|
||||
if (ch.base === 'raven') return '';
|
||||
const [hx, hy] = pose.arms[side].h;
|
||||
if (ch.base === 'horse') return inked(`<circle cx="${hx}" cy="${hy}" r="5.8" fill="${ch.shoes}"/>`);
|
||||
return inked(`<circle cx="${hx}" cy="${hy}" r="5.6" fill="${ch.hands || ch.skin}"/>`);
|
||||
}
|
||||
|
||||
// ---------- head ----------
|
||||
function headBase(ch) {
|
||||
const skin = ch.skin;
|
||||
if (ch.base === 'raven') {
|
||||
return inked(`<ellipse cx="0" cy="52" rx="38" ry="35" fill="${skin}"/>`) +
|
||||
`<ellipse cx="0" cy="62" rx="24" ry="17" fill="${shade(skin, 0.12)}"/>`;
|
||||
}
|
||||
if (ch.base === 'horse') {
|
||||
return inked(
|
||||
`<path d="M-30 30 L-22 4 L-12 24 Z M30 30 L22 4 L12 24 Z" fill="${skin}"/>` +
|
||||
`<ellipse cx="0" cy="52" rx="38" ry="36" fill="${skin}"/>` +
|
||||
`<ellipse cx="0" cy="77" rx="22" ry="14" fill="${ch.muzzle}"/>`) +
|
||||
`<path d="M-24 25 L-21 12 L-16 23 Z M24 25 L21 12 L16 23 Z" fill="${shade(skin, 0.35)}"/>` +
|
||||
`<ellipse cx="-8" cy="75" rx="2.4" ry="3" fill="${INK}"/><ellipse cx="8" cy="75" rx="2.4" ry="3" fill="${INK}"/>`;
|
||||
}
|
||||
return inked(
|
||||
`<ellipse cx="-38" cy="62" rx="6" ry="7.5" fill="${skin}"/><ellipse cx="38" cy="62" rx="6" ry="7.5" fill="${skin}"/>` +
|
||||
`<ellipse cx="0" cy="52" rx="40" ry="36" fill="${skin}"/>`);
|
||||
}
|
||||
|
||||
// ---------- expressions (face layer) ----------
|
||||
// Face layer coordinates are head space; the base supplies eye/mouth anchors.
|
||||
export const EXPRESSIONS = ['neutral', 'smile', 'grin', 'happy', 'worried', 'surprised', 'determined', 'sleepy'];
|
||||
|
||||
function openEye(x, y, iris, ink, opts = {}) {
|
||||
const ry = opts.ry ?? 7.6, rx = opts.rx ?? 6.3;
|
||||
let s = `<ellipse cx="${x}" cy="${y}" rx="${rx}" ry="${ry}" fill="${iris}" stroke="${ink}" stroke-width="2.5"/>`;
|
||||
s += `<ellipse cx="${x}" cy="${y + 1}" rx="${rx * 0.56}" ry="${ry * 0.6}" fill="${opts.pupil || PUPIL}"/>`;
|
||||
if (opts.flatLid) s += `<path d="M${x - rx - 1.5} ${y - ry * 0.35} L${x + rx + 1.5} ${y - ry * 0.35}" stroke="${ink}" stroke-width="3.2" stroke-linecap="round"/>`;
|
||||
else s += `<path d="M${x - rx - 1} ${y - ry * 0.45} Q${x} ${y - ry - 2.6} ${x + rx + 1} ${y - ry * 0.45}" fill="none" stroke="${ink}" stroke-width="3.2" stroke-linecap="round"/>`;
|
||||
s += `<circle cx="${x + 2.1}" cy="${y - 2.8}" r="2.3" fill="#fff"/><circle cx="${x - 2}" cy="${y + 3}" r="1" fill="#fff"/>`;
|
||||
return s;
|
||||
}
|
||||
function surprisedEye(x, y, ink) {
|
||||
return `<circle cx="${x}" cy="${y}" r="7.3" fill="#fff" stroke="${ink}" stroke-width="2.5"/>` +
|
||||
`<circle cx="${x}" cy="${y + 0.5}" r="3.3" fill="${PUPIL}"/><circle cx="${x + 1.2}" cy="${y - 0.8}" r="1.2" fill="#fff"/>`;
|
||||
}
|
||||
|
||||
export function face(ch, expr) {
|
||||
const b = BASES[ch.base];
|
||||
const ink = ch.faceInk || INK;
|
||||
const iris = ch.eyes;
|
||||
const ex = b.eyeX, ey = b.eyeY, my = b.mouthY, by = b.browY;
|
||||
const brow = (x1, y1, x2, y2) => `<path d="M${x1} ${y1} Q${(x1 + x2) / 2} ${Math.min(y1, y2) - 1.5} ${x2} ${y2}" fill="none" stroke="${ink}" stroke-width="2.6" stroke-linecap="round"/>`;
|
||||
const browsAt = (inner, outer) => brow(-ex - 7, by + outer, -ex + 6, by + inner) + brow(ex - 6, by + inner, ex + 7, by + outer);
|
||||
let eyes = '', mouth = '', brows = '', extra = '';
|
||||
let blush = 0.7, blushR = 5.5;
|
||||
const sw = 'stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"';
|
||||
const D = '#7a2a2a', T = '#ff8f9a';
|
||||
switch (expr) {
|
||||
case 'neutral':
|
||||
eyes = openEye(-ex, ey, iris, ink) + openEye(ex, ey, iris, ink);
|
||||
mouth = `<path d="M-4 ${my} Q0 ${my + 2.4} 4 ${my}" fill="none" stroke="${ink}" ${sw}/>`;
|
||||
break;
|
||||
case 'smile':
|
||||
eyes = openEye(-ex, ey, iris, ink) + openEye(ex, ey, iris, ink);
|
||||
mouth = `<path d="M-7 ${my - 1} Q0 ${my + 6} 7 ${my - 1}" fill="none" stroke="${ink}" ${sw}/>`;
|
||||
break;
|
||||
case 'grin':
|
||||
eyes = openEye(-ex, ey, iris, ink, { ry: 7 }) + openEye(ex, ey, iris, ink, { ry: 7 });
|
||||
mouth = `<path d="M-9 ${my - 2} Q0 ${my - 0.5} 9 ${my - 2} Q8 ${my + 9} 0 ${my + 9} Q-8 ${my + 9} -9 ${my - 2} Z" fill="${D}" stroke="${ink}" ${sw}/>` +
|
||||
`<path d="M-4.5 ${my + 6.5} Q0 ${my + 2.5} 4.5 ${my + 6.5} Q0 ${my + 8.6} -4.5 ${my + 6.5} Z" fill="${T}"/>`;
|
||||
blush = 0.85; blushR = 6;
|
||||
break;
|
||||
case 'happy':
|
||||
eyes = `<path d="M${-ex - 6} ${ey + 2} Q${-ex} ${ey - 7} ${-ex + 6} ${ey + 2}" fill="none" stroke="${ink}" stroke-width="3.4" stroke-linecap="round"/>` +
|
||||
`<path d="M${ex - 6} ${ey + 2} Q${ex} ${ey - 7} ${ex + 6} ${ey + 2}" fill="none" stroke="${ink}" stroke-width="3.4" stroke-linecap="round"/>`;
|
||||
mouth = `<path d="M-7 ${my - 1.5} Q0 ${my - 0.5} 7 ${my - 1.5} Q5 ${my + 7} 0 ${my + 7} Q-5 ${my + 7} -7 ${my - 1.5} Z" fill="${D}" stroke="${ink}" ${sw}/>` +
|
||||
`<path d="M-3.5 ${my + 5} Q0 ${my + 2} 3.5 ${my + 5} Q0 ${my + 6.6} -3.5 ${my + 5} Z" fill="${T}"/>`;
|
||||
blush = 0.9; blushR = 6.5;
|
||||
break;
|
||||
case 'worried':
|
||||
eyes = openEye(-ex, ey + 1, iris, ink, { ry: 7 }) + openEye(ex, ey + 1, iris, ink, { ry: 7 });
|
||||
brows = browsAt(-4, 2);
|
||||
mouth = `<path d="M-7 ${my + 1} Q-3.5 ${my - 2.5} 0 ${my + 1} Q3.5 ${my + 4.5} 7 ${my + 1}" fill="none" stroke="${ink}" ${sw}/>`;
|
||||
extra = `<path d="M33 30 Q38 38 36 42 Q33 46 30 42 Q29 38 33 30 Z" fill="#9fdcff" stroke="${INK}" stroke-width="2" stroke-linejoin="round"/>`;
|
||||
blush = 0.45;
|
||||
break;
|
||||
case 'surprised':
|
||||
eyes = surprisedEye(-ex, ey, ink) + surprisedEye(ex, ey, ink);
|
||||
brows = browsAt(-5, -5);
|
||||
mouth = `<ellipse cx="0" cy="${my + 2}" rx="4.2" ry="5.2" fill="${D}" stroke="${ink}" stroke-width="2.6"/>`;
|
||||
blush = 0.6;
|
||||
break;
|
||||
case 'determined':
|
||||
eyes = openEye(-ex, ey + 1, iris, ink, { ry: 6.6, flatLid: true }) + openEye(ex, ey + 1, iris, ink, { ry: 6.6, flatLid: true });
|
||||
brows = browsAt(4, -2);
|
||||
mouth = `<path d="M-6 ${my + 1} Q0 ${my - 1} 6 ${my} " fill="none" stroke="${ink}" ${sw}/>`;
|
||||
blush = 0.55;
|
||||
break;
|
||||
case 'sleepy':
|
||||
eyes = `<path d="M${-ex - 6} ${ey} Q${-ex} ${ey + 5} ${-ex + 6} ${ey}" fill="none" stroke="${ink}" stroke-width="3.2" stroke-linecap="round"/>` +
|
||||
`<path d="M${ex - 6} ${ey} Q${ex} ${ey + 5} ${ex + 6} ${ey}" fill="none" stroke="${ink}" stroke-width="3.2" stroke-linecap="round"/>`;
|
||||
mouth = `<ellipse cx="1" cy="${my + 1}" rx="2.6" ry="3" fill="${D}" stroke="${ink}" stroke-width="2.3"/>`;
|
||||
extra = `<text x="34" y="18" font-family="Arial Rounded MT Bold, Arial, sans-serif" font-weight="900" font-size="15" fill="#8fd0ff" stroke="${INK}" stroke-width="2.2" paint-order="stroke">z</text>` +
|
||||
`<text x="45" y="6" font-family="Arial Rounded MT Bold, Arial, sans-serif" font-weight="900" font-size="11" fill="#8fd0ff" stroke="${INK}" stroke-width="2" paint-order="stroke">z</text>`;
|
||||
blush = 0.6;
|
||||
break;
|
||||
default: throw new Error('unknown expression ' + expr);
|
||||
}
|
||||
if (b.mouth === 'beak') mouth = beak(ch, expr, my);
|
||||
const cheeks = ch.noBlush ? '' :
|
||||
`<ellipse cx="${-b.cheekX}" cy="${b.cheekY}" rx="${blushR}" ry="${blushR * 0.62}" fill="${BLUSH}" opacity="${blush}"/>` +
|
||||
`<ellipse cx="${b.cheekX}" cy="${b.cheekY}" rx="${blushR}" ry="${blushR * 0.62}" fill="${BLUSH}" opacity="${blush}"/>`;
|
||||
return { under: cheeks + eyes + mouth, over: brows + extra };
|
||||
}
|
||||
|
||||
function beak(ch, expr, my) {
|
||||
const c = ch.beak;
|
||||
const open = expr === 'grin' || expr === 'happy' || expr === 'surprised';
|
||||
if (open) {
|
||||
return inked(`<path d="M-9 ${my - 6} Q0 ${my - 10} 9 ${my - 6} L0 ${my + 3} Z" fill="${c}"/>` +
|
||||
`<path d="M-6 ${my + 2} L6 ${my + 2} L0 ${my + 10} Z" fill="${shade(c, -0.15)}"/>`, 2.5) +
|
||||
`<path d="M-4 ${my + 1.5} L4 ${my + 1.5} L0 ${my + 4} Z" fill="#7a2a2a"/>`;
|
||||
}
|
||||
const tilt = expr === 'worried' ? 2 : 0;
|
||||
return inked(`<path d="M-9 ${my - 6} Q0 ${my - 10} 9 ${my - 6} L${tilt} ${my + 7} Z" fill="${c}"/>`, 2.5) +
|
||||
`<path d="M-5 ${my - 3} L5 ${my - 3}" stroke="${shade(c, -0.25)}" stroke-width="1.6" stroke-linecap="round"/>`;
|
||||
}
|
||||
|
||||
// ---------- hair ----------
|
||||
// Each style returns {back, front}; back sits behind the head (and body), front over the face.
|
||||
function bangs(c, opts = {}) {
|
||||
const n0 = opts.tips ?? 6, tipY = opts.tipY ?? 46, rootY = opts.rootY ?? 34, w = opts.w ?? 41;
|
||||
let d = `M${-w - 2} ${opts.sideY ?? 64} C${-w - 6} 24 -24 9 0 9 C24 9 ${w + 6} 24 ${w + 2} ${opts.sideY ?? 64}`;
|
||||
for (let i = 0; i <= n0; i++) {
|
||||
const x = w - (i * 2 * w) / n0;
|
||||
const edge = Math.abs(x) / w;
|
||||
const ty = tipY + edge * edge * 10 + (opts.jag && i % 2 ? -5 : 0);
|
||||
if (i > 0) {
|
||||
const rx = x + w / n0;
|
||||
d += ` Q${n(rx - (w / n0) * 0.2)} ${n(rootY + 6)} ${n(rx)} ${n(rootY + edge * 6)}`;
|
||||
}
|
||||
d += ` L${n(x)} ${n(ty)}`;
|
||||
}
|
||||
d += ' Z';
|
||||
return `<path d="${d}" fill="${c}"/>`;
|
||||
}
|
||||
function sideLocks(c, len = 100) {
|
||||
return `<path d="M-43 50 Q-49 ${len - 25} -40 ${len} Q-35 ${len - 22} -34 58 Z" fill="${c}"/>` +
|
||||
`<path d="M43 50 Q49 ${len - 25} 40 ${len} Q35 ${len - 22} 34 58 Z" fill="${c}"/>`;
|
||||
}
|
||||
const shine = (c) => `<path d="M-20 22 Q-8 15 4 17" fill="none" stroke="${shade(c, 0.45)}" stroke-width="3" stroke-linecap="round" opacity=".8"/>`;
|
||||
|
||||
export const HAIR = {
|
||||
none: () => ({ back: '', front: '' }),
|
||||
long: (c) => ({
|
||||
back: `<path d="M-40 34 C-55 70 -53 108 -42 124 Q0 130 42 124 C53 108 55 70 40 34 Z" fill="${c}"/>`,
|
||||
front: bangs(c) + sideLocks(c, 104) + shine(c),
|
||||
}),
|
||||
wild: (c) => ({
|
||||
back: `<path d="M-40 30 L-54 48 L-47 60 L-56 78 L-47 88 L-52 108 L-36 104 L-30 118 L-14 110 L0 120 L14 110 L30 118 L36 104 L52 108 L47 88 L56 78 L47 60 L54 48 L40 30 Z" fill="${c}"/>`,
|
||||
front: bangs(c, { tips: 7, jag: true, tipY: 44 }) + sideLocks(c, 98),
|
||||
}),
|
||||
twintails: (c, acc) => ({
|
||||
back: `<path d="M-36 28 C-74 34 -76 84 -58 118 Q-54 100 -48 92 Q-46 108 -40 114 C-44 80 -46 58 -30 44 Z" fill="${c}"/>` +
|
||||
`<path d="M36 28 C74 34 76 84 58 118 Q54 100 48 92 Q46 108 40 114 C44 80 46 58 30 44 Z" fill="${c}"/>` +
|
||||
`<path d="M-40 40 C-50 64 -48 86 -40 92 L40 92 C48 86 50 64 40 40 Z" fill="${c}"/>`,
|
||||
front: bangs(c, { tips: 7 }) + sideLocks(c, 94) + shine(c) +
|
||||
`<circle cx="-36" cy="30" r="5" fill="${acc || '#ff5a6e'}"/><circle cx="36" cy="30" r="5" fill="${acc || '#ff5a6e'}"/>`,
|
||||
}),
|
||||
bob: (c) => ({
|
||||
back: `<path d="M-42 40 C-54 66 -52 90 -42 98 L42 98 C52 90 54 66 42 40 Z" fill="${c}"/>`,
|
||||
front: bangs(c, { tips: 5 }) + sideLocks(c, 94) + shine(c),
|
||||
}),
|
||||
messy: (c) => ({
|
||||
back: `<path d="M-38 24 L-54 30 L-46 42 L-58 56 L-44 62 L-50 78 L-38 76 L38 76 L50 78 L44 62 L58 56 L46 42 L54 30 L38 24 Z" fill="${c}"/>`,
|
||||
front: bangs(c, { tips: 5, jag: true, tipY: 48, rootY: 32 }) +
|
||||
`<path d="M-6 12 L-2 -2 L6 12 L16 2 L16 16 Z" fill="${c}"/>` + shine(c),
|
||||
}),
|
||||
short: (c) => ({
|
||||
back: '',
|
||||
front: bangs(c, { tips: 5, tipY: 40, rootY: 30, sideY: 56 }) + shine(c),
|
||||
}),
|
||||
curly: (c) => {
|
||||
let back = '';
|
||||
for (let a = 150; a <= 390; a += 20) {
|
||||
const r = (a * Math.PI) / 180;
|
||||
back += `<circle cx="${n(Math.cos(r) * 44)}" cy="${n(52 + Math.sin(r) * 40)}" r="13" fill="${c}"/>`;
|
||||
}
|
||||
back += `<circle cx="-46" cy="84" r="12" fill="${c}"/><circle cx="46" cy="84" r="12" fill="${c}"/>`;
|
||||
let front = '';
|
||||
for (const [x, y, r] of [[-30, 30, 12], [-14, 22, 13], [3, 20, 13], [19, 23, 13], [33, 32, 11], [-38, 44, 9], [40, 45, 9]]) front += `<circle cx="${x}" cy="${y}" r="${r}" fill="${c}"/>`;
|
||||
return { back, front };
|
||||
},
|
||||
braids: (c) => {
|
||||
let br = '';
|
||||
for (const sx of [-1, 1]) for (let i = 0; i < 5; i++) br += `<ellipse cx="${sx * (37 + i * 0.6)}" cy="${76 + i * 11}" rx="7" ry="7" fill="${c}"/>`;
|
||||
return {
|
||||
back: `<path d="M-42 40 C-50 60 -48 80 -42 86 L42 86 C48 80 50 60 42 40 Z" fill="${c}"/>`,
|
||||
front: `<path d="M-43 64 C-48 24 -24 9 0 9 C24 9 48 24 43 64 L36 46 Q20 30 2 26 L0 30 L-2 26 Q-20 30 -36 46 Z" fill="${c}"/>` + br + shine(c),
|
||||
};
|
||||
},
|
||||
bun: (c) => ({
|
||||
back: `<circle cx="10" cy="6" r="15" fill="${c}"/><path d="M-42 40 C-50 60 -48 76 -42 80 L42 80 C48 76 50 60 42 40 Z" fill="${c}"/>`,
|
||||
front: bangs(c, { tips: 6, tipY: 44 }) + sideLocks(c, 90) + shine(c),
|
||||
}),
|
||||
crest: (c) => ({
|
||||
back: '',
|
||||
front: `<path d="M-6 20 Q-12 2 -2 -2 Q0 8 2 16 Q6 0 16 2 Q10 12 6 22 Z" fill="${c}"/>`,
|
||||
}),
|
||||
mane: (c) => ({
|
||||
back: `<path d="M8 14 Q46 10 50 50 Q56 80 44 108 Q34 86 34 60 Z" fill="${c}"/>`,
|
||||
front: `<path d="M-20 26 Q-14 6 4 8 Q20 6 22 24 Q12 18 8 34 Q2 22 -6 36 Q-10 24 -20 26 Z" fill="${c}"/>`,
|
||||
}),
|
||||
helmet: (c, acc) => ({
|
||||
back: `<ellipse cx="0" cy="50" rx="47" ry="43" fill="${c}"/>`,
|
||||
front: `<path d="M-46 52 A47 43 0 0 1 46 52 L40 50 A40 36 0 0 0 -40 50 Z" fill="${shade(c, 0.25)}"/>` +
|
||||
`<circle cx="-45" cy="58" r="9" fill="${c}"/><circle cx="45" cy="58" r="9" fill="${c}"/>` +
|
||||
`<circle cx="-45" cy="58" r="4" fill="${acc}"/><circle cx="45" cy="58" r="4" fill="${acc}"/>` +
|
||||
`<path d="M30 18 L42 -2" fill="none"/><circle cx="42" cy="-3" r="4" fill="${acc}"/>`,
|
||||
}),
|
||||
};
|
||||
|
||||
// ---------- accessories ----------
|
||||
// Each returns {back?, mid?, front?}: back = behind head, mid = between head skin and face, front = over hair.
|
||||
export const ACC = {
|
||||
bow: (c) => ({ front: `<path d="M24 20 L12 10 L13 30 Z M24 20 L36 8 L37 28 Z" fill="${c}"/><circle cx="24" cy="20" r="4.5" fill="${shade(c, -0.15)}"/>` }),
|
||||
glasses: (c) => ({ front: `<g fill="#ffffff" fill-opacity=".18" stroke="${c}" stroke-width="2.6"><circle cx="-15" cy="61" r="10.5"/><circle cx="15" cy="61" r="10.5"/></g><path d="M-4.5 59 Q0 56 4.5 59" fill="none" stroke="${c}" stroke-width="2.6"/>` }),
|
||||
headphones: (c, c2) => ({
|
||||
back: `<path d="M-42 50 Q-44 2 0 2 Q44 2 42 50" fill="none" stroke="${INK}" stroke-width="9"/><path d="M-42 50 Q-44 2 0 2 Q44 2 42 50" fill="none" stroke="${c}" stroke-width="4"/>`,
|
||||
front: `<rect x="-50" y="48" width="13" height="20" rx="6" fill="${c}"/><rect x="37" y="48" width="13" height="20" rx="6" fill="${c}"/>` +
|
||||
(c2 ? `<circle cx="-43.5" cy="58" r="3" fill="${c2}"/><circle cx="43.5" cy="58" r="3" fill="${c2}"/>` : ''),
|
||||
}),
|
||||
foxEars: (c, inner) => ({
|
||||
back: `<path d="M-38 34 L-34 -4 L-12 18 Z M38 34 L34 -4 L12 18 Z" fill="${c}"/>`,
|
||||
front: `<path d="M-33 22 L-31 4 L-21 15 Z M33 22 L31 4 L21 15 Z" fill="${inner}" stroke="none"/>`,
|
||||
}),
|
||||
foxMask: (c, mark) => ({
|
||||
front: `<g transform="rotate(-24 -26 16)"><path d="M-40 14 Q-26 0 -12 14 Q-14 28 -26 30 Q-38 28 -40 14 Z" fill="${c}"/>` +
|
||||
`<path d="M-38 10 L-36 -2 L-30 7 M-14 10 L-16 -2 L-22 7" fill="${c}"/>` +
|
||||
`<path d="M-33 15 L-29 13 M-23 13 L-19 15" stroke="${mark}" stroke-width="2.4"/><circle cx="-26" cy="24" r="2" fill="${mark}" stroke="none"/></g>`,
|
||||
}),
|
||||
tail: (c, tip) => ({ body: `<path d="M18 128 Q58 128 56 92 Q54 74 44 80 Q50 96 36 110 Q24 118 14 116 Z" fill="${c}"/>` + (tip ? `<path d="M56 92 Q54 74 44 80 Q48 86 47 92 Z" fill="${tip}"/>` : '') }),
|
||||
horseTail: (c) => ({ body: `<path d="M14 124 Q40 118 46 140 Q48 152 38 154 Q40 140 26 134 Q20 132 14 132 Z" fill="${c}"/>` }),
|
||||
earring: (c) => ({ front: `<circle cx="-40" cy="74" r="3.6" fill="${c}"/><path d="M-40 77 L-40 84" stroke="${c}" stroke-width="2.4"/>` }),
|
||||
circlet: (c, gem) => ({ front: `<path d="M-38 40 Q0 26 38 40" fill="none" stroke="${INK}" stroke-width="7"/><path d="M-38 40 Q0 26 38 40" fill="none" stroke="${c}" stroke-width="3"/><path d="M0 26 L5 33 L0 40 L-5 33 Z" fill="${gem}"/>` }),
|
||||
cubes: (c, glow) => ({
|
||||
back: `<path d="M-40 40 Q-46 10 -20 2 L20 2 Q46 10 40 40 Z" fill="${shade(c, -0.2)}"/>`,
|
||||
front: [[-26, 10, 9], [-8, 2, 10], [12, 4, 9], [28, 14, 8], [2, -14, 8], [-17, -8, 7], [20, -8, 6]]
|
||||
.map(([x, y, s]) => `<rect x="${x - s / 2}" y="${y - s / 2}" width="${s}" height="${s}" rx="1.5" fill="${c}"/><rect x="${x - s / 4}" y="${y - s / 4}" width="${s / 2}" height="${s / 2}" fill="${glow}" stroke="none"/>`).join(''),
|
||||
}),
|
||||
crystals: (c) => ({ front: `<path d="M-30 22 L-34 -4 L-18 14 Z M-12 12 L-8 -16 L2 10 Z M8 10 L20 -10 L22 14 Z M24 18 L40 4 L36 26 Z" fill="${c}"/><path d="M-9 8 L-8 -8 M18 6 L20 -4" stroke="#ffffff" stroke-width="2" opacity=".8"/>` }),
|
||||
beard: (c) => ({ mid: `<path d="M-38 62 Q-40 96 -16 108 Q0 118 16 108 Q40 96 38 62 Q30 84 14 82 Q0 88 -14 82 Q-30 84 -38 62 Z" fill="${c}"/>` }),
|
||||
goatee: (c) => ({ mid: `<path d="M-18 82 Q0 90 18 82 Q16 100 0 110 Q-16 100 -18 82 Z" fill="${c}"/>` }),
|
||||
longNose: (c) => ({ front: `<path d="M-4 61 Q2 56 7 61 Q15 67 17 73 Q15 77 11 75 Q2 70 -4 66 Z" fill="${c}"/>` }),
|
||||
tokin: (c) => ({ front: `<path d="M-6 16 L-4 2 Q2 -4 8 2 L10 16 Z" fill="${c}"/><path d="M-12 18 Q2 12 16 18" fill="none" stroke="${INK}" stroke-width="3"/>` }),
|
||||
ridges: (c) => ({ mid: `<path d="M-6 20 Q0 15 6 20 L5 42 Q0 46 -5 42 Z M-24 26 Q-18 22 -14 28 L-16 44 Q-21 44 -23 40 Z M24 26 Q18 22 14 28 L16 44 Q21 44 23 40 Z" fill="${c}"/>` }),
|
||||
forehead: (c) => ({ front: `<circle cx="0" cy="38" r="4.5" fill="${c}"/>` }),
|
||||
hood: (c) => ({ back: `<path d="M-50 70 Q-58 4 0 2 Q58 4 50 70 Q52 96 30 98 L-30 98 Q-52 96 -50 70 Z" fill="${c}"/>` }),
|
||||
hoodRim: (c) => ({ front: `<path d="M-48 70 Q-54 6 0 4 Q54 6 48 70 L42 68 Q46 16 0 14 Q-46 16 -42 68 Z" fill="${c}"/>` }),
|
||||
wingCap: (c, wing, lens) => ({
|
||||
front: `<path d="M-40 34 Q-38 6 0 6 Q38 6 40 34 Q0 22 -40 34 Z" fill="${c}"/>` +
|
||||
`<path d="M30 20 Q46 2 58 4 Q50 10 54 12 Q44 16 46 20 Q40 22 36 26 Z" fill="${wing}"/>` +
|
||||
`<rect x="-26" y="22" width="20" height="13" rx="6" fill="${lens}"/><rect x="6" y="22" width="20" height="13" rx="6" fill="${lens}"/><path d="M-6 28 L6 28" fill="none"/>`,
|
||||
}),
|
||||
scarf: (c) => ({ body: `<path d="M-18 86 Q0 96 18 86 L20 94 Q0 104 -20 94 Z M8 94 L16 116 L6 114 L2 98 Z" fill="${c}"/>` }),
|
||||
spire: (c, glow) => ({
|
||||
back: `<path d="M-26 22 L0 -14 L26 22 Z" fill="${c}"/>`,
|
||||
front: `<path d="M-8 18 L0 -6 L8 18 Z" fill="${glow}"/><path d="M-30 30 Q0 14 30 30" fill="none" stroke="${glow}" stroke-width="3"/>`,
|
||||
}),
|
||||
spiralBand: (c, metal) => ({ front: `<path d="M-40 38 Q0 24 40 38 L40 46 Q0 32 -40 46 Z" fill="${c}"/><rect x="-9" y="27" width="18" height="12" rx="2" fill="${metal}"/><path d="M0 33 m-3 0 a3 3 0 1 1 3 3 a5 5 0 1 1 5 -5" fill="none" stroke="${INK}" stroke-width="1.6"/>` }),
|
||||
helm: (c, horn, eye) => ({
|
||||
front: `<path d="M-42 40 Q-40 2 0 0 Q40 2 42 40 Q0 28 -42 40 Z" fill="${c}"/>` +
|
||||
`<path d="M-38 22 Q-60 14 -58 -10 Q-48 6 -34 10 Z M38 22 Q60 14 58 -10 Q48 6 34 10 Z" fill="${horn}"/>` +
|
||||
`<circle cx="0" cy="18" r="7" fill="#fff"/><circle cx="0" cy="18" r="3.6" fill="${eye}" stroke="none"/>`,
|
||||
}),
|
||||
goggles: (c, lens) => ({ front: `<path d="M-42 34 Q0 20 42 34" fill="none" stroke="${INK}" stroke-width="7"/><path d="M-42 34 Q0 20 42 34" fill="none" stroke="${c}" stroke-width="3"/><rect x="-24" y="18" width="18" height="13" rx="6" fill="${lens}"/><rect x="6" y="18" width="18" height="13" rx="6" fill="${lens}"/>` }),
|
||||
pencil: () => ({ front: `<g transform="rotate(-35 18 4)"><rect x="4" y="1" width="28" height="6" rx="1" fill="#ffd84a"/><path d="M32 1 L39 4 L32 7 Z" fill="#f3d9b1"/><rect x="2" y="1" width="5" height="6" fill="#ff8aa0"/></g>` }),
|
||||
starClip: (c) => ({ front: sparkle(-28, 22, 7, c) }),
|
||||
twoTone: (c) => ({ front: `<path d="M2 10 C24 10 46 24 43 64 L36 46 L30 38 L22 46 L12 34 L4 40 Z" fill="${c}" opacity=".95"/>` }),
|
||||
feather: (c) => ({ front: `<path d="M-30 18 Q-44 0 -36 -8 Q-28 2 -26 14 Z" fill="${c}"/><path d="M-30 14 L-36 -4" stroke="${INK}" stroke-width="1.5"/>` }),
|
||||
collarRing: (c, gem) => ({ body: `<path d="M-17 88 Q0 98 17 88 L18 94 Q0 104 -18 94 Z" fill="${c}"/><rect x="-4" y="93" width="8" height="6" rx="2" fill="${gem}"/>` }),
|
||||
mask: (c) => ({ mid: `<path d="M-38 66 Q0 74 38 66 Q36 92 0 98 Q-36 92 -38 66 Z" fill="${c}"/>` }),
|
||||
};
|
||||
|
||||
// ---------- props ----------
|
||||
// Props are drawn around a grip point at (0,0) and extend mostly upward (-y), ~26 px.
|
||||
export const PROPS = {
|
||||
book: (c) => `<path d="M-13 -20 L13 -20 L13 6 L-13 6 Z" fill="${c}"/><path d="M-10 -20 L-10 6" fill="none"/><path d="M-4 -12 L8 -12 M-4 -6 L6 -6" stroke="${shade(c, 0.6)}" stroke-width="2"/><path d="M13 -17 L15 -17 L15 4 L13 6" fill="#f3e6cf"/>`,
|
||||
tablet: (c, g) => `<rect x="-11" y="-24" width="22" height="30" rx="3" fill="${c}"/><rect x="-7" y="-20" width="14" height="20" rx="1.5" fill="${g}" stroke="none"/><path d="M-4 -15 L4 -15 M-4 -10 L2 -10 M-4 -5 L4 -5" stroke="${shade(c, -0.3)}" stroke-width="1.6"/>`,
|
||||
magnifier: (c) => `<path d="M0 4 L6 -10" stroke="${INK}" stroke-width="8"/><path d="M0 4 L6 -10" stroke="${c}" stroke-width="4"/><circle cx="10" cy="-20" r="11" fill="#cfefff"/><circle cx="10" cy="-20" r="11" fill="none" stroke="${c}" stroke-width="3.5"/><path d="M5 -24 Q8 -28 12 -27" stroke="#fff" stroke-width="2.4"/>`,
|
||||
cards: (c) => `<g transform="rotate(-14)"><rect x="-14" y="-22" width="15" height="21" rx="2" fill="${c}"/><path d="M-10 -15 q3 -4 6 0 q-3 5 -6 9" fill="none" stroke-width="2"/></g><g transform="rotate(10)"><rect x="-2" y="-24" width="15" height="21" rx="2" fill="${c}"/><path d="M2 -17 h7 M5 -17 v9" fill="none" stroke-width="2"/></g>`,
|
||||
keys: (c) => `<circle cx="0" cy="-10" r="8" fill="none" stroke="${INK}" stroke-width="7"/><circle cx="0" cy="-10" r="8" fill="none" stroke="${c}" stroke-width="3"/><circle cx="-6" cy="-20" r="5" fill="${c}"/><rect x="-7.5" y="-22" width="3" height="3" fill="${INK}" stroke="none"/><path d="M8 -6 L18 4 L15 7 M14 0 L11 3" fill="none" stroke-width="6" stroke="${INK}"/><path d="M8 -6 L18 4 L15 7 M14 0 L11 3" fill="none" stroke-width="2.6" stroke="${c}"/>`,
|
||||
hammer: (c, h) => `<path d="M0 6 L0 -18" stroke="${INK}" stroke-width="8"/><path d="M0 6 L0 -18" stroke="${h}" stroke-width="4"/><rect x="-13" y="-28" width="26" height="13" rx="3" fill="${c}"/><path d="M-9 -24 L-9 -19" stroke="${shade(c, 0.5)}" stroke-width="2"/>`,
|
||||
horn: (c, band) => `<path d="M-2 6 Q-14 -10 0 -26 L18 -16 Q2 -8 4 6 Z" fill="${c}"/><ellipse cx="9" cy="-21" rx="10.5" ry="5.5" transform="rotate(30 9 -21)" fill="${shade(c, -0.2)}"/><ellipse cx="9" cy="-21" rx="6" ry="2.6" transform="rotate(30 9 -21)" fill="${INK}"/><path d="M-6 -4 Q0 -8 5 -4" fill="none" stroke="${band}" stroke-width="3.4"/><path d="M-6 -4 Q0 -8 5 -4" fill="none" stroke-width="0"/>`,
|
||||
fan: (c) => `<path d="M0 4 L0 -6" stroke="${INK}" stroke-width="6"/><path d="M0 -4 Q-20 -14 -16 -30 Q0 -36 16 -30 Q20 -14 0 -4 Z" fill="${c}"/><path d="M0 -6 L-8 -28 M0 -6 L0 -32 M0 -6 L8 -28" stroke="${shade(c, -0.3)}" stroke-width="1.6"/>`,
|
||||
brush: (c) => `<path d="M0 6 L0 -16" stroke="${INK}" stroke-width="8"/><path d="M0 6 L0 -16" stroke="${c}" stroke-width="4"/><path d="M-5 -16 L5 -16 Q6 -26 0 -34 Q-6 -26 -5 -16 Z" fill="#1a1a1a"/><circle cx="12" cy="-28" r="3" fill="#1a1a1a" stroke="none"/><circle cx="16" cy="-20" r="2" fill="#1a1a1a" stroke="none"/>`,
|
||||
quill: (c) => `<path d="M-2 6 Q-2 -14 14 -32 Q12 -14 0 4 Z" fill="${c}"/><path d="M-2 6 L8 -18" stroke="${INK}" stroke-width="1.6"/><circle cx="-4" cy="9" r="2.4" fill="#3a2a6b" stroke="none"/>`,
|
||||
heart: (c) => `<path d="M0 0 L-12 -12 L-12 -20 L-8 -24 L-4 -24 L0 -20 L4 -24 L8 -24 L12 -20 L12 -12 Z" fill="${c}"/><rect x="-8" y="-20" width="4" height="4" fill="#fff" stroke="none"/>`,
|
||||
spool: (c, t) => `<rect x="-10" y="-24" width="20" height="5" rx="1.5" fill="${c}"/><rect x="-10" y="-4" width="20" height="5" rx="1.5" fill="${c}"/><rect x="-7" y="-19" width="14" height="15" fill="${t}"/><path d="M7 -12 Q22 -10 18 6" fill="none" stroke="${t}" stroke-width="2.4"/>`,
|
||||
envelope: (c, seal) => `<rect x="-14" y="-20" width="28" height="19" rx="2" fill="${c}"/><path d="M-14 -20 L0 -9 L14 -20" fill="none"/><circle cx="0" cy="-9" r="4" fill="${seal}"/>`,
|
||||
wand: (c, star) => `<path d="M0 6 L0 -14" stroke="${INK}" stroke-width="7"/><path d="M0 6 L0 -14" stroke="${c}" stroke-width="3"/>${sparkle(0, -24, 12, star)}<circle cx="0" cy="-24" r="2.5" fill="#fff" stroke="none"/>`,
|
||||
cube: (c, g) => `<path d="M0 -30 L13 -23 L13 -8 L0 -1 L-13 -8 L-13 -23 Z" fill="${c}"/><path d="M-13 -23 L0 -16 L13 -23 M0 -16 L0 -1" fill="none"/><path d="M0 -16 L13 -23 L13 -8 L0 -1 Z" fill="${g}" opacity=".55" stroke="none"/>`,
|
||||
crystal: (c) => `<path d="M0 -32 L10 -16 L6 2 L-6 2 L-10 -16 Z" fill="${c}"/><path d="M0 -32 L0 2 M-10 -16 L10 -16" fill="none" stroke-width="1.6"/><path d="M-4 -24 L-6 -14" stroke="#fff" stroke-width="2"/>`,
|
||||
ship: (c, g) => `<g transform="rotate(-30 0 -14)"><path d="M0 -32 Q7 -20 7 -6 L14 2 L-14 2 L-7 -6 Q-7 -20 0 -32 Z" fill="${c}"/><circle cx="0" cy="-16" r="4" fill="${g}"/><path d="M-4 3 L0 10 L4 3 Z" fill="#ff8a2a"/></g>`,
|
||||
shield: (c, m) => `<path d="M-13 -26 L13 -26 L13 -10 Q13 2 0 7 Q-13 2 -13 -10 Z" fill="${c}"/><path d="M4 -20 A8 8 0 1 0 6 -6 A6 6 0 1 1 4 -20 Z" fill="${m}" stroke="none"/>`,
|
||||
mirror: (c, g) => `<path d="M0 6 L0 -6" stroke="${INK}" stroke-width="7"/><path d="M0 6 L0 -6" stroke="${c}" stroke-width="3"/><ellipse cx="0" cy="-18" rx="11" ry="13" fill="${c}"/><ellipse cx="0" cy="-18" rx="7" ry="9" fill="${g}" stroke="none"/><path d="M-3 -23 L2 -14" stroke="#fff" stroke-width="2"/>`,
|
||||
orb: (c, g) => `<circle cx="0" cy="-14" r="12" fill="${c}"/><circle cx="0" cy="-14" r="6" fill="${g}" stroke="none"/><path d="M-6 -20 Q-2 -24 3 -23" stroke="#fff" stroke-width="2"/>`,
|
||||
panel: (c, g) => `<rect x="-15" y="-26" width="30" height="22" rx="3" fill="${c}"/><rect x="-11" y="-21" width="10" height="6" rx="1" fill="${g}" stroke="none"/><rect x="2" y="-21" width="9" height="13" rx="1" fill="${g}" stroke="none" opacity=".6"/><rect x="-11" y="-12" width="10" height="4" rx="1" fill="${g}" stroke="none" opacity=".6"/>`,
|
||||
camera: (c, g) => `<rect x="-14" y="-20" width="28" height="18" rx="3" fill="${c}"/><rect x="-8" y="-24" width="9" height="5" rx="1" fill="${c}"/><circle cx="2" cy="-11" r="6" fill="${g}"/><circle cx="10" cy="-16" r="1.6" fill="#ff5a6e" stroke="none"/>`,
|
||||
pad: (c, g) => `<path d="M-15 -14 Q-15 -22 -6 -22 L6 -22 Q15 -22 15 -14 L16 -2 Q16 4 10 2 L5 -4 L-5 -4 L-10 2 Q-16 4 -16 -2 Z" fill="${c}"/><circle cx="7" cy="-14" r="2.4" fill="${g}" stroke="none"/><path d="M-9 -14 h6 M-6 -17 v6" stroke="${g}" stroke-width="2"/>`,
|
||||
seed: (c, g) => `<path d="M0 -32 Q14 -20 10 -6 Q6 4 0 4 Q-6 4 -10 -6 Q-14 -20 0 -32 Z" fill="${c}"/><path d="M0 -26 Q6 -16 0 -2" fill="none" stroke="${g}" stroke-width="2.4"/><circle cx="-4" cy="-14" r="2.4" fill="${g}" stroke="none"/>`,
|
||||
scroll: (c) => `<rect x="-13" y="-22" width="26" height="22" fill="${c}"/><rect x="-16" y="-25" width="32" height="5" rx="2.5" fill="${shade(c, -0.25)}"/><rect x="-16" y="-2" width="32" height="5" rx="2.5" fill="${shade(c, -0.25)}"/><path d="M-7 -15 h14 M-7 -9 h10" stroke="${INK}" stroke-width="1.8"/>`,
|
||||
};
|
||||
|
||||
// ---------- emblems ----------
|
||||
// Chest badge glyphs, drawn inside a 6.5-radius disc at (0,104) on the torso.
|
||||
export const EMBLEMS = {
|
||||
star: (c) => sparkle(0, 0, 4.4, c),
|
||||
eye: (c) => `<path d="M-4.5 0 Q0 -4.5 4.5 0 Q0 4.5 -4.5 0 Z" fill="#fff" stroke="${INK}" stroke-width="1.4"/><circle r="1.8" fill="${c}"/>`,
|
||||
key: (c) => `<circle cx="0" cy="-2" r="2.2" fill="none" stroke="${c}" stroke-width="1.8"/><path d="M0 0 L0 4.5 M0 3 L1.8 3" stroke="${c}" stroke-width="1.8"/>`,
|
||||
spiral: (c) => `<path d="M0 0 m-1.5 0 a1.5 1.5 0 1 1 1.5 1.5 a3 3 0 1 1 3 -3" fill="none" stroke="${c}" stroke-width="1.6"/>`,
|
||||
cube: (c) => `<path d="M0 -4 L3.5 -2 L3.5 2 L0 4 L-3.5 2 L-3.5 -2 Z" fill="${c}"/>`,
|
||||
flake: (c) => `<path d="M0 -4.5 V4.5 M-3.9 -2.2 L3.9 2.2 M-3.9 2.2 L3.9 -2.2" stroke="${c}" stroke-width="1.6"/>`,
|
||||
feather: (c) => `<path d="M-2 4 Q-2 -2 3 -5 Q3 1 -2 4 Z" fill="${c}"/>`,
|
||||
heart: (c) => `<path d="M0 3.5 L-3.6 0 Q-4.5 -3.5 -1.8 -3.6 Q0 -3.4 0 -1.6 Q0 -3.4 1.8 -3.6 Q4.5 -3.5 3.6 0 Z" fill="${c}"/>`,
|
||||
bolt: (c) => `<path d="M1 -4.5 L-2.5 0.5 L0 0.5 L-1 4.5 L2.5 -0.5 L0 -0.5 Z" fill="${c}"/>`,
|
||||
moon: (c) => `<path d="M1 -4 A4 4 0 1 0 2.6 3 A3 3 0 1 1 1 -4 Z" fill="${c}"/>`,
|
||||
rune: (c) => `<path d="M-2 -4 V4 M-2 -2 L2 -4 M-2 1 L2 -1" stroke="${c}" stroke-width="1.6"/>`,
|
||||
gear: (c) => `<circle r="3.2" fill="none" stroke="${c}" stroke-width="2"/><path d="M0 -4.8 V-3 M0 4.8 V3 M-4.8 0 H-3 M4.8 0 H3" stroke="${c}" stroke-width="1.8"/>`,
|
||||
wave: (c) => `<path d="M-4 1 Q-2 -2 0 1 Q2 4 4 1" fill="none" stroke="${c}" stroke-width="1.8"/>`,
|
||||
glyph: (c) => `<path d="M-3 -3 H3 L-3 3 H3" fill="none" stroke="${c}" stroke-width="1.6"/>`,
|
||||
rainbow: () => `<path d="M-4 2 A4 4 0 0 1 4 2" fill="none" stroke="#ff5a6e" stroke-width="1.5"/><path d="M-2.5 2 A2.5 2.5 0 0 1 2.5 2" fill="none" stroke="#59b7ff" stroke-width="1.5"/>`,
|
||||
};
|
||||
|
||||
function emblem(ch) {
|
||||
if (!ch.emblem) return '';
|
||||
const e = ch.emblem;
|
||||
const y = ch.base === 'raven' ? 104 : 106;
|
||||
return `<g transform="translate(0 ${y})">` + inked(`<circle r="6.8" fill="${e.disc || '#fff6e8'}"/>`, 2.4) + EMBLEMS[e.shape](e.color) + '</g>';
|
||||
}
|
||||
|
||||
// ---------- pose effects ----------
|
||||
function fx(kind, ch) {
|
||||
const acc = ch.accent || '#ffd84a';
|
||||
switch (kind) {
|
||||
case 'sparkles': return sparkle(-62, 62, 7, acc) + sparkle(-34, 6, 5, '#fff') + sparkle(60, 70, 6, acc);
|
||||
case 'dots': return inked(`<circle cx="36" cy="16" r="3" fill="#fff"/><circle cx="46" cy="7" r="4" fill="#fff"/><circle cx="58" cy="-2" r="5.5" fill="#fff"/>`, 2.2);
|
||||
case 'bang': return inked(`<path d="M-50 18 L-44 32 M-62 30 L-50 38 M50 18 L44 32 M62 30 L50 38" fill="none"/>`, 3.2);
|
||||
case 'present': return sparkle(60, 64, 5, acc) + sparkle(28, 66, 4, '#fff');
|
||||
case 'wave': return `<path d="M60 38 Q68 46 64 56 M66 30 Q76 44 71 58" fill="none" stroke="${INK}" stroke-width="2.6" stroke-linecap="round"/>`;
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- assembly ----------
|
||||
function merge(ch) {
|
||||
const out = { back: '', mid: '', front: '' };
|
||||
for (const [name, ...args] of ch.acc || []) {
|
||||
const r = ACC[name](...args);
|
||||
for (const k of Object.keys(out)) if (r[k]) out[k] += r[k];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function propAt(ch, pose) {
|
||||
if (!pose.prop || !ch.prop) return '';
|
||||
const h = pose.arms[pose.prop.hand].h;
|
||||
const [name, ...args] = ch.prop;
|
||||
const x = h[0] + pose.prop.dx, y = h[1] + pose.prop.dy;
|
||||
return `<g transform="translate(${n(x)} ${n(y)}) rotate(${pose.prop.rot})">` + inked(PROPS[name](...args), 2.6) + '</g>';
|
||||
}
|
||||
|
||||
export function render(ch, poseName, expr) {
|
||||
const pose = POSES[poseName];
|
||||
if (!pose) throw new Error('unknown pose ' + poseName);
|
||||
const acc = merge(ch);
|
||||
const hair = HAIR[ch.hair.style](ch.hair.color, ch.hair.accent);
|
||||
const f = face(ch, expr);
|
||||
const hd = pose.head;
|
||||
const headT = `translate(${hd.dx} ${hd.dy}) rotate(${hd.rot} 0 88)`;
|
||||
|
||||
let s = '';
|
||||
s += `<g transform="translate(0 ${-pose.lift})">`;
|
||||
// 1. behind everything: hair back, accessory back (rotates with the head)
|
||||
s += `<g transform="${headT}">` + inked(acc.back + hair.back) + '</g>';
|
||||
// 2. cape, tail, legs
|
||||
s += cape(ch);
|
||||
const tails = (ch.acc || []).filter(([a]) => a === 'tail' || a === 'horseTail').map(([a, ...r]) => ACC[a](...r).body).join('');
|
||||
s += inked(tails);
|
||||
s += legs(ch, pose);
|
||||
// 3. back arm (non-front arms), torso, body accessories
|
||||
s += torso(ch);
|
||||
const bodyAcc = (ch.acc || []).filter(([a]) => a !== 'tail' && a !== 'horseTail').map(([a, ...r]) => ACC[a](...r).body || '').join('');
|
||||
s += inked(bodyAcc);
|
||||
s += emblem(ch);
|
||||
for (const side of ['L', 'R']) if (!pose.arms[side].front) s += arm(ch, pose, side);
|
||||
s += propAt(ch, pose);
|
||||
for (const side of ['L', 'R']) if (!pose.arms[side].front) s += hand(ch, pose, side);
|
||||
// 4. head
|
||||
s += `<g transform="${headT}">` + headBase(ch) + inked(acc.mid) + f.under + inked(hair.front + acc.front) + f.over + '</g>';
|
||||
// 5. front arms (thinking hand at chin)
|
||||
for (const side of ['L', 'R']) if (pose.arms[side].front) s += arm(ch, pose, side) + hand(ch, pose, side);
|
||||
s += fx(pose.fx, ch);
|
||||
s += '</g>';
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${VIEWBOX}" width="320" height="344">` +
|
||||
`<title>${ch.name} — ${poseName}, ${expr}</title>${s}</svg>\n`;
|
||||
}
|
||||
13
cast/rig/shot.sh
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env bash
|
||||
# Screenshot the gallery with Edge headless (Git Bash). Usage from the repo root:
|
||||
# cast/rig/shot.sh <out.png> [character] [pose] [width] [height]
|
||||
# A fresh profile dir per call keeps headless Edge off the desktop browser profile.
|
||||
set -euo pipefail
|
||||
out=$1; c=${2:-}; p=${3:-}; w=${4:-1400}; h=${5:-1800}
|
||||
root=$(cd "$(dirname "$0")/.." && pwd -W)
|
||||
prof="$(cygpath -m "${TMPDIR:-/tmp}")/cast-edge-$$"
|
||||
rm -f "$out"
|
||||
# Edge may hand the job to an already-running instance and return at once, so wait for the file.
|
||||
"/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" --headless=new --disable-gpu --hide-scrollbars --user-data-dir="$prof" --screenshot="$(cygpath -m "$out")" --window-size="$w,$h" "file:///$root/gallery.html?c=$c&p=$p" >/dev/null 2>&1 &
|
||||
for _ in $(seq 120); do [ -s "$out" ] && break; sleep 0.25; done
|
||||
sleep 0.5; ls -la "$out"
|
||||
1
cast/stickers/aetheria/neutral-grin.svg
Normal file
|
After Width: | Height: | Size: 6 KiB |
1
cast/stickers/aetheria/neutral-smile.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
1
cast/stickers/aetheria/neutral-surprised.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
cast/stickers/brokkr/neutral-grin.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
cast/stickers/brokkr/neutral-smile.svg
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
1
cast/stickers/brokkr/neutral-surprised.svg
Normal file
|
After Width: | Height: | Size: 5 KiB |
1
cast/stickers/druzkai/neutral-grin.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-80 -10 160 172" width="320" height="344"><title>Druzkai — neutral, grin</title><g transform="translate(0 0)"><g transform="translate(0 0) rotate(0 0 88)"><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g><path d="M-8 126 L-10 149" fill="none" stroke="#3b2618" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/><path d="M-8 126 L-10 149" fill="none" stroke="#4a2a5a" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="-12" cy="150" rx="7.5" ry="5" fill="#2a1830"/></g><path d="M8 126 L10 149" fill="none" stroke="#3b2618" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 126 L10 149" fill="none" stroke="#4a2a5a" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="12" cy="150" rx="7.5" ry="5" fill="#2a1830"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><path d="M-15 88 Q-23 112 -29 144 Q0 150 29 144 Q23 112 15 88 Z" fill="#4a2a5a"/><path d="M-28 138 Q0 144 28 138 L29 144 Q0 150 -29 144 Z" fill="#d9a43a"/><path d="M-13 88 Q0 98 13 88 Z" fill="#d9a43a"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g><g transform="translate(0 106)"><g stroke="#3b2618" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"><circle r="6.8" fill="#fff6e8"/></g><path d="M0 0 m-1.5 0 a1.5 1.5 0 1 1 1.5 1.5 a3 3 0 1 1 3 -3" fill="none" stroke="#3fd0c8" stroke-width="1.6"/></g><path d="M-15 95 Q-23 107 -24 120" fill="none" stroke="#3b2618" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/><path d="M-15 95 Q-23 107 -24 120" fill="none" stroke="#4a2a5a" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 95 Q23 107 24 120" fill="none" stroke="#3b2618" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 95 Q23 107 24 120" fill="none" stroke="#4a2a5a" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><g transform="translate(28 116) rotate(8)"><g stroke="#3b2618" stroke-width="2.6" stroke-linejoin="round" stroke-linecap="round"><path d="M0 -32 Q14 -20 10 -6 Q6 4 0 4 Q-6 4 -10 -6 Q-14 -20 0 -32 Z" fill="#6fbf6a"/><path d="M0 -26 Q6 -16 0 -2" fill="none" stroke="#c8ff9a" stroke-width="2.4"/><circle cx="-4" cy="-14" r="2.4" fill="#c8ff9a" stroke="none"/></g></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="-24" cy="120" r="5.6" fill="#c4553e"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="24" cy="120" r="5.6" fill="#c4553e"/></g><g transform="translate(0 0) rotate(0 0 88)"><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="-38" cy="62" rx="6" ry="7.5" fill="#c4553e"/><ellipse cx="38" cy="62" rx="6" ry="7.5" fill="#c4553e"/><ellipse cx="0" cy="52" rx="40" ry="36" fill="#c4553e"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><path d="M-6 20 Q0 15 6 20 L5 42 Q0 46 -5 42 Z M-24 26 Q-18 22 -14 28 L-16 44 Q-21 44 -23 40 Z M24 26 Q18 22 14 28 L16 44 Q21 44 23 40 Z" fill="#a03a2c"/></g><ellipse cx="-26" cy="71" rx="6" ry="3.7199999999999998" fill="#ff9a8a" opacity="0.85"/><ellipse cx="26" cy="71" rx="6" ry="3.7199999999999998" fill="#ff9a8a" opacity="0.85"/><ellipse cx="-15" cy="61" rx="6.3" ry="7" fill="#3fd0c8" stroke="#3b2618" stroke-width="2.5"/><ellipse cx="-15" cy="62" rx="3.528" ry="4.2" fill="#2a1a12"/><path d="M-22.3 57.85 Q-15 51.4 -7.699999999999999 57.85" fill="none" stroke="#3b2618" stroke-width="3.2" stroke-linecap="round"/><circle cx="-12.9" cy="58.2" r="2.3" fill="#fff"/><circle cx="-17" cy="64" r="1" fill="#fff"/><ellipse cx="15" cy="61" rx="6.3" ry="7" fill="#3fd0c8" stroke="#3b2618" stroke-width="2.5"/><ellipse cx="15" cy="62" rx="3.528" ry="4.2" fill="#2a1a12"/><path d="M7.699999999999999 57.85 Q15 51.4 22.3 57.85" fill="none" stroke="#3b2618" stroke-width="3.2" stroke-linecap="round"/><circle cx="17.1" cy="58.2" r="2.3" fill="#fff"/><circle cx="13" cy="64" r="1" fill="#fff"/><path d="M-9 74 Q0 75.5 9 74 Q8 85 0 85 Q-8 85 -9 74 Z" fill="#7a2a2a" stroke="#3b2618" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M-4.5 82.5 Q0 78.5 4.5 82.5 Q0 84.6 -4.5 82.5 Z" fill="#ff8f9a"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="0" cy="38" r="4.5" fill="#3fd0c8"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
1
cast/stickers/druzkai/neutral-smile.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-80 -10 160 172" width="320" height="344"><title>Druzkai — neutral, smile</title><g transform="translate(0 0)"><g transform="translate(0 0) rotate(0 0 88)"><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g><path d="M-8 126 L-10 149" fill="none" stroke="#3b2618" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/><path d="M-8 126 L-10 149" fill="none" stroke="#4a2a5a" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="-12" cy="150" rx="7.5" ry="5" fill="#2a1830"/></g><path d="M8 126 L10 149" fill="none" stroke="#3b2618" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 126 L10 149" fill="none" stroke="#4a2a5a" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="12" cy="150" rx="7.5" ry="5" fill="#2a1830"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><path d="M-15 88 Q-23 112 -29 144 Q0 150 29 144 Q23 112 15 88 Z" fill="#4a2a5a"/><path d="M-28 138 Q0 144 28 138 L29 144 Q0 150 -29 144 Z" fill="#d9a43a"/><path d="M-13 88 Q0 98 13 88 Z" fill="#d9a43a"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g><g transform="translate(0 106)"><g stroke="#3b2618" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"><circle r="6.8" fill="#fff6e8"/></g><path d="M0 0 m-1.5 0 a1.5 1.5 0 1 1 1.5 1.5 a3 3 0 1 1 3 -3" fill="none" stroke="#3fd0c8" stroke-width="1.6"/></g><path d="M-15 95 Q-23 107 -24 120" fill="none" stroke="#3b2618" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/><path d="M-15 95 Q-23 107 -24 120" fill="none" stroke="#4a2a5a" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 95 Q23 107 24 120" fill="none" stroke="#3b2618" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 95 Q23 107 24 120" fill="none" stroke="#4a2a5a" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><g transform="translate(28 116) rotate(8)"><g stroke="#3b2618" stroke-width="2.6" stroke-linejoin="round" stroke-linecap="round"><path d="M0 -32 Q14 -20 10 -6 Q6 4 0 4 Q-6 4 -10 -6 Q-14 -20 0 -32 Z" fill="#6fbf6a"/><path d="M0 -26 Q6 -16 0 -2" fill="none" stroke="#c8ff9a" stroke-width="2.4"/><circle cx="-4" cy="-14" r="2.4" fill="#c8ff9a" stroke="none"/></g></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="-24" cy="120" r="5.6" fill="#c4553e"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="24" cy="120" r="5.6" fill="#c4553e"/></g><g transform="translate(0 0) rotate(0 0 88)"><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="-38" cy="62" rx="6" ry="7.5" fill="#c4553e"/><ellipse cx="38" cy="62" rx="6" ry="7.5" fill="#c4553e"/><ellipse cx="0" cy="52" rx="40" ry="36" fill="#c4553e"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><path d="M-6 20 Q0 15 6 20 L5 42 Q0 46 -5 42 Z M-24 26 Q-18 22 -14 28 L-16 44 Q-21 44 -23 40 Z M24 26 Q18 22 14 28 L16 44 Q21 44 23 40 Z" fill="#a03a2c"/></g><ellipse cx="-26" cy="71" rx="5.5" ry="3.41" fill="#ff9a8a" opacity="0.7"/><ellipse cx="26" cy="71" rx="5.5" ry="3.41" fill="#ff9a8a" opacity="0.7"/><ellipse cx="-15" cy="61" rx="6.3" ry="7.6" fill="#3fd0c8" stroke="#3b2618" stroke-width="2.5"/><ellipse cx="-15" cy="62" rx="3.528" ry="4.56" fill="#2a1a12"/><path d="M-22.3 57.58 Q-15 50.8 -7.699999999999999 57.58" fill="none" stroke="#3b2618" stroke-width="3.2" stroke-linecap="round"/><circle cx="-12.9" cy="58.2" r="2.3" fill="#fff"/><circle cx="-17" cy="64" r="1" fill="#fff"/><ellipse cx="15" cy="61" rx="6.3" ry="7.6" fill="#3fd0c8" stroke="#3b2618" stroke-width="2.5"/><ellipse cx="15" cy="62" rx="3.528" ry="4.56" fill="#2a1a12"/><path d="M7.699999999999999 57.58 Q15 50.8 22.3 57.58" fill="none" stroke="#3b2618" stroke-width="3.2" stroke-linecap="round"/><circle cx="17.1" cy="58.2" r="2.3" fill="#fff"/><circle cx="13" cy="64" r="1" fill="#fff"/><path d="M-7 75 Q0 82 7 75" fill="none" stroke="#3b2618" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="0" cy="38" r="4.5" fill="#3fd0c8"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
1
cast/stickers/druzkai/neutral-surprised.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-80 -10 160 172" width="320" height="344"><title>Druzkai — neutral, surprised</title><g transform="translate(0 0)"><g transform="translate(0 0) rotate(0 0 88)"><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g><path d="M-8 126 L-10 149" fill="none" stroke="#3b2618" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/><path d="M-8 126 L-10 149" fill="none" stroke="#4a2a5a" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="-12" cy="150" rx="7.5" ry="5" fill="#2a1830"/></g><path d="M8 126 L10 149" fill="none" stroke="#3b2618" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 126 L10 149" fill="none" stroke="#4a2a5a" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="12" cy="150" rx="7.5" ry="5" fill="#2a1830"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><path d="M-15 88 Q-23 112 -29 144 Q0 150 29 144 Q23 112 15 88 Z" fill="#4a2a5a"/><path d="M-28 138 Q0 144 28 138 L29 144 Q0 150 -29 144 Z" fill="#d9a43a"/><path d="M-13 88 Q0 98 13 88 Z" fill="#d9a43a"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g><g transform="translate(0 106)"><g stroke="#3b2618" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"><circle r="6.8" fill="#fff6e8"/></g><path d="M0 0 m-1.5 0 a1.5 1.5 0 1 1 1.5 1.5 a3 3 0 1 1 3 -3" fill="none" stroke="#3fd0c8" stroke-width="1.6"/></g><path d="M-15 95 Q-23 107 -24 120" fill="none" stroke="#3b2618" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/><path d="M-15 95 Q-23 107 -24 120" fill="none" stroke="#4a2a5a" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 95 Q23 107 24 120" fill="none" stroke="#3b2618" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 95 Q23 107 24 120" fill="none" stroke="#4a2a5a" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><g transform="translate(28 116) rotate(8)"><g stroke="#3b2618" stroke-width="2.6" stroke-linejoin="round" stroke-linecap="round"><path d="M0 -32 Q14 -20 10 -6 Q6 4 0 4 Q-6 4 -10 -6 Q-14 -20 0 -32 Z" fill="#6fbf6a"/><path d="M0 -26 Q6 -16 0 -2" fill="none" stroke="#c8ff9a" stroke-width="2.4"/><circle cx="-4" cy="-14" r="2.4" fill="#c8ff9a" stroke="none"/></g></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="-24" cy="120" r="5.6" fill="#c4553e"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="24" cy="120" r="5.6" fill="#c4553e"/></g><g transform="translate(0 0) rotate(0 0 88)"><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="-38" cy="62" rx="6" ry="7.5" fill="#c4553e"/><ellipse cx="38" cy="62" rx="6" ry="7.5" fill="#c4553e"/><ellipse cx="0" cy="52" rx="40" ry="36" fill="#c4553e"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><path d="M-6 20 Q0 15 6 20 L5 42 Q0 46 -5 42 Z M-24 26 Q-18 22 -14 28 L-16 44 Q-21 44 -23 40 Z M24 26 Q18 22 14 28 L16 44 Q21 44 23 40 Z" fill="#a03a2c"/></g><ellipse cx="-26" cy="71" rx="5.5" ry="3.41" fill="#ff9a8a" opacity="0.6"/><ellipse cx="26" cy="71" rx="5.5" ry="3.41" fill="#ff9a8a" opacity="0.6"/><circle cx="-15" cy="61" r="7.3" fill="#fff" stroke="#3b2618" stroke-width="2.5"/><circle cx="-15" cy="61.5" r="3.3" fill="#2a1a12"/><circle cx="-13.8" cy="60.2" r="1.2" fill="#fff"/><circle cx="15" cy="61" r="7.3" fill="#fff" stroke="#3b2618" stroke-width="2.5"/><circle cx="15" cy="61.5" r="3.3" fill="#2a1a12"/><circle cx="16.2" cy="60.2" r="1.2" fill="#fff"/><ellipse cx="0" cy="78" rx="4.2" ry="5.2" fill="#7a2a2a" stroke="#3b2618" stroke-width="2.6"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="0" cy="38" r="4.5" fill="#3fd0c8"/></g><path d="M-22 42 Q-15.5 40.5 -9 42" fill="none" stroke="#3b2618" stroke-width="2.6" stroke-linecap="round"/><path d="M9 42 Q15.5 40.5 22 42" fill="none" stroke="#3b2618" stroke-width="2.6" stroke-linecap="round"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
1
cast/stickers/epiphany/cheer-determined.svg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
1
cast/stickers/epiphany/cheer-grin.svg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
1
cast/stickers/epiphany/cheer-happy.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
cast/stickers/epiphany/cheer-neutral.svg
Normal file
|
After Width: | Height: | Size: 6 KiB |
1
cast/stickers/epiphany/cheer-sleepy.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
1
cast/stickers/epiphany/cheer-smile.svg
Normal file
|
After Width: | Height: | Size: 6 KiB |
1
cast/stickers/epiphany/cheer-surprised.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
1
cast/stickers/epiphany/cheer-worried.svg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
1
cast/stickers/epiphany/neutral-determined.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
1
cast/stickers/epiphany/neutral-grin.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
cast/stickers/epiphany/neutral-happy.svg
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
1
cast/stickers/epiphany/neutral-neutral.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
cast/stickers/epiphany/neutral-sleepy.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
cast/stickers/epiphany/neutral-smile.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
cast/stickers/epiphany/neutral-surprised.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
cast/stickers/epiphany/neutral-worried.svg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
1
cast/stickers/epiphany/presenting-determined.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
1
cast/stickers/epiphany/presenting-grin.svg
Normal file
|
After Width: | Height: | Size: 6 KiB |
1
cast/stickers/epiphany/presenting-happy.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
cast/stickers/epiphany/presenting-neutral.svg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
1
cast/stickers/epiphany/presenting-sleepy.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
cast/stickers/epiphany/presenting-smile.svg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
1
cast/stickers/epiphany/presenting-surprised.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
cast/stickers/epiphany/presenting-worried.svg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
1
cast/stickers/epiphany/surprised-determined.svg
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
1
cast/stickers/epiphany/surprised-grin.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
cast/stickers/epiphany/surprised-happy.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-80 -10 160 172" width="320" height="344"><title>Epiphany — surprised, happy</title><g transform="translate(0 -7)"><g transform="translate(0 -1) rotate(-3 0 88)"><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><path d="M-36 28 C-74 34 -76 84 -58 118 Q-54 100 -48 92 Q-46 108 -40 114 C-44 80 -46 58 -30 44 Z" fill="#8fd8b4"/><path d="M36 28 C74 34 76 84 58 118 Q54 100 48 92 Q46 108 40 114 C44 80 46 58 30 44 Z" fill="#8fd8b4"/><path d="M-40 40 C-50 64 -48 86 -40 92 L40 92 C48 86 50 64 40 40 Z" fill="#8fd8b4"/></g></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g><path d="M-8 126 L-11 149" fill="none" stroke="#3b2618" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/><path d="M-8 126 L-11 149" fill="none" stroke="#2c2c34" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="-13" cy="150" rx="7.5" ry="5" fill="#4a3424"/></g><path d="M8 126 L15 143" fill="none" stroke="#3b2618" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 126 L15 143" fill="none" stroke="#2c2c34" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="17" cy="144" rx="7.5" ry="5" fill="#4a3424"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><path d="M-16 88 Q-21 104 -26 136 Q0 141 26 136 Q21 104 16 88 Z" fill="#f4efe6"/><path d="M0 98 L0 138" fill="none" stroke-width="2.5"/><path d="M-25 130 Q0 135 25 130 L26 136 Q0 141 -26 136 Z" fill="#e8384f"/><path d="M-12 88 L0 101 L12 88 Z" fill="#e8384f"/><path d="M-19 110 Q0 114 19 110 L19.6 115 Q0 119 -19.6 115 Z" fill="#3b3b3b"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g><g transform="translate(0 106)"><g stroke="#3b2618" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"><circle r="6.8" fill="#fff6e8"/></g><path d="M1 -4.5 L-2.5 0.5 L0 0.5 L-1 4.5 L2.5 -0.5 L0 -0.5 Z" fill="#e8384f"/></g><path d="M-15 95 Q-34 102 -51 88" fill="none" stroke="#3b2618" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/><path d="M-15 95 Q-34 102 -51 88" fill="none" stroke="#f4efe6" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 95 Q34 102 51 88" fill="none" stroke="#3b2618" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 95 Q34 102 51 88" fill="none" stroke="#f4efe6" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="-51" cy="88" r="5.6" fill="#ffe0c8"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><circle cx="51" cy="88" r="5.6" fill="#ffe0c8"/></g><g transform="translate(0 -1) rotate(-3 0 88)"><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><ellipse cx="-38" cy="62" rx="6" ry="7.5" fill="#ffe0c8"/><ellipse cx="38" cy="62" rx="6" ry="7.5" fill="#ffe0c8"/><ellipse cx="0" cy="52" rx="40" ry="36" fill="#ffe0c8"/></g><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"></g><ellipse cx="-26" cy="71" rx="6.5" ry="4.03" fill="#ff9a8a" opacity="0.9"/><ellipse cx="26" cy="71" rx="6.5" ry="4.03" fill="#ff9a8a" opacity="0.9"/><path d="M-21 63 Q-15 54 -9 63" fill="none" stroke="#3b2618" stroke-width="3.4" stroke-linecap="round"/><path d="M9 63 Q15 54 21 63" fill="none" stroke="#3b2618" stroke-width="3.4" stroke-linecap="round"/><path d="M-7 74.5 Q0 75.5 7 74.5 Q5 83 0 83 Q-5 83 -7 74.5 Z" fill="#7a2a2a" stroke="#3b2618" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M-3.5 81 Q0 78 3.5 81 Q0 82.6 -3.5 81 Z" fill="#ff8f9a"/><g stroke="#3b2618" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"><path d="M-43 64 C-47 24 -24 9 0 9 C24 9 47 24 43 64 L41 56 Q34 40 35.1 38.3 L29.3 51.1 Q22.3 40 23.4 36.6 L17.6 47.8 Q10.5 40 11.7 34.9 L5.9 46.2 Q-1.2 40 0 34.9 L-5.9 46.2 Q-12.9 40 -11.7 36.6 L-17.6 47.8 Q-24.6 40 -23.4 38.3 L-29.3 51.1 Q-36.3 40 -35.1 40 L-41 56 Z" fill="#8fd8b4"/><path d="M-43 50 Q-49 69 -40 94 Q-35 72 -34 58 Z" fill="#8fd8b4"/><path d="M43 50 Q49 69 40 94 Q35 72 34 58 Z" fill="#8fd8b4"/><path d="M-20 22 Q-8 15 4 17" fill="none" stroke="#c1ead6" stroke-width="3" stroke-linecap="round" opacity=".8"/><circle cx="-36" cy="30" r="5" fill="#e8384f"/><circle cx="36" cy="30" r="5" fill="#e8384f"/><path d="M24 20 L12 10 L13 30 Z M24 20 L36 8 L37 28 Z" fill="#e8384f"/><circle cx="24" cy="20" r="4.5" fill="#c53043"/></g></g><g stroke="#3b2618" stroke-width="3.2" stroke-linejoin="round" stroke-linecap="round"><path d="M-50 18 L-44 32 M-62 30 L-50 38 M50 18 L44 32 M62 30 L50 38" fill="none"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
1
cast/stickers/epiphany/surprised-neutral.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
1
cast/stickers/epiphany/surprised-sleepy.svg
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
1
cast/stickers/epiphany/surprised-smile.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
1
cast/stickers/epiphany/surprised-surprised.svg
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
1
cast/stickers/epiphany/surprised-worried.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
cast/stickers/epiphany/thinking-determined.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
cast/stickers/epiphany/thinking-grin.svg
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
1
cast/stickers/epiphany/thinking-happy.svg
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
1
cast/stickers/epiphany/thinking-neutral.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
cast/stickers/epiphany/thinking-sleepy.svg
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
1
cast/stickers/epiphany/thinking-smile.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
1
cast/stickers/epiphany/thinking-surprised.svg
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
1
cast/stickers/epiphany/thinking-worried.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
cast/stickers/epiphany/waving-determined.svg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
1
cast/stickers/epiphany/waving-grin.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
1
cast/stickers/epiphany/waving-happy.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
1
cast/stickers/epiphany/waving-neutral.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
cast/stickers/epiphany/waving-sleepy.svg
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
1
cast/stickers/epiphany/waving-smile.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
cast/stickers/epiphany/waving-surprised.svg
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
1
cast/stickers/epiphany/waving-worried.svg
Normal file
|
After Width: | Height: | Size: 6 KiB |
1
cast/stickers/eureka/cheer-determined.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
1
cast/stickers/eureka/cheer-grin.svg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
1
cast/stickers/eureka/cheer-happy.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
1
cast/stickers/eureka/cheer-neutral.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
1
cast/stickers/eureka/cheer-sleepy.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
1
cast/stickers/eureka/cheer-smile.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
1
cast/stickers/eureka/cheer-surprised.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
1
cast/stickers/eureka/cheer-worried.svg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
1
cast/stickers/eureka/neutral-determined.svg
Normal file
|
After Width: | Height: | Size: 6 KiB |
1
cast/stickers/eureka/neutral-grin.svg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
1
cast/stickers/eureka/neutral-happy.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
cast/stickers/eureka/neutral-neutral.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
1
cast/stickers/eureka/neutral-sleepy.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
cast/stickers/eureka/neutral-smile.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
1
cast/stickers/eureka/neutral-surprised.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
cast/stickers/eureka/neutral-worried.svg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
1
cast/stickers/eureka/presenting-determined.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
1
cast/stickers/eureka/presenting-grin.svg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
1
cast/stickers/eureka/presenting-happy.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
cast/stickers/eureka/presenting-neutral.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
1
cast/stickers/eureka/presenting-sleepy.svg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
1
cast/stickers/eureka/presenting-smile.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
1
cast/stickers/eureka/presenting-surprised.svg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
1
cast/stickers/eureka/presenting-worried.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
1
cast/stickers/eureka/surprised-determined.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
cast/stickers/eureka/surprised-grin.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
cast/stickers/eureka/surprised-happy.svg
Normal file
|
After Width: | Height: | Size: 5 KiB |
1
cast/stickers/eureka/surprised-neutral.svg
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
1
cast/stickers/eureka/surprised-sleepy.svg
Normal file
|
After Width: | Height: | Size: 5.2 KiB |