diff --git a/examples/avatar-prototype/skull-outline.pxl b/examples/avatar-prototype/skull-outline.pxl deleted file mode 100644 index 96afd2d8..00000000 --- a/examples/avatar-prototype/skull-outline.pxl +++ /dev/null @@ -1,397 +0,0 @@ -// ============================================================================ -// SKULL OUTLINE - 3/4 View Silhouette Study -// ============================================================================ -// Reference: reference-skull.jpg -// Goal: Nail the silhouette before adding internal detail -// Canvas: 32x40 -// ============================================================================ - -{ - "type": "palette", - "name": "outline_pal", - "colors": { - "_": "transparent", - "outline": "#302828", - "fill": "#e8e0d8", - "fill2": "#e8e0d8", - "fill3": "#e8e0d8", - "fill4": "#e8e0d8", - "fill5": "#e8e0d8", - "fill6": "#e8e0d8" - } -} - -// V1: Build from simple shapes -{ - "type": "sprite", - "name": "skull_outline_v1", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { - "union": [ - { "ellipse": [14, 12, 13, 11] }, - { "rect": [5, 18, 22, 20] } - ], - "z": 0 - } - } -} - -// V2: Capture 3/4 view asymmetry -{ - "type": "sprite", - "name": "skull_outline_v2", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { - "union": [ - { "ellipse": [13, 11, 12, 10] }, - { "ellipse": [6, 13, 5, 8] }, - { "ellipse": [24, 20, 5, 6] }, - { "rect": [6, 18, 20, 8] }, - { "rect": [8, 26, 16, 8] }, - { "ellipse": [10, 34, 4, 5] }, - { "ellipse": [22, 34, 4, 5] } - ], - "z": 0 - } - } -} - -// V3: Refined proportions (has gap bug) -{ - "type": "sprite", - "name": "skull_outline_v3", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { - "union": [ - { "ellipse": [14, 10, 13, 10] }, - { "ellipse": [5, 12, 4, 6] }, - { "rect": [5, 16, 22, 10] }, - { "ellipse": [25, 22, 4, 4] }, - { "ellipse": [15, 32, 9, 7] } - ], - "z": 0 - } - } -} - -// V4: Simpler approach - just 3 main shapes with guaranteed overlap -{ - "type": "sprite", - "name": "skull_outline_v4", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - // Cranium - "fill": { "ellipse": [14, 11, 13, 11], "z": 0 }, - // Mid-face + jaw as one tall rect (will trim later) - "fill2": { "rect": [6, 18, 20, 20], "z": 0 } - } -} - -// V5: Separate regions - same z (has overlap bug) -{ - "type": "sprite", - "name": "skull_outline_v5", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 11, 13, 11], "z": 0 }, - "fill3": { "ellipse": [6, 14, 5, 7], "z": 0 }, - "fill4": { "rect": [6, 18, 21, 12], "z": 0 }, - "fill5": { "ellipse": [25, 23, 4, 5], "z": 0 }, - "fill6": { "ellipse": [16, 34, 8, 5], "z": 0 } - } -} - -// V6: Different z-levels (still has bug) -{ - "type": "sprite", - "name": "skull_outline_v6", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 11, 13, 11], "z": 0 }, - "fill2": { "ellipse": [6, 14, 5, 7], "z": 1 }, - "fill3": { "rect": [6, 18, 21, 12], "z": 2 }, - "fill4": { "ellipse": [25, 23, 4, 5], "z": 3 }, - "fill5": { "ellipse": [16, 34, 8, 5], "z": 4 } - } -} - -// V7: 3 shapes - works -{ - "type": "sprite", - "name": "skull_outline_v7", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 11, 13, 11], "z": 0 }, - "fill2": { "rect": [6, 18, 20, 20], "z": 0 }, - "fill3": { "ellipse": [25, 23, 4, 5], "z": 0 } - } -} - -// V8: 4 shapes - works -{ - "type": "sprite", - "name": "skull_outline_v8", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 11, 13, 11], "z": 0 }, - "fill2": { "rect": [6, 18, 20, 20], "z": 0 }, - "fill3": { "ellipse": [25, 23, 4, 5], "z": 0 }, - "fill4": { "ellipse": [5, 14, 4, 6], "z": 0 } - } -} - -// V9: 5 shapes - has bug (5 region limit) -{ - "type": "sprite", - "name": "skull_outline_v9", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 11, 13, 11], "z": 0 }, - "fill2": { "rect": [6, 18, 20, 10], "z": 0 }, - "fill3": { "ellipse": [25, 23, 4, 5], "z": 0 }, - "fill4": { "ellipse": [5, 14, 4, 6], "z": 0 }, - "fill5": { "ellipse": [16, 34, 8, 6], "z": 0 } - } -} - -// V10: 4 regions using union (has bug - 5 total shapes) -{ - "type": "sprite", - "name": "skull_outline_v10", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 11, 13, 11], "z": 0 }, - "fill2": { - "union": [ - { "rect": [6, 18, 20, 10] }, - { "ellipse": [16, 34, 8, 6] } - ], - "z": 0 - }, - "fill3": { "ellipse": [25, 23, 4, 5], "z": 0 }, - "fill4": { "ellipse": [5, 14, 4, 6], "z": 0 } - } -} - -// V11: 4 shapes - has gap between cranium and jaw -{ - "type": "sprite", - "name": "skull_outline_v11", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 10, 13, 10], "z": 0 }, - "fill2": { "ellipse": [15, 28, 11, 12], "z": 0 }, - "fill3": { "ellipse": [26, 22, 4, 5], "z": 0 }, - "fill4": { "ellipse": [4, 13, 4, 7], "z": 0 } - } -} - -// V12: Overlaps well but jaw too long -{ - "type": "sprite", - "name": "skull_outline_v12", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 11, 14, 12], "z": 0 }, - "fill2": { "ellipse": [15, 26, 11, 14], "z": 0 }, - "fill3": { "ellipse": [26, 22, 4, 6], "z": 0 }, - "fill4": { "ellipse": [4, 14, 4, 7], "z": 0 } - } -} - -// V13: Better proportions - has gap -{ - "type": "sprite", - "name": "skull_outline_v13", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 12, 14, 12], "z": 0 }, - "fill2": { "ellipse": [15, 28, 10, 10], "z": 0 }, - "fill3": { "ellipse": [25, 24, 4, 5], "z": 0 }, - "fill4": { "ellipse": [4, 14, 3, 6], "z": 0 } - } -} - -// V14: Good baseline -{ - "type": "sprite", - "name": "skull_outline_v14", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 12, 14, 12], "z": 0 }, - "fill2": { "ellipse": [15, 26, 10, 12], "z": 0 }, - "fill3": { "ellipse": [26, 23, 4, 5], "z": 0 }, - "fill4": { "ellipse": [4, 14, 3, 6], "z": 0 } - } -} - -// V15: Has gap - shapes taper at overlap zone -{ - "type": "sprite", - "name": "skull_outline_v15", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 11, 13, 11], "z": 0 }, - "fill2": { "ellipse": [15, 27, 9, 11], "z": 0 }, - "fill3": { "ellipse": [26, 21, 4, 6], "z": 0 }, - "fill4": { "ellipse": [4, 13, 3, 6], "z": 0 } - } -} - -// V16: 4 ellipses - works but overly complex -{ - "type": "sprite", - "name": "skull_outline_v16", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "ellipse": [14, 13, 13, 13], "z": 0 }, - "fill2": { "ellipse": [15, 29, 10, 10], "z": 0 }, - "fill3": { "ellipse": [26, 22, 4, 6], "z": 0 }, - "fill4": { "ellipse": [4, 14, 3, 7], "z": 0 } - } -} - -// ============================================================================ -// SIMPLE APPROACH: Circle + Rect + Triangle (mushroom shape) -// ============================================================================ - -// V17: Simple mushroom - symmetric front view -{ - "type": "sprite", - "name": "skull_outline_v17", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "circle": [16, 12, 12], "z": 0 }, - "fill2": { "rect": [6, 18, 20, 10], "z": 0 }, - "fill3": { "polygon": [[6, 28], [26, 28], [16, 38]], "z": 0 } - } -} - -// V18: 3/4 view - shapes shifted left -{ - "type": "sprite", - "name": "skull_outline_v18", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "circle": [14, 12, 12], "z": 0 }, - "fill2": { "rect": [4, 18, 22, 10], "z": 0 }, - "fill3": { "polygon": [[4, 28], [26, 28], [14, 38]], "z": 0 } - } -} - -// V19: Has gap - triangle not connected -{ - "type": "sprite", - "name": "skull_outline_v19", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "circle": [14, 12, 12], "z": 0 }, - "fill2": { "rect": [8, 18, 20, 10], "z": 0 }, - "fill3": { "polygon": [[8, 28], [8, 38], [26, 33]], "z": 0 } - } -} - -// V20: Triangle too narrow -{ - "type": "sprite", - "name": "skull_outline_v20", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "circle": [14, 12, 12], "z": 0 }, - "fill2": { "rect": [6, 18, 20, 10], "z": 0 }, - "fill3": { "polygon": [[6, 28], [6, 38], [28, 33]], "z": 0 } - } -} - -// V21: Sharp triangle chin -{ - "type": "sprite", - "name": "skull_outline_v21", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "circle": [14, 12, 12], "z": 0 }, - "fill2": { "rect": [6, 18, 20, 10], "z": 0 }, - "fill3": { "polygon": [[6, 28], [26, 28], [28, 36]], "z": 0 } - } -} - -// V22: 6-vertex polygon has fill bug -{ - "type": "sprite", - "name": "skull_outline_v22", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "circle": [14, 12, 12], "z": 0 }, - "fill2": { "rect": [6, 18, 20, 10], "z": 0 }, - "fill3": { - "polygon": [ - [6, 28], [26, 28], [20, 32], [24, 36], [20, 38], [6, 38] - ], - "z": 0 - } - } -} - -// V23: 4-vertex trapezoid - works but straight hypotenuse -{ - "type": "sprite", - "name": "skull_outline_v23", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "circle": [14, 12, 12], "z": 0 }, - "fill2": { "rect": [6, 18, 20, 10], "z": 0 }, - "fill3": { - "polygon": [[6, 28], [26, 28], [24, 38], [6, 38]], - "z": 0 - } - } -} - -// V24: 5-vertex polygon - bowed hypotenuse -{ - "type": "sprite", - "name": "skull_outline_v24", - "size": [32, 40], - "palette": "outline_pal", - "regions": { - "fill": { "circle": [14, 12, 12], "z": 0 }, - "fill2": { "rect": [6, 18, 20, 10], "z": 0 }, - // 5 vertices: add midpoint on hypotenuse that bows inward - "fill3": { - "polygon": [ - [6, 28], // top-left - [26, 28], // top-right - [20, 33], // midpoint bows inward (was ~25,33 on straight line) - [24, 38], // chin right - [6, 38] // chin left - ], - "z": 0 - } - } -} diff --git a/examples/avatar-prototype/skull-outline_skull_outline_v23.png b/examples/avatar-prototype/skull-outline_skull_outline_v23.png deleted file mode 100644 index ae043ea3..00000000 Binary files a/examples/avatar-prototype/skull-outline_skull_outline_v23.png and /dev/null differ diff --git a/examples/avatar-prototype/skull-outline_skull_outline_v24.png b/examples/avatar-prototype/skull-outline_skull_outline_v24.png deleted file mode 100644 index 4207a084..00000000 Binary files a/examples/avatar-prototype/skull-outline_skull_outline_v24.png and /dev/null differ diff --git a/examples/avatar-prototype/skull.png b/examples/avatar-prototype/skull.png deleted file mode 100644 index 563198bd..00000000 Binary files a/examples/avatar-prototype/skull.png and /dev/null differ diff --git a/examples/avatar-prototype/skull.pxl b/examples/avatar-prototype/skull.pxl deleted file mode 100644 index 6dfb2378..00000000 --- a/examples/avatar-prototype/skull.pxl +++ /dev/null @@ -1,720 +0,0 @@ -// ============================================================================ -// SKULL HEAD - Artisan Workflow Test -// ============================================================================ -// Reference: examples/iso-prototype/reference-skull.jpg -// Target: 32x40 isometric skull with 3-surface shading -// -// Components: -// - cranium (dome shape) -// - eye_sockets (dark recessed areas) -// - nose (triangular cavity) -// - jaw (teeth, lower structure) -// - shading (lit/shadow overlays) -// ============================================================================ - -// Bone palette - derived from reference skull colors -{ - "type": "palette", - "name": "skull_pal", - "colors": { - "_": "transparent", - - // Bone ramp (5 levels) - "bone_hi": "#f8f4f0", - "bone_lit": "#e8e0d8", - "bone": "#d8d0c8", - "bone_shd": "#b8b0a8", - "bone_dark": "#989088", - - // Cavities (eye sockets, nose) - "cavity": "#484040", - "cavity_deep": "#302828", - - // Teeth - "tooth": "#e0d8d0", - "tooth_shd": "#c8c0b8", - "tooth_gap": "#686060", - - // Multiple regions with same color - "cavity_l": "#484040", - "cavity_r": "#484040", - "cavity_nose": "#484040", - "bone_jaw": "#d8d0c8", - "bone_lit_jaw": "#e8e0d8", - "bone_shd_jaw": "#b8b0a8", - "bone_shd_nose": "#b8b0a8", - "bone_dark_l": "#989088", - "bone_dark_r": "#989088" - } -} - -// V0: Initial skull draft -{ - "type": "sprite", - "name": "skull_v0", - "size": [32, 40], - "palette": "skull_pal", - "regions": { - // CRANIUM - main dome shape - "bone": { "ellipse": [16, 14, 13, 12], "z": 0 }, - - // Cranium shading - lit side (right) - "bone_lit": { - "polygon": [ - [18, 4], [24, 6], [27, 10], [28, 16], [27, 22], [24, 24], [18, 24], - [20, 20], [22, 16], [22, 10], [20, 6] - ], - "z": 1 - }, - - // Cranium shading - shadow side (left) - "bone_shd": { - "polygon": [ - [14, 4], [8, 6], [5, 10], [4, 16], [5, 22], [8, 24], [14, 24], - [12, 20], [10, 16], [10, 10], [12, 6] - ], - "z": 1 - }, - - // Highlight on dome - "bone_hi": { - "polygon": [[20, 6], [24, 8], [25, 12], [23, 14], [20, 12], [19, 8]], - "z": 2 - }, - - // EYE SOCKETS - dark recessed areas - "cavity_l": { - "ellipse": [11, 16, 4, 3], - "z": 3 - }, - "cavity_r": { - "ellipse": [21, 16, 4, 3], - "z": 3 - }, - - // Deeper eye shadows - "cavity_deep": { - "points": [[10, 16], [11, 17], [20, 16], [21, 17]], - "z": 4 - }, - - // NOSE - triangular cavity - "cavity_nose": { - "polygon": [[14, 20], [18, 20], [16, 26]], - "z": 3 - }, - - // Nose bone ridge - "bone_shd_nose": { - "line": [[16, 14], [16, 20]], - "z": 2 - }, - - // JAW - lower face structure - "bone_jaw": { - "polygon": [ - [8, 26], [24, 26], [26, 28], [26, 34], [24, 38], [8, 38], [6, 34], [6, 28] - ], - "z": 0 - }, - - // Jaw shading - "bone_lit_jaw": { - "polygon": [[20, 26], [24, 26], [26, 28], [26, 34], [24, 36], [20, 36]], - "z": 1 - }, - "bone_shd_jaw": { - "polygon": [[8, 26], [12, 26], [12, 36], [8, 38], [6, 34], [6, 28]], - "z": 1 - }, - - // TEETH - horizontal lines - "tooth": { - "rect": [10, 28, 12, 6], - "z": 2 - }, - "tooth_gap": { - "union": [ - { "line": [[10, 30], [22, 30]] }, - { "line": [[10, 32], [22, 32]] }, - { "points": [[12, 28], [14, 28], [16, 28], [18, 28], [20, 28]] }, - { "points": [[12, 34], [14, 34], [16, 34], [18, 34], [20, 34]] } - ], - "z": 3 - }, - - // Cheekbone structure - "bone_dark_l": { - "polygon": [[6, 22], [8, 20], [10, 22], [10, 26], [6, 26]], - "z": 1 - }, - "bone_dark_r": { - "polygon": [[26, 22], [24, 20], [22, 22], [22, 26], [26, 26]], - "z": 1 - } - } -} - -// ============================================================================ -// CRANIUM ITERATION - Round 1 -// ============================================================================ - -// FINAL: Iteration Round 2 - 3/4 view (front-left) perspective -// Reference: skull faces viewer's left. Right side = near (more visible), -// Left side = far (foreshortened). Light from upper-right. -// Round 2 changes: brow ridge, angular eye sockets, narrower jaw, teeth character -{ - "type": "sprite", - "name": "skull", - "size": [32, 40], - "palette": "skull_pal", - "regions": { - // CRANIUM - off-center ellipse shifted right (near side extends further right) - "bone": { "ellipse": [18, 12, 13, 11], "z": 0 }, - - // Cranium shading - lit/near side (right) - wide, visible curve - "bone_lit": { - "polygon": [ - [19, 2], [26, 4], [29, 8], [30, 14], [29, 20], [25, 23], [19, 23], - [22, 20], [25, 15], [25, 10], [22, 5] - ], - "z": 1 - }, - - // Cranium shading - shadow/far side (left) - narrow, foreshortened - "bone_shd": { - "polygon": [ - [16, 2], [12, 4], [9, 8], [7, 14], [8, 20], [11, 23], [16, 23], - [14, 20], [12, 15], [12, 10], [14, 5] - ], - "z": 1 - }, - - // Deep shadow on far left edge + brow ridge overhang (1px line) - "bone_dark": { - "union": [ - { "polygon": [[7, 6], [5, 10], [5, 16], [7, 22], [9, 20], [8, 14], [9, 8]] }, - { "polygon": [[7, 13], [17, 12], [28, 13], [28, 14], [7, 14]] } - ], - "z": 2 - }, - - // Highlight on dome - prominent specular on near/right side - "bone_hi": { - "polygon": [[23, 4], [28, 6], [29, 11], [27, 14], [24, 12], [23, 7]], - "z": 2 - }, - - // EYE SOCKETS - angular trapezoids (wider at brow, narrow toward cheek) - // Left (far) - smaller, foreshortened - "cavity_l": { - "polygon": [[8, 15], [15, 14], [14, 20], [9, 21]], - "z": 3 - }, - // Right (near) - larger, angular trapezoid converging downward - "cavity_r": { - "polygon": [[18, 14], [27, 14], [26, 21], [19, 21]], - "z": 3 - }, - - // Deeper eye shadows - concentrated at inner/lower areas - "cavity_deep": { - "union": [ - { "polygon": [[9, 16], [13, 15], [13, 19], [10, 20]] }, - { "polygon": [[20, 15], [25, 15], [25, 19], [21, 20]] } - ], - "z": 4 - }, - - // NOSE - narrower, offset right for 3/4 perspective - "cavity_nose": { - "polygon": [[15, 22], [19, 22], [18, 25], [17, 27], [15, 25]], - "z": 3 - }, - - // Nose bone ridge - offset right, starts below brow - "bone_shd_nose": { - "line": [[17, 15], [17, 22]], - "z": 2 - }, - - // CHEEKBONES - pronounced, left (far) narrower, right (near) wider - "bone_dark_l": { - "polygon": [[5, 18], [8, 15], [9, 18], [9, 24], [5, 25]], - "z": 1 - }, - "bone_dark_r": { - "polygon": [[29, 17], [27, 14], [26, 18], [26, 24], [29, 25]], - "z": 1 - }, - - // JAW - narrower than cranium, asymmetric 3/4 - "bone_jaw": { - "polygon": [ - [8, 24], [26, 24], [28, 26], [28, 33], [24, 37], [11, 37], [7, 33], [6, 26] - ], - "z": 0 - }, - - // Jaw shading - lit/near side (right) - "bone_lit_jaw": { - "polygon": [[20, 24], [26, 24], [28, 26], [28, 33], [24, 35], [20, 35]], - "z": 1 - }, - // Jaw shading - shadow/far side (left) - "bone_shd_jaw": { - "polygon": [[8, 24], [13, 24], [13, 35], [11, 37], [7, 33], [6, 26]], - "z": 1 - }, - - // TEETH - narrower to match jaw, with character - "tooth": { - "rect": [10, 26, 16, 7], - "z": 2 - }, - // Teeth gaps - clean bite line with varied vertical dividers - "tooth_gap": { - "union": [ - // Bite line (main horizontal gap between upper and lower teeth) - { "line": [[10, 29], [26, 29]] }, - // Upper teeth vertical dividers - varied widths for character - { "points": [[13, 26], [16, 26], [18, 26], [21, 26], [24, 26]] }, - { "points": [[13, 27], [16, 27], [18, 27], [21, 27], [24, 27]] }, - { "points": [[13, 28], [16, 28], [18, 28], [21, 28], [24, 28]] }, - // Lower teeth vertical dividers - offset for character - { "points": [[12, 30], [15, 30], [19, 30], [22, 30]] }, - { "points": [[12, 31], [15, 31], [19, 31], [22, 31]] }, - { "points": [[12, 32], [15, 32], [19, 32], [22, 32]] } - ], - "z": 3 - }, - // Tooth shading for depth on far/left side - "tooth_shd": { - "union": [ - { "line": [[10, 26], [10, 33]] }, - { "line": [[11, 26], [11, 33]] }, - { "line": [[10, 33], [26, 33]] } - ], - "z": 3 - } - } -} - -// V1B: Taller dome, more pronounced brow ridge -{ - "type": "sprite", - "name": "skull_v1b", - "size": [32, 40], - "palette": "skull_pal", - "regions": { - // CRANIUM - taller dome - "bone": { "ellipse": [16, 12, 13, 12], "z": 0 }, - - // Brow ridge - prominent overhang - "bone_shd": { - "polygon": [ - [5, 12], [8, 10], [16, 9], [24, 10], [27, 12], [27, 15], [5, 15] - ], - "z": 1 - }, - - // Cranium shading - lit side - "bone_lit": { - "polygon": [ - [18, 2], [24, 4], [27, 8], [28, 12], [27, 10], [24, 6], [18, 4], - [20, 6], [22, 9], [23, 12] - ], - "z": 1 - }, - - // Highlight - "bone_hi": { - "polygon": [[19, 3], [23, 5], [24, 8], [22, 10], [19, 8]], - "z": 2 - }, - - // EYE SOCKETS - recessed under brow - "cavity_l": { - "polygon": [[7, 15], [13, 15], [13, 19], [7, 19]], - "z": 3 - }, - "cavity_r": { - "polygon": [[19, 15], [25, 15], [25, 19], [19, 19]], - "z": 3 - }, - - // Deeper shadows - "cavity_deep": { - "points": [[9, 17], [10, 17], [11, 17], [21, 17], [22, 17], [23, 17]], - "z": 4 - }, - - // NOSE - "cavity_nose": { - "polygon": [[14, 20], [18, 20], [17, 23], [16, 26], [15, 23]], - "z": 3 - }, - - "bone_shd_nose": { - "line": [[16, 15], [16, 20]], - "z": 2 - }, - - // JAW - "bone_jaw": { - "polygon": [ - [7, 26], [25, 26], [27, 28], [26, 34], [23, 38], [9, 38], [6, 34], [5, 28] - ], - "z": 0 - }, - - "bone_lit_jaw": { - "polygon": [[20, 26], [25, 26], [27, 28], [26, 34], [23, 36], [20, 36]], - "z": 1 - }, - "bone_shd_jaw": { - "polygon": [[7, 26], [12, 26], [12, 36], [9, 38], [6, 34], [5, 28]], - "z": 1 - }, - - // TEETH - "tooth": { - "rect": [9, 28, 14, 6], - "z": 2 - }, - "tooth_gap": { - "union": [ - { "line": [[9, 30], [23, 30]] }, - { "line": [[9, 32], [23, 32]] }, - { "points": [[11, 28], [13, 28], [15, 28], [17, 28], [19, 28], [21, 28]] }, - { "points": [[11, 34], [13, 34], [15, 34], [17, 34], [19, 34], [21, 34]] } - ], - "z": 3 - }, - - // Cheekbones - "bone_dark_l": { - "polygon": [[5, 19], [7, 17], [8, 19], [8, 26], [5, 26]], - "z": 1 - }, - "bone_dark_r": { - "polygon": [[27, 19], [25, 17], [24, 19], [24, 26], [27, 26]], - "z": 1 - } - } -} - -// ============================================================================ -// TALKING ANIMATION - Mouth State Overlays -// ============================================================================ -// Skull mouth positions for dialogue: -// - mouth_closed: jaw in neutral position (default skull) -// - mouth_small: slight jaw drop, gap between teeth rows -// - mouth_wide: full jaw drop, more teeth visible -// ============================================================================ - -// MOUTH CLOSED - neutral position, covers base and redraws at same position -// Use this to ensure consistent overlay behavior in compositions -{ - "type": "sprite", - "name": "skull_mouth_closed", - "size": [32, 40], - "palette": "skull_pal", - "regions": { - // Cover existing jaw area completely - "bone": { "rect": [2, 23, 28, 17], "z": 10 }, - - // JAW - matches base skull narrower 3/4 perspective - "bone_jaw": { - "polygon": [ - [8, 24], [26, 24], [28, 26], [28, 33], [24, 37], [11, 37], [7, 33], [6, 26] - ], - "z": 11 - }, - - // Jaw shading - "bone_lit_jaw": { - "polygon": [[20, 24], [26, 24], [28, 26], [28, 33], [24, 35], [20, 35]], - "z": 12 - }, - "bone_shd_jaw": { - "polygon": [[8, 24], [13, 24], [13, 35], [11, 37], [7, 33], [6, 26]], - "z": 12 - }, - - // TEETH - narrower, with character - "tooth": { - "rect": [10, 26, 16, 7], - "z": 13 - }, - "tooth_gap": { - "union": [ - { "line": [[10, 29], [26, 29]] }, - { "points": [[13, 26], [16, 26], [18, 26], [21, 26], [24, 26]] }, - { "points": [[13, 27], [16, 27], [18, 27], [21, 27], [24, 27]] }, - { "points": [[13, 28], [16, 28], [18, 28], [21, 28], [24, 28]] }, - { "points": [[12, 30], [15, 30], [19, 30], [22, 30]] }, - { "points": [[12, 31], [15, 31], [19, 31], [22, 31]] }, - { "points": [[12, 32], [15, 32], [19, 32], [22, 32]] } - ], - "z": 14 - }, - "tooth_shd": { - "union": [ - { "line": [[10, 26], [10, 33]] }, - { "line": [[11, 26], [11, 33]] }, - { "line": [[10, 33], [26, 33]] } - ], - "z": 14 - }, - - // Cheekbone structure - matches base skull 3/4 - "bone_dark_l": { - "polygon": [[5, 18], [8, 15], [9, 18], [9, 24], [5, 25]], - "z": 12 - }, - "bone_dark_r": { - "polygon": [[29, 17], [27, 14], [26, 18], [26, 24], [29, 25]], - "z": 12 - } - } -} - -// MOUTH SMALL OPEN - slight jaw drop (3px), gap between upper and lower teeth -{ - "type": "sprite", - "name": "skull_mouth_small", - "size": [32, 40], - "palette": "skull_pal", - "regions": { - // Cover existing jaw area completely - "bone": { "rect": [2, 23, 28, 17], "z": 10 }, - - // Dark cavity behind teeth (mouth interior) - "cavity": { - "rect": [10, 30, 16, 3], - "z": 11 - }, - - // UPPER JAW - stays in place, narrower 3/4 perspective - "bone_jaw": { - "polygon": [ - [8, 24], [26, 24], [28, 26], [28, 30], [6, 30], [6, 26] - ], - "z": 12 - }, - - // Upper jaw shading - "bone_lit_jaw": { - "polygon": [[20, 24], [26, 24], [28, 26], [28, 30], [20, 30]], - "z": 13 - }, - "bone_shd_jaw": { - "polygon": [[8, 24], [13, 24], [13, 30], [6, 30], [6, 26]], - "z": 13 - }, - - // UPPER TEETH - narrower, with character - "tooth": { - "rect": [10, 26, 16, 3], - "z": 14 - }, - "tooth_gap": { - "union": [ - { "points": [[13, 26], [16, 26], [18, 26], [21, 26], [24, 26]] }, - { "points": [[13, 27], [16, 27], [18, 27], [21, 27], [24, 27]] }, - { "points": [[13, 28], [16, 28], [18, 28], [21, 28], [24, 28]] } - ], - "z": 15 - }, - - // LOWER JAW - dropped 3px, narrower 3/4 perspective - "bone_dark": { - "polygon": [ - [8, 33], [26, 33], [28, 35], [24, 39], [11, 39], [7, 35] - ], - "z": 12 - }, - - // Lower jaw shading - "bone_lit": { - "polygon": [[20, 33], [26, 33], [28, 35], [24, 37], [20, 37]], - "z": 13 - }, - "bone_shd": { - "polygon": [[8, 33], [13, 33], [13, 37], [11, 39], [7, 35]], - "z": 13 - }, - - // LOWER TEETH - dropped, with character - "tooth_shd": { - "rect": [10, 33, 16, 4], - "z": 14 - }, - "cavity_deep": { - "union": [ - { "line": [[10, 35], [26, 35]] }, - { "points": [[12, 33], [15, 33], [19, 33], [22, 33]] }, - { "points": [[12, 34], [15, 34], [19, 34], [22, 34]] }, - { "points": [[12, 35], [15, 35], [19, 35], [22, 35]] }, - { "points": [[12, 36], [15, 36], [19, 36], [22, 36]] } - ], - "z": 15 - }, - - // Cheekbones - matches base skull - "bone_dark_l": { - "polygon": [[5, 18], [8, 15], [9, 18], [9, 24], [5, 25]], - "z": 13 - }, - "bone_dark_r": { - "polygon": [[29, 17], [27, 14], [26, 18], [26, 24], [29, 25]], - "z": 13 - } - } -} - -// MOUTH WIDE OPEN - full jaw drop (5px), maximum teeth exposure -{ - "type": "sprite", - "name": "skull_mouth_wide", - "size": [32, 40], - "palette": "skull_pal", - "regions": { - // Cover existing jaw area completely - "bone": { "rect": [2, 23, 28, 17], "z": 10 }, - - // Dark cavity behind teeth (larger mouth interior) - "cavity": { - "rect": [9, 30, 18, 6], - "z": 11 - }, - "cavity_deep": { - "rect": [11, 31, 14, 4], - "z": 12 - }, - - // UPPER JAW - stays in place, narrower 3/4 perspective - "bone_jaw": { - "polygon": [ - [8, 24], [26, 24], [28, 26], [28, 30], [6, 30], [6, 26] - ], - "z": 13 - }, - - // Upper jaw shading - "bone_lit_jaw": { - "polygon": [[20, 24], [26, 24], [28, 26], [28, 30], [20, 30]], - "z": 14 - }, - "bone_shd_jaw": { - "polygon": [[8, 24], [13, 24], [13, 30], [6, 30], [6, 26]], - "z": 14 - }, - - // UPPER TEETH - narrower, with character - "tooth": { - "rect": [10, 26, 16, 3], - "z": 15 - }, - "tooth_gap": { - "union": [ - { "points": [[13, 26], [16, 26], [18, 26], [21, 26], [24, 26]] }, - { "points": [[13, 27], [16, 27], [18, 27], [21, 27], [24, 27]] }, - { "points": [[13, 28], [16, 28], [18, 28], [21, 28], [24, 28]] } - ], - "z": 16 - }, - - // LOWER JAW - dropped 5px, narrower 3/4 perspective - "bone_dark": { - "polygon": [ - [8, 36], [26, 36], [28, 38], [24, 42], [11, 42], [7, 38] - ], - "z": 13 - }, - - // Lower jaw shading - "bone_lit": { - "polygon": [[20, 36], [26, 36], [28, 38], [24, 40], [20, 40]], - "z": 14 - }, - "bone_shd": { - "polygon": [[8, 36], [13, 36], [13, 40], [11, 42], [7, 38]], - "z": 14 - }, - - // LOWER TEETH - dropped further, with character - "tooth_shd": { - "rect": [10, 36, 16, 4], - "z": 15 - }, - "bone_shd_nose": { - "union": [ - { "line": [[10, 38], [26, 38]] }, - { "points": [[12, 36], [15, 36], [19, 36], [22, 36]] }, - { "points": [[12, 37], [15, 37], [19, 37], [22, 37]] }, - { "points": [[12, 38], [15, 38], [19, 38], [22, 38]] }, - { "points": [[12, 39], [15, 39], [19, 39], [22, 39]] } - ], - "z": 16 - }, - - // Cheekbones - matches base skull - "bone_dark_l": { - "polygon": [[5, 18], [8, 15], [9, 18], [9, 24], [5, 25]], - "z": 14 - }, - "bone_dark_r": { - "polygon": [[29, 17], [27, 14], [26, 18], [26, 24], [29, 25]], - "z": 14 - } - } -} - -// ============================================================================ -// TALKING COMPOSITIONS - Layer mouth states on skull base -// ============================================================================ - -{ - "type": "composition", - "name": "skull_talk_closed", - "size": [32, 40], - "cell_size": [32, 40], - "sprites": { "S": "skull", "M": "skull_mouth_closed" }, - "layers": [{ "map": ["S"] }, { "map": ["M"] }] -} - -{ - "type": "composition", - "name": "skull_talk_small", - "size": [32, 40], - "cell_size": [32, 40], - "sprites": { "S": "skull", "M": "skull_mouth_small" }, - "layers": [{ "map": ["S"] }, { "map": ["M"] }] -} - -{ - "type": "composition", - "name": "skull_talk_wide", - "size": [32, 40], - "cell_size": [32, 40], - "sprites": { "S": "skull", "M": "skull_mouth_wide" }, - "layers": [{ "map": ["S"] }, { "map": ["M"] }] -} - -// ============================================================================ -// TALKING ANIMATION - Frame sequence for dialogue -// ============================================================================ - -{ - "type": "animation", - "name": "skull_talking", - "frames": [ - "skull_talk_closed", "skull_talk_small", "skull_talk_wide", "skull_talk_small", - "skull_talk_closed", "skull_talk_small", "skull_talk_wide", "skull_talk_wide", - "skull_talk_small", "skull_talk_closed", "skull_talk_closed", "skull_talk_small" - ], - "duration": 80, - "loop": true -} diff --git a/examples/avatar-prototype/skull_4x.png b/examples/avatar-prototype/skull_4x.png deleted file mode 100644 index 1dcf3c16..00000000 Binary files a/examples/avatar-prototype/skull_4x.png and /dev/null differ diff --git a/examples/avatar-prototype/skull_talk_closed.png b/examples/avatar-prototype/skull_talk_closed.png deleted file mode 100644 index 1666254b..00000000 Binary files a/examples/avatar-prototype/skull_talk_closed.png and /dev/null differ diff --git a/examples/avatar-prototype/skull_talk_small.png b/examples/avatar-prototype/skull_talk_small.png deleted file mode 100644 index 9489343f..00000000 Binary files a/examples/avatar-prototype/skull_talk_small.png and /dev/null differ diff --git a/examples/avatar-prototype/skull_talk_wide.png b/examples/avatar-prototype/skull_talk_wide.png deleted file mode 100644 index 6bfc6028..00000000 Binary files a/examples/avatar-prototype/skull_talk_wide.png and /dev/null differ diff --git a/examples/avatar-prototype/skull_talking.gif b/examples/avatar-prototype/skull_talking.gif deleted file mode 100644 index 39df6433..00000000 Binary files a/examples/avatar-prototype/skull_talking.gif and /dev/null differ diff --git a/examples/avatar-prototype/skull_talking_4x.gif b/examples/avatar-prototype/skull_talking_4x.gif deleted file mode 100644 index 115cc6b7..00000000 Binary files a/examples/avatar-prototype/skull_talking_4x.gif and /dev/null differ diff --git a/examples/avatar-prototype/skull_v0.png b/examples/avatar-prototype/skull_v0.png deleted file mode 100644 index 1b7d7f34..00000000 Binary files a/examples/avatar-prototype/skull_v0.png and /dev/null differ diff --git a/examples/avatar-prototype/skull_v0_4x.png b/examples/avatar-prototype/skull_v0_4x.png deleted file mode 100644 index 04f297e7..00000000 Binary files a/examples/avatar-prototype/skull_v0_4x.png and /dev/null differ diff --git a/examples/avatar-prototype/skull_v1a_4x.png b/examples/avatar-prototype/skull_v1a_4x.png deleted file mode 100644 index 4c930056..00000000 Binary files a/examples/avatar-prototype/skull_v1a_4x.png and /dev/null differ diff --git a/examples/avatar-prototype/skull_v1b_4x.png b/examples/avatar-prototype/skull_v1b_4x.png deleted file mode 100644 index 8875d940..00000000 Binary files a/examples/avatar-prototype/skull_v1b_4x.png and /dev/null differ diff --git a/examples/skull/GAPS.md b/examples/skull/GAPS.md new file mode 100644 index 00000000..a74e537d --- /dev/null +++ b/examples/skull/GAPS.md @@ -0,0 +1,90 @@ +# Gaps found while drawing the skull + +These are places where the compiler couldn't express what the art needed, or +where it didn't behave the way the docs describe. Each one has a minimal repro +and a note on how `skull.pxl` works around it. Checked against `pxl` built from +this branch. + +## 1. Region paint order is random unless every region has `z` + +```json5 +{ type: "palette", name: "p", colors: { _: "transparent", a: "#ff0000", b: "#0000ff", c: "#00ff00", d: "#ffff00" } } +{ type: "sprite", name: "order", size: [4, 4], palette: "p", regions: { + a: { rect: [0, 0, 4, 4] }, + b: { rect: [1, 1, 3, 3] }, + c: { rect: [2, 2, 2, 2] }, + d: { points: [[3, 3]] }, +} } +``` + +The docs (format/regions.md, "Z-Order") say the default is definition order. +`Sprite.regions` is a `HashMap`, though, and `render_structured` stable-sorts +by z, so equal-z regions paint in hash order. That order changes from process +to process: rendering the repro 6 times gave 3 different PNGs. **Workaround:** +every region in `skull.pxl` has an explicit, unique `z`. The fix is probably an +`IndexMap` for `regions`, which would also make `fill`/`except` dependency +order match the source. + +## 2. `auto-outline` (and several other region modifiers) does nothing + +```json5 +{ type: "palette", name: "p", colors: { _: "transparent", body: "#e6dccd", outline: "#000000" } } +{ type: "sprite", name: "blob", size: [8, 8], palette: "p", regions: { + body: { rect: [2, 2, 4, 4], z: 0 }, + outline: { "auto-outline": "body", thickness: 1, z: 1 }, +} } +``` + +`pxl mask --count` reports 16 `body` pixels and 0 `outline` pixels. +`RegionDef` parses `auto-outline`, `repeat`/`spacing`/`offset-alternate`, +`transform`, `jitter`, `round` and the `x`/`y` range limits, but +`structured.rs::rasterize_region` never reads any of them, so they are dropped +without a warning. **Workaround:** the outline is drawn explicitly as a closed +polyline. The teeth are listed one by one instead of using `repeat`. + +## 3. `fill: "inside(X)"` can flood the outside of a concave shape + +```json5 +{ type: "palette", name: "p", colors: { _: "transparent", bone: "#e6dccd", outline: "#000000" } } +{ type: "sprite", name: "cup", size: [10, 10], palette: "p", regions: { + outline: { z: 1, line: [[0, 0], [3, 0], [3, 6], [6, 6], [6, 0], [9, 0], [9, 9], [0, 9], [0, 0]] }, + bone: { z: 0, fill: "inside(outline)" }, +} } +``` + +The seed is the centre of the boundary's bounding box, or the nearest free +pixel to it. For the "U" that pixel is in the notch, so `bone` fills the notch +and leaks out of the top of the canvas, and the inside of the U stays empty. +The docs offer `seed: [x, y]`, but `RegionDef.seed` is the `u32` jitter seed +and `flood_fill` is always called with `None`. A related problem: a `fill` +nested inside `union`/`intersect` isn't deferred the way a top-level `fill` is, +so it can run before its boundary exists and fill the whole canvas. +**Workaround:** the cranium keeps its `fill` (its centre is inside). The +mandible is concave, so its interior is a `path` polygon with the same vertices +as its outline. Every `fill` is top-level. + +## 4. Compositions can't offset a layer + +```json5 +{ type: "palette", name: "p", colors: { _: "transparent", x: "#ff0000" } } +{ type: "sprite", name: "dot", size: [4, 4], palette: "p", regions: { x: { points: [[0, 0]] } } } +{ type: "sprite", name: "dot_moved", palette: "p", source: "dot", transform: ["shift:2,2"] } +{ type: "composition", name: "scene", size: [4, 4], cell_size: [4, 4], + sprites: { D: "dot_moved" }, layers: [{ map: ["D"] }] } +``` + +`pxl render -s dot_moved` puts the dot at (2,2), but `pxl render -c scene` puts +it at (0,0). The composition path (`render_composition_to_image`) renders the +resolved regions and skips the derived sprite's image `transform`. The +`{ sprite, x, y }` layer form in format/composition.md doesn't help either: it +renders an empty canvas. The only offset left is `cell_size` with a +mismatched map, and that warns on every render. **Workaround:** the talking +poses `skull_jaw_ajar` and `skull_jaw_open` are written out in full as their +own sprites, not as `shift`-ed copies of `skull_jaw`. + +## 5. Derived sprites still need `palette` + +The docs example `{ type: "sprite", name: "hero_outlined", source: "hero", +transform: [...] }` fails with `missing field 'palette'`. In `skull.pxl`, +the composition after that line then wasn't found either. **Workaround:** not needed after gap 4, but any +`source:` sprite has to repeat `palette`. diff --git a/examples/skull/README.md b/examples/skull/README.md new file mode 100644 index 00000000..de16c05c --- /dev/null +++ b/examples/skull/README.md @@ -0,0 +1,55 @@ +# Skull + +A 3/4-view skull facing right, drawn in pixelsrc after `reference-skull.jpg`. +It's a 40x40 sprite on a transparent background, and the jaw is a separate +sprite so the skull can talk. + +![reference vs render](skull_comparison.png) + +![talking](skull_talking.gif) + +## Files + +| File | What it is | +|------|------------| +| `skull.pxl` | Source: the `bone` palette, the `skull_cranium` and `skull_jaw` sprites, two open-jaw poses (`skull_jaw_ajar`, `skull_jaw_open`), the `skull` / `skull_talk_*` compositions and the `skull_talking` animation | +| `skull.png` | The still at 1x (40x40) | +| `skull_8x.png` | The still at 8x (320x320) | +| `skull_talking.gif` | The talking loop at 8x | +| `reference-skull.jpg` | The target art | +| `skull_comparison.png` | Reference and render side by side, both skulls scaled to the same height | +| `compare.py` | Builds `skull_comparison.png` (needs Pillow) | +| `GAPS.md` | Things the compiler couldn't express, with minimal repros | + +## Render + +From the repo root, after `cargo build --release`: + +```sh +pxl=target/release/pxl +$pxl render examples/skull/skull.pxl -c skull -o examples/skull/skull.png +$pxl render examples/skull/skull.pxl -c skull --scale 8 -o examples/skull/skull_8x.png +$pxl render examples/skull/skull.pxl --gif --animation skull_talking --scale 8 -o examples/skull/skull_talking.gif +python3 examples/skull/compare.py +``` + +## How it's built + +- **Silhouette.** `outline` is a closed polyline (`line` with many points), + and the cranium's `bone` is `fill: "inside(outline)"`. +- **Shading.** The back-of-skull shadow is `mid: { fill: "inside(outline)", + except: ["lit"] }`, where `lit` is a transparent mask polygon covering the + lit side. Other planes are small `path` polygons: the shadowed temple, the + cheekbone underside and the far side of the face. The dome highlight is a + `path` too. +- **Detail.** The angular orbits are `path` polygons with a lighter inner `rim` + polygon. The nasal notch, tooth roots, ramus edge and chin ridge are `points` + and `line`s. The teeth are two pixels each: a lit column (`tooth`) over the + beige base. A transparent `gap` region at z 200 punches the see-through hole + at the corner of the mouth. +- **Jaw.** `skull_jaw` is drawn over the cranium. Where it meets the skull, its + top edge is a soft `seam`/`gum` line instead of a hard outline. The cranium + has a dark mouth interior hidden behind the jaw, and that interior shows when + the jaw opens. The open poses swing the mandible around the hinge: the chin + drops 1px (ajar) or 2px (open) and the hinge stays put. +- **Order.** Every region has an explicit `z` (see GAPS.md). diff --git a/examples/skull/compare.py b/examples/skull/compare.py new file mode 100644 index 00000000..e92e527e --- /dev/null +++ b/examples/skull/compare.py @@ -0,0 +1,31 @@ +"""Build skull_comparison.png: the reference beside pixelsrc's render. + + python3 examples/skull/compare.py (needs Pillow; run from the repo root) + +Expects examples/skull/skull.png to be rendered first (see README.md). +""" +from pathlib import Path + +from PIL import Image, ImageDraw + +HERE = Path(__file__).parent +ref = Image.open(HERE / "reference-skull.jpg").convert("RGB") +mine = Image.open(HERE / "skull.png").convert("RGBA") + +# Crop both to the skull, then scale the render (nearest neighbour) so the +# two skulls are the same height on screen. +ref = ref.crop((50, 20, 285, 300)) +mine = mine.crop(mine.getbbox()) +k = 8 +mine = mine.resize((mine.width * k, mine.height * k), Image.NEAREST) +ref = ref.resize((round(ref.width * mine.height / ref.height), mine.height), Image.NEAREST) + +pad, label_h = 16, 24 +out = Image.new("RGB", (ref.width + mine.width + pad * 3, mine.height + pad * 2 + label_h), "white") +out.paste(ref, (pad, pad + label_h)) +out.paste(mine, (ref.width + pad * 2, pad + label_h), mine) +draw = ImageDraw.Draw(out) +draw.text((pad, pad), "reference-skull.jpg", fill=(80, 76, 72)) +draw.text((ref.width + pad * 2, pad), "skull.pxl -> pxl render (40x40, shown 8x)", fill=(80, 76, 72)) +out.save(HERE / "skull_comparison.png") +print("Saved:", HERE / "skull_comparison.png") diff --git a/examples/avatar-prototype/reference-skull.jpg b/examples/skull/reference-skull.jpg similarity index 100% rename from examples/avatar-prototype/reference-skull.jpg rename to examples/skull/reference-skull.jpg diff --git a/examples/skull/skull.png b/examples/skull/skull.png new file mode 100644 index 00000000..3cb036dd Binary files /dev/null and b/examples/skull/skull.png differ diff --git a/examples/skull/skull.pxl b/examples/skull/skull.pxl new file mode 100644 index 00000000..5714b94c --- /dev/null +++ b/examples/skull/skull.pxl @@ -0,0 +1,432 @@ +// skull.pxl - 3/4 view skull facing right, after reference-skull.jpg +// +// 40x40 canvas, transparent background. The skull is two sprites so the jaw +// can move: skull_cranium (everything fixed) and skull_jaw (the mandible); +// the `skull` composition stacks them into the still. +// +// Every region carries an explicit z: region maps are unordered, so paint +// order must be spelled out (see GAPS.md). + +{ + type: "palette", + name: "bone", + colors: { + _: "transparent", + lit: "transparent", // mask only: the lit side of the cranium + gap: "transparent", // see-through hole at the corner of the mouth + outline: "#6f6a64", + crease: "#7c766f", + hi: "#fefded", // cream highlight + edge: "#fefded", + tooth: "#fefded", + root: "#fefded", + bone: "#e6dccd", // beige base + fleck: "#e6dccd", + mid: "#c5baa9", // tan shadow + shade: "#c5baa9", + gum: "#c5baa9", + umber: "#a4998a", // deepest bone shadow + seam: "#a4998a", + socket: "#554d48", // dark warm grey cavities + rim: "#7f7a74", // lighter inner wall of the orbits + }, +} + +// --------------------------------------------------------------------------- +// Cranium + upper jaw: everything that stays put when the skull talks +// --------------------------------------------------------------------------- +{ + type: "sprite", + name: "skull_cranium", + size: [40, 40], + palette: "bone", + regions: { + // silhouette as a closed polyline; the bone fills it + outline: { + z: 100, + line: [ + [17, 0], [22, 0], [23, 1], [27, 1], [29, 2], [34, 7], [34, 12], [35, 13], + [35, 16], [34, 17], [34, 20], [31, 23], [31, 32], [18, 32], [10, 23], [8, 23], + [6, 22], [5, 21], [5, 18], [4, 17], [4, 10], [5, 9], [5, 7], [9, 3], [11, 2], + [13, 1], [16, 1], [17, 0], + ], + }, + bone: { z: 0, fill: "inside(outline)" }, + + // everything right of this line is lit; left of it is the back-of-skull shadow + lit: { + z: -10, + path: "M17,-1 L40,-1 L40,39 L10,39 L10,21 L11,20 L10,19 L8,18 L8,16 L6,14 L6,13 L8,11 L8,9 L10,7 L10,6 L12,5 L12,4 L14,3 L15,2 L17,1 Z", + }, + mid: { z: 10, fill: "inside(outline)", except: ["lit"] }, + + // shadowed temple, cheekbone underside, far side of the face, brow + shade: { + z: 20, + union: [ + // temple hollow behind the orbit + { points: [[14, 15], [13, 16], [14, 16], [13, 17], [14, 17], [12, 18], [13, 18], [11, 19], [12, 19], [13, 19]] }, + // cheekbone underside plane + { path: "M20,20 L23,20 L23,22 L22,23 L21,25 L19,25 L18,24 L19,21 Z" }, + // right wall of the nose, far side of the face + { line: [[29, 18], [29, 22]] }, + { path: "M31,20 L33,20 L32,22 L31,22 Z" }, + // glabella + { line: [[25, 11], [28, 11]] }, + { path: "M19,26 L20,26 L20,28 L19,28 Z" }, + ], + }, + + // cream light on the dome, brow ridge, cheekbone and nose bridge + hi: { + z: 30, + union: [ + { path: "M19,2 L26,2 L28,3 L31,6 L32,7 L32,8 L33,9 L33,10 L17,10 L19,9 L16,8 L12,7 L15,4 Z" }, + { points: [[17, 11], [24, 11], [29, 11], [30, 11], [16, 13], [16, 14]] }, + { path: "M15,15 L16,15 L16,17 L15,17 Z" }, + { points: [[18, 18], [24, 12], [25, 12], [26, 13], [27, 13], [27, 14], [27, 15], [28, 15], [27, 16], [28, 16], [29, 16]] }, + { points: [[27, 17], [29, 17], [30, 17], [25, 18], [26, 18], [30, 18]] }, + { path: "M23,19 L26,19 L26,22 L25,22 L24,21 L24,20 Z" }, + { line: [[30, 19], [30, 22]] }, + { line: [[31, 19], [33, 19]] }, + ], + }, + // small breaks in the brow highlight + fleck: { z: 32, points: [[21, 10], [27, 10], [32, 10], [33, 10], [26, 15], [26, 16]] }, + + // tooth roots above the gum line + root: { + z: 35, + union: [ + { line: [[24, 23], [24, 25]] }, + { line: [[26, 23], [26, 25]] }, + { line: [[27, 23], [27, 24]] }, + { line: [[29, 23], [29, 25]] }, + { line: [[22, 25], [23, 25]] }, + ], + }, + + umber: { + z: 40, + union: [ + { line: [[26, 12], [28, 12]] }, + // cheek underside where the jaw tucks in + { points: [[11, 22], [13, 23]] }, + { line: [[19, 27], [19, 28]] }, + { points: [[19, 26], [29, 26], [30, 26]] }, + ], + }, + // gum line above the upper teeth + gum: { z: 42, line: [[20, 26], [28, 26]] }, + + // upper teeth: lit tooth on bone, two pixels per tooth + tooth: { + z: 45, + union: [ + { line: [[21, 27], [21, 28]] }, + { line: [[23, 27], [23, 28]] }, + { line: [[25, 27], [25, 28]] }, + { line: [[27, 27], [27, 28]] }, + { line: [[29, 27], [29, 28]] }, + ], + }, + + // temple ridge running down into the jaw hinge + crease: { + z: 50, + union: [ + { line: [[16, 11], [16, 12]] }, + { points: [[15, 13], [15, 14]] }, + { line: [[14, 18], [14, 19]] }, + { points: [[11, 20], [10, 21], [10, 22]] }, + ], + }, + + socket: { + z: 60, + union: [ + // near orbit: angular, heavy top-right rim + { path: "M18,11 L22,11 L23,12 L25,14 L25,16 L24,17 L22,19 L19,19 L19,18 L17,16 L17,12 Z" }, + // far orbit, cut off by the outline + { path: "M31,11 L33,11 L34,12 L34,17 L33,18 L31,18 L29,16 L29,13 Z" }, + // nasal notch + { points: [[28, 17], [27, 18], [28, 18], [27, 19], [28, 19], [27, 20], [28, 20], [27, 21]] }, + // mouth corner + { points: [[16, 25], [17, 25], [18, 25], [17, 26], [18, 26]] }, + // mouth interior, hidden behind the jaw when closed + { rect: [18, 29, 13, 3] }, + ], + }, + + // lighter inner walls of the orbits and the nasal septum + rim: { + z: 70, + union: [ + { path: "M18,13 L21,13 L23,15 L23,17 L22,18 L19,18 L18,17 Z" }, + { path: "M31,13 L32,13 L33,14 L33,16 L32,17 L31,17 L30,16 L29,15 L30,14 Z" }, + { points: [[28, 21], [27, 22]] }, + ], + }, + + gap: { z: 200, points: [[17, 27], [18, 27]] }, + }, +} + +// --------------------------------------------------------------------------- +// Mandible + lower teeth: the only moving part +// --------------------------------------------------------------------------- +{ + type: "sprite", + name: "skull_jaw", + size: [40, 40], + palette: "bone", + regions: { + outline: { + z: 100, + line: [ + [11, 22], [13, 24], [15, 25], [16, 26], [16, 27], [17, 28], [18, 28], [19, 29], + [31, 29], [31, 36], [30, 37], [24, 37], [22, 36], [20, 36], [19, 35], [18, 35], + [17, 34], [14, 34], [11, 33], [10, 32], [10, 24], [11, 23], [11, 22], + ], + }, + bone: { + z: 0, + path: "M11,22 L13,24 L15,25 L16,26 L16,27 L17,28 L18,28 L19,29 L31,29 L31,36 L30,37 L24,37 L22,36 L20,36 L19,35 L18,35 L17,34 L14,34 L11,33 L10,32 L10,24 L11,23 Z", + }, + mid: { + z: 20, + union: [ + { line: [[11, 29], [11, 30]] }, + { path: "M12,32 L19,32 L21,33 L21,34 L19,34 L18,33 L12,33 Z" }, + { path: "M17,30 L18,30 L18,31 L17,31 Z" }, + { rect: [22, 32, 5, 1] }, + { points: [[22, 35]] }, + ], + }, + umber: { + z: 30, + union: [ + { line: [[11, 31], [11, 32]] }, + { points: [[12, 33], [18, 34], [20, 35], [21, 35], [22, 36], [23, 36]] }, + { rect: [27, 32, 3, 1] }, + { path: "M19,30 L20,30 L20,31 L19,31 Z" }, + ], + }, + // chin ridge + hi: { + z: 40, + union: [ + { line: [[27, 33], [27, 35]] }, + { line: [[28, 33], [28, 34]] }, + { points: [[26, 35], [26, 36], [29, 35], [30, 36]] }, + ], + }, + // lower teeth + tooth: { + z: 50, + union: [ + { line: [[21, 30], [21, 31]] }, + { line: [[23, 30], [23, 31]] }, + { line: [[25, 30], [25, 31]] }, + { line: [[27, 30], [27, 31]] }, + { line: [[29, 30], [29, 31]] }, + ], + }, + // where the jaw meets the skull: soft shadow lines, not hard outline + seam: { + z: 110, + points: [[11, 22], [12, 23], [13, 24], [15, 25], [16, 26], [17, 28], [18, 28]], + }, + gum: { z: 110, line: [[19, 29], [30, 29]] }, + // lit front edge of the ramus, drawn over its own outline + edge: { + z: 120, + points: [[11, 23], [12, 24], [13, 25], [14, 25], [14, 26], [15, 26], [15, 27], [16, 27], [16, 28], [17, 29], [18, 29]], + }, + }, +} + +// --------------------------------------------------------------------------- +// Talking poses: the mandible swung open around the jaw hinge. Written out in +// full because compositions cannot offset a layer (see GAPS.md). +// --------------------------------------------------------------------------- + +// Jaw dropped one pixel at the chin, hinged at the ear +{ + type: "sprite", + name: "skull_jaw_ajar", + size: [40, 40], + palette: "bone", + regions: { + outline: { + z: 100, + line: [ + [11, 22], [13, 24], [15, 26], [16, 27], [16, 28], [17, 29], [18, 29], [19, 30], + [31, 30], [31, 37], [30, 38], [24, 38], [22, 37], [20, 37], [19, 36], [18, 36], + [17, 35], [14, 34], [11, 33], [10, 32], [10, 24], [11, 23], [11, 22], + ], + }, + bone: { + z: 0, + path: "M11,22 L13,24 L15,26 L16,27 L16,28 L17,29 L18,29 L19,30 L31,30 L31,37 L30,38 L24,38 L22,37 L20,37 L19,36 L18,36 L17,35 L14,34 L11,33 L10,32 L10,24 L11,23 Z", + }, + mid: { + z: 20, + union: [ + { line: [[11, 29], [11, 30]] }, + { path: "M12,32 L19,33 L21,34 L21,35 L19,35 L18,34 L12,33 Z" }, + { path: "M17,31 L18,31 L18,32 L17,32 Z" }, + { rect: [22, 33, 5, 1] }, + { points: [[22, 36]] }, + ], + }, + umber: { + z: 30, + union: [ + { line: [[11, 31], [11, 32]] }, + { points: [[12, 33], [18, 35], [20, 36], [21, 36], [22, 37], [23, 37]] }, + { rect: [27, 33, 3, 1] }, + { path: "M19,31 L20,31 L20,32 L19,32 Z" }, + ], + }, + // chin ridge + hi: { + z: 40, + union: [ + { line: [[27, 34], [27, 36]] }, + { line: [[28, 34], [28, 35]] }, + { points: [[26, 36], [26, 37], [29, 36], [30, 37]] }, + ], + }, + // lower teeth + tooth: { + z: 50, + union: [ + { line: [[21, 31], [21, 32]] }, + { line: [[23, 31], [23, 32]] }, + { line: [[25, 31], [25, 32]] }, + { line: [[27, 31], [27, 32]] }, + { line: [[29, 31], [29, 32]] }, + ], + }, + // where the jaw meets the skull: soft shadow lines, not hard outline + seam: { + z: 110, + points: [[11, 22], [12, 23], [13, 24], [15, 26], [16, 27], [17, 29], [18, 29]], + }, + gum: { z: 110, line: [[19, 30], [30, 30]] }, + // lit front edge of the ramus, drawn over its own outline + edge: { + z: 120, + points: [[11, 23], [12, 24], [13, 25], [14, 25], [14, 26], [15, 27], [15, 28], [16, 28], [16, 29], [17, 30], [18, 30]], + }, + }, +} + +// Jaw open: chin down two pixels, hinge fixed +{ + type: "sprite", + name: "skull_jaw_open", + size: [40, 40], + palette: "bone", + regions: { + outline: { + z: 100, + line: [ + [11, 22], [13, 25], [15, 26], [16, 27], [16, 28], [17, 30], [18, 30], [19, 31], + [31, 31], [31, 38], [30, 39], [24, 39], [22, 38], [20, 38], [19, 37], [18, 37], + [17, 36], [14, 35], [11, 33], [10, 32], [10, 24], [11, 23], [11, 22], + ], + }, + bone: { + z: 0, + path: "M11,22 L13,25 L15,26 L16,27 L16,28 L17,30 L18,30 L19,31 L31,31 L31,38 L30,39 L24,39 L22,38 L20,38 L19,37 L18,37 L17,36 L14,35 L11,33 L10,32 L10,24 L11,23 Z", + }, + mid: { + z: 20, + union: [ + { line: [[11, 29], [11, 30]] }, + { path: "M12,32 L19,34 L21,35 L21,36 L19,36 L18,35 L12,33 Z" }, + { path: "M17,32 L18,32 L18,33 L17,33 Z" }, + { rect: [22, 34, 5, 1] }, + { points: [[22, 37]] }, + ], + }, + umber: { + z: 30, + union: [ + { line: [[11, 31], [11, 32]] }, + { points: [[12, 33], [18, 36], [20, 37], [21, 37], [22, 38], [23, 38]] }, + { rect: [27, 34, 3, 1] }, + { path: "M19,32 L20,32 L20,33 L19,33 Z" }, + ], + }, + // chin ridge + hi: { + z: 40, + union: [ + { line: [[27, 35], [27, 37]] }, + { line: [[28, 35], [28, 36]] }, + { points: [[26, 37], [26, 38], [29, 37], [30, 38]] }, + ], + }, + // lower teeth + tooth: { + z: 50, + union: [ + { line: [[21, 32], [21, 33]] }, + { line: [[23, 32], [23, 33]] }, + { line: [[25, 32], [25, 33]] }, + { line: [[27, 32], [27, 33]] }, + { line: [[29, 32], [29, 33]] }, + ], + }, + // where the jaw meets the skull: soft shadow lines, not hard outline + seam: { + z: 110, + points: [[11, 22], [12, 23], [13, 25], [15, 26], [16, 27], [17, 30], [18, 30]], + }, + gum: { z: 110, line: [[19, 31], [30, 31]] }, + // lit front edge of the ramus, drawn over its own outline + edge: { + z: 120, + points: [[11, 23], [12, 24], [13, 26], [14, 26], [14, 27], [15, 27], [15, 28], [16, 28], [16, 29], [17, 31], [18, 31]], + }, + }, +} + +{ + type: "composition", + name: "skull", + size: [40, 40], + cell_size: [40, 40], + sprites: { C: "skull_cranium", J: "skull_jaw" }, + layers: [{ map: ["C"] }, { map: ["J"] }], +} + +{ + type: "composition", + name: "skull_talk_ajar", + size: [40, 40], + cell_size: [40, 40], + sprites: { C: "skull_cranium", J: "skull_jaw_ajar" }, + layers: [{ map: ["C"] }, { map: ["J"] }], +} + +{ + type: "composition", + name: "skull_talk_open", + size: [40, 40], + cell_size: [40, 40], + sprites: { C: "skull_cranium", J: "skull_jaw_open" }, + layers: [{ map: ["C"] }, { map: ["J"] }], +} + +// closed -> ajar -> open -> ajar, with a beat of silence +{ + type: "animation", + name: "skull_talking", + frames: ["skull", "skull_talk_ajar", "skull_talk_open", "skull_talk_ajar", "skull", "skull_talk_open", "skull_talk_ajar", "skull", "skull"], + duration: 110, + loop: true, +} diff --git a/examples/skull/skull_8x.png b/examples/skull/skull_8x.png new file mode 100644 index 00000000..5c1911d3 Binary files /dev/null and b/examples/skull/skull_8x.png differ diff --git a/examples/skull/skull_comparison.png b/examples/skull/skull_comparison.png new file mode 100644 index 00000000..9e597bcc Binary files /dev/null and b/examples/skull/skull_comparison.png differ diff --git a/examples/skull/skull_talking.gif b/examples/skull/skull_talking.gif new file mode 100644 index 00000000..2b5e5039 Binary files /dev/null and b/examples/skull/skull_talking.gif differ