diff --git a/examples/stiwi/README.md b/examples/stiwi/README.md new file mode 100644 index 0000000..f9cee7e --- /dev/null +++ b/examples/stiwi/README.md @@ -0,0 +1,48 @@ +# Stiwi: a hand-authored semantic portrait + +![48-pixel portrait of Stiwi](stiwi-hand-48-preview.png) + +Authored by **GPT-6.1-Sol running in Codex (model as declared by the +harness)**. This records the selected model; it is not an independent +attestation of the backend. Pixelsrc rendered the artwork. + +The model placed every palette colour and geometric region by hand while +looking at a photo reference. There was no image import, quantizer or tracing. +The reference photo is intentionally not included or linked here. + +The portrait went through **six revisions with human feedback**: softening the +side hair, curving the moustache, exposing a small philtrum, and closing a gap +in the right forelock. Named palette tokens, semantic roles and relationships, +and layered `rect`, `union`, `points`, `line` and `stroke` regions make those +changes editable rather than requiring a new pixel grid. + +| Variant | Editable source | Native render | Nearest-neighbour preview | +| --- | --- | --- | --- | +| Detailed, 48 × 48 | [source](stiwi-hand-48.pxl) | [PNG](stiwi-hand-48.png) | [528 × 528](stiwi-hand-48-preview.png) | +| Compact, 32 × 32 | [source](stiwi-hand-32.pxl) | [PNG](stiwi-hand-32.png) | [512 × 512](stiwi-hand-32-preview.png) | + +The 48-pixel portrait is independently authored, not an enlargement of the +32-pixel study. + +## A renderer bug found during the revisions + +A three-pixel forelock exposed that semantic `line` regions ignored their +`thickness`, including inside a `union`. [PR #25](https://github.com/scbrown/pixelsrc/pull/25) +fixed the renderer and added PNG/mask regression coverage. These examples +require a build containing that fix. + +## Validate and render + +From this directory, using `pxl` built from the repository: + +```sh +pxl validate --strict stiwi-hand-48.pxl +pxl validate --strict stiwi-hand-32.pxl +pxl render stiwi-hand-48.pxl --strict --scale 1 -o stiwi-hand-48.png +pxl render stiwi-hand-48.pxl --strict --scale 11 -o stiwi-hand-48-preview.png +pxl render stiwi-hand-32.pxl --strict --scale 1 -o stiwi-hand-32.png +pxl render stiwi-hand-32.pxl --strict --scale 16 -o stiwi-hand-32-preview.png +``` + +`cargo test --test stiwi_example` validates both sources strictly and compares +all four newly rendered PNGs with the saved artwork. diff --git a/examples/stiwi/stiwi-hand-32-preview.png b/examples/stiwi/stiwi-hand-32-preview.png new file mode 100644 index 0000000..f06455e Binary files /dev/null and b/examples/stiwi/stiwi-hand-32-preview.png differ diff --git a/examples/stiwi/stiwi-hand-32.png b/examples/stiwi/stiwi-hand-32.png new file mode 100644 index 0000000..8261655 Binary files /dev/null and b/examples/stiwi/stiwi-hand-32.png differ diff --git a/examples/stiwi/stiwi-hand-32.pxl b/examples/stiwi/stiwi-hand-32.pxl new file mode 100644 index 0000000..d8596e3 --- /dev/null +++ b/examples/stiwi/stiwi-hand-32.pxl @@ -0,0 +1,86 @@ +// Stiwi, hand-drawn from the GitHub photograph. 32px icon study. +// Every contour, highlight and grey beard strand below is an authored shape. +// No raster input, import, quantizer, generated grid or per-pixel tracing. +{ + type: "palette", name: "stiwi-hand-32", + colors: { + background: "#acb894", "background-light": "#dce3cd", "background-leaf": "#83976c", + shirt: "#263d60", "shirt-shadow": "#1b2a45", collar: "#3d5475", + hair: "#302e30", "hair-swoop": "#302e30", "hair-shadow": "#24252a", "hair-wave": "#514a48", "hair-silver": "#8b8580", + skin: "#bc947b", "skin-under-nose": "#bc947b", "skin-light": "#d5af91", "skin-shadow": "#966f60", "skin-warm": "#c49a7d", + beard: "#393435", "beard-shadow": "#292a2d", "beard-mid": "#615b56", "beard-grey": "#96958a", "beard-white": "#c5c2b3", + moustache: "#4c4038", "moustache-grey": "#96958a", mouth: "#664b40", lip: "#ae816b", teeth: "#d9c8a6", + "eye-white": "#c9bcaa", iris: "#626963", pupil: "#343738", + frame: "#2c3036", "frame-light": "#7b7e78", "lens-glint": "#b0b9ab" + }, + roles: { + background: "fill", "background-light": "highlight", "background-leaf": "shadow", + shirt: "fill", "shirt-shadow": "shadow", collar: "boundary", + hair: "fill", "hair-swoop": "fill", "hair-shadow": "shadow", "hair-wave": "highlight", "hair-silver": "highlight", + skin: "fill", "skin-under-nose": "fill", "skin-light": "highlight", "skin-shadow": "shadow", "skin-warm": "fill", + beard: "fill", "beard-shadow": "shadow", "beard-mid": "highlight", "beard-grey": "highlight", "beard-white": "highlight", + moustache: "anchor", "moustache-grey": "highlight", mouth: "anchor", lip: "fill", teeth: "anchor", + "eye-white": "fill", iris: "anchor", pupil: "anchor", frame: "boundary", "frame-light": "highlight", "lens-glint": "highlight" + }, + relationships: { + "skin-light": {type: "derives-from", target: "skin"}, + "skin-shadow": {type: "derives-from", target: "skin"}, + "hair-wave": {type: "derives-from", target: "hair"}, + "beard-grey": {type: "derives-from", target: "beard"}, + "shirt-shadow": {type: "derives-from", target: "shirt"} + } +} +{ + type: "sprite", name: "stiwi-hand-32", size: [32,32], palette: "stiwi-hand-32", + regions: { + background: {rect: [0,0,32,32], z: 0}, + "background-light": {union: [{rect: [0,0,9,10], round: 3},{rect: [25,0,7,16], round: 3},{rect: [0,18,5,8], round: 2}], z: 1}, + "background-leaf": {union: [{rect: [0,11,5,7], round: 2},{rect: [28,21,4,7], round: 2}], z: 2}, + shirt: {union: [{rect: [0,30,32,2]},{rect: [3,28,26,4]},{rect: [7,26,18,6]}], z: 5}, + "shirt-shadow": {union: [{rect: [8,26,16,6]},{line: [[1,31],[6,29],[8,28]]}], z: 6}, + collar: {union: [{line: [[6,28],[10,27],[13,31]]},{line: [[25,28],[22,27],[20,31]]},{stroke: [10,29,12,3]}], z: 7}, + // Wavy crown with a high, off-centre swept forelock and long side hair. + hair: {union: [{rect: [9,2,14,10], round: 4},{rect: [13,1,8,7], round: 3},{rect: [8,4,17,9], round: 3}, + // Alternate one-pixel curls and notches; narrow locks finish at the ears. + {rect: [7,7,5,12]},{rect: [6,8,3,3]},{rect: [5,11,4,2]},{rect: [6,13,3,3]},{rect: [5,16,4,2]},{rect: [6,18,3,2]},{rect: [8,20,2,2]}, + {rect: [22,8,4,11]},{rect: [25,9,2,2]},{rect: [24,11,4,2]},{rect: [25,14,2,2]},{rect: [24,16,4,2]},{rect: [24,18,2,2]},{rect: [23,20,2,1]}, + {points: [[5,14],[4,16],[27,15],[26,19]]}], z: 10}, + "hair-shadow": {union: [{line: [[7,10],[6,12],[7,15],[6,17],[8,20]]},{line: [[24,11],[26,12],[25,15],[26,17],[24,20]]},{rect: [17,3,4,6], round: 2}], z: 11}, + // Exposed forehead, cheeks and small ear lobes. + skin: {union: [{rect: [9,7,14,17], round: 4},{rect: [8,13,2,6], round: 1},{rect: [23,13,2,6], round: 1}], z: 20}, + "skin-light": {union: [{rect: [11,8,7,4], round: 1},{rect: [10,16,3,3], round: 1},{line: [[16,14],[16,17]]}], z: 21}, + "skin-shadow": {union: [{line: [[22,10],[22,17],[23,19]], thickness: 1},{line: [[9,17],[10,20]]},{points: [[8,15],[24,15],[18,17],[15,18]]}], z: 22}, + "skin-warm": {union: [{rect: [13,16,3,3]},{rect: [17,16,3,3]},{points: [[16,18],[17,17]]}], z: 23}, + // An extra right-hand forelock covers the bare upper temple. + "hair-swoop": {line: [[20,7],[21,9],[23,11],[25,11]], thickness: 3, z: 24}, + // Hair sweeps over the forehead without flattening the open face. + "hair-wave": {union: [{line: [[8,9],[9,6],[12,4],[15,4]]},{line: [[14,3],[16,2],[19,3],[20,6],[22,8],[25,8]]},{line: [[8,12],[7,13],[8,15]]},{line: [[7,17],[8,19]]},{line: [[25,13],[26,14],[25,16],[26,17],[24,19]]},{line: [[21,8],[22,9],[24,10]]}], z: 25}, + "hair-silver": {union: [{line: [[8,8],[9,6],[11,5]]},{points: [[13,3],[18,2],[22,7],[6,12],[7,16],[8,19],[26,12],[25,17],[24,20]]}], z: 26}, + "eye-white": {union: [{rect: [11,13,3,1]},{rect: [19,13,3,1]}], z: 30}, + iris: {points: [[12,13],[20,13]], z: 31}, + pupil: {points: [[12,14],[20,14]], z: 32}, + // Full beard tapers below the jaw; it is not a dark rectangular mouth. + beard: {union: [{rect: [8,18,16,7], round: 2},{rect: [9,23,14,5], round: 2},{rect: [11,27,10,3]},{rect: [13,29,6,2]}], z: 40}, + "beard-shadow": {union: [{line: [[9,20],[9,24],[12,28],[14,30]]},{line: [[23,20],[23,24],[21,28],[18,30]]},{rect: [14,26,4,3]}], z: 41}, + "beard-mid": {union: [{line: [[10,22],[11,25],[13,27]]},{line: [[22,22],[21,25],[20,27]]},{line: [[13,23],[14,26],[14,29]]},{line: [[18,23],[18,26],[17,29]]}], z: 42}, + "beard-grey": {union: [{line: [[12,24],[13,26],[13,29]]},{line: [[16,24],[15,27],[16,30]]},{line: [[20,23],[19,26],[19,28]]},{points: [[10,21],[22,21],[11,24],[21,25],[18,29]]}], z: 43}, + "beard-white": {points: [[13,25],[14,28],[16,26],[16,29],[18,27],[20,24]], z: 44}, + // Restore rev 3 cheeks; retain only a one-pixel central philtrum. + "skin-under-nose": {rect: [16,18,1,2], z: 49}, + // Open smile, modest teeth and moustache parted around the nose. + mouth: {union: [{rect: [12,20,9,2], round: 1},{line: [[12,20],[14,22],[18,22],[21,20]]}], z: 50}, + teeth: {rect: [15,21,3,1], z: 51}, + lip: {line: [[14,22],[18,22],[20,21]], z: 52}, + moustache: {union: [ + {rect: [13,18,2,2]},{rect: [18,18,2,2]}, + {line: [[13,19],[11,20],[10,22],[9,23]]}, + {line: [[19,19],[21,20],[21,22],[22,23]]}, + {points: [[12,19],[11,21],[10,23],[20,19],[22,21],[21,23]]}, + {rect: [12,20,9,1]}], z: 53}, + "moustache-grey": {points: [[13,18],[11,20],[10,22],[19,19],[21,21]], z: 54}, + // Thin round metal frames, rather than thick square sunglasses. + frame: {union: [{line: [[8,13],[9,11],[12,11],[14,13],[14,15],[12,17],[10,17],[8,15],[8,13]]},{line: [[18,13],[19,11],[22,11],[24,13],[24,15],[22,17],[20,17],[18,15],[18,13]]},{line: [[15,12],[17,12]]},{line: [[6,12],[8,12]]},{line: [[25,12],[26,13]]}], z: 60}, + "frame-light": {points: [[10,17],[11,17],[20,17],[21,17],[8,14],[24,14]], z: 61}, + "lens-glint": {points: [[10,12],[19,12]], z: 62} + } +} diff --git a/examples/stiwi/stiwi-hand-48-preview.png b/examples/stiwi/stiwi-hand-48-preview.png new file mode 100644 index 0000000..db05230 Binary files /dev/null and b/examples/stiwi/stiwi-hand-48-preview.png differ diff --git a/examples/stiwi/stiwi-hand-48.png b/examples/stiwi/stiwi-hand-48.png new file mode 100644 index 0000000..1db1b76 Binary files /dev/null and b/examples/stiwi/stiwi-hand-48.png differ diff --git a/examples/stiwi/stiwi-hand-48.pxl b/examples/stiwi/stiwi-hand-48.pxl new file mode 100644 index 0000000..bbed3d2 --- /dev/null +++ b/examples/stiwi/stiwi-hand-48.pxl @@ -0,0 +1,101 @@ +// Stiwi, hand-authored 48px portrait. Independently placed facial features, +// not a scaled grid of the 32px study. Photograph used only by eye. +{ + type: "palette", name: "stiwi-hand-48", + colors: { + background: "#adba99", "background-sky": "#dce5db", "background-glow": "#c8ce9f", "background-leaf": "#8d9f77", "background-deep": "#798d6a", + shirt: "#2c4266", "shirt-light": "#3e577a", "shirt-shadow": "#1e304e", collar: "#233655", "collar-edge": "#526582", + hair: "#302e31", "hair-shadow": "#25262c", "hair-forelock": "#3d3639", "hair-wave": "#59504d", "hair-silver": "#8e8680", + skin: "#bb937a", "skin-under-nose": "#bb937a", "skin-light": "#d6b397", "skin-shadow": "#967461", "skin-warm": "#c49b7e", "nose-shadow": "#806252", + beard: "#393436", "beard-shadow": "#2c2c30", "beard-mid": "#605b55", "beard-grey": "#96958b", "beard-white": "#c7c4b5", + moustache: "#50443c", "moustache-grey": "#8d8575", mouth: "#5c453c", lip: "#aa7d67", teeth: "#d9c9ac", + brow: "#615249", "eye-white": "#c6b9a6", iris: "#757d74", pupil: "#363a3b", + frame: "#292e35", "frame-light": "#777b79", "lens-glint": "#b8c0b2" + }, + roles: { + background: "fill", "background-sky": "fill", "background-glow": "highlight", "background-leaf": "shadow", "background-deep": "shadow", + shirt: "fill", "shirt-light": "highlight", "shirt-shadow": "shadow", collar: "fill", "collar-edge": "boundary", + hair: "fill", "hair-shadow": "shadow", "hair-forelock": "fill", "hair-wave": "highlight", "hair-silver": "highlight", + skin: "fill", "skin-under-nose": "fill", "skin-light": "highlight", "skin-shadow": "shadow", "skin-warm": "fill", "nose-shadow": "shadow", + beard: "fill", "beard-shadow": "shadow", "beard-mid": "highlight", "beard-grey": "highlight", "beard-white": "highlight", + moustache: "anchor", "moustache-grey": "highlight", mouth: "anchor", lip: "fill", teeth: "anchor", + brow: "anchor", "eye-white": "fill", iris: "anchor", pupil: "anchor", frame: "boundary", "frame-light": "highlight", "lens-glint": "highlight" + }, + relationships: { + "skin-light": {type: "derives-from", target: "skin"}, + "skin-shadow": {type: "derives-from", target: "skin"}, + "hair-wave": {type: "derives-from", target: "hair"}, + "hair-silver": {type: "derives-from", target: "hair"}, + "beard-grey": {type: "derives-from", target: "beard"}, + "beard-white": {type: "derives-from", target: "beard"}, + "shirt-light": {type: "derives-from", target: "shirt"} + } +} +{ + type: "sprite", name: "stiwi-hand-48", size: [48,48], palette: "stiwi-hand-48", + regions: { + // Broad quiet garden patches suggest the photograph's blurred background. + background: {rect: [0,0,48,48], z: 0}, + "background-sky": {union: [{rect: [0,0,14,12], round: 4},{rect: [36,0,12,22], round: 5},{rect: [0,20,7,11], round: 3}], z: 1}, + "background-glow": {union: [{rect: [0,10,9,10], round: 3},{rect: [39,24,9,13], round: 4},{rect: [2,34,9,8], round: 3}], z: 2}, + "background-leaf": {union: [{rect: [0,26,6,10], round: 2},{rect: [41,14,7,8], round: 3},{rect: [35,34,8,9], round: 3}], z: 3}, + "background-deep": {union: [{rect: [0,37,5,6], round: 2},{rect: [44,36,4,8], round: 1}], z: 4}, + // Navy shoulders and a stand collar sit behind the long beard. + shirt: {union: [{rect: [0,45,48,3]},{rect: [3,43,42,5]},{rect: [8,41,32,7]},{rect: [14,39,20,9]}], z: 8}, + "shirt-light": {union: [{line: [[0,45],[8,43],[13,41]], thickness: 1},{line: [[35,42],[42,44],[47,46]]},{points: [[5,45],[8,44],[38,44],[41,45]]}], z: 9}, + "shirt-shadow": {union: [{rect: [15,40,18,8]},{line: [[4,46],[9,44],[12,42]]}], z: 10}, + collar: {union: [{rect: [13,39,5,7], round: 1},{rect: [30,39,5,7], round: 1}], z: 11}, + "collar-edge": {union: [{line: [[13,40],[14,44],[18,47]]},{line: [[34,40],[33,44],[30,47]]},{stroke: [17,44,14,4]}], z: 12}, + // Asymmetric hair silhouette: higher forelock on the viewer's right. + hair: {union: [{rect: [15,3,18,13], round: 5},{rect: [20,1,11,10], round: 4},{rect: [11,5,13,14], round: 4},{rect: [30,6,7,13], round: 3}, + // Left: outward curls alternate with green notches, then taper at the ear. + {rect: [11,12,5,15]},{rect: [9,13,4,4]},{rect: [8,17,5,3]},{rect: [9,20,4,3]},{rect: [7,22,6,2]},{rect: [9,24,5,2]},{rect: [10,26,4,2]},{rect: [12,28,2,2]}, + {line: [[9,16],[7,18],[8,19]]},{line: [[9,23],[8,25],[10,26]]}, + // Right: swept locks kick out at different heights, never a flat side wall. + {rect: [32,12,5,14]},{rect: [36,12,4,3]},{rect: [37,15,4,3]},{rect: [36,18,4,3]},{rect: [37,21,3,2]},{rect: [35,23,4,3]},{rect: [34,26,4,2]},{rect: [34,28,2,2]}, + {line: [[39,16],[41,15]]},{line: [[38,22],[40,21]]},{line: [[36,27],[37,28]]}], z: 20}, + "hair-shadow": {union: [{line: [[12,14],[10,17],[11,20],[9,23],[11,25],[12,28]]},{line: [[35,16],[38,18],[37,20],[38,22],[36,25],[35,28]]},{rect: [25,4,5,9], round: 2}], z: 21}, + // Warm face planes. The forehead stays open beneath the swept hair. + skin: {union: [{rect: [14,10,21,24], round: 5},{rect: [12,20,3,9], round: 1},{rect: [34,20,3,9], round: 1}], z: 30}, + "skin-light": {union: [{rect: [17,11,9,6], round: 2},{rect: [15,25,5,4], round: 2},{line: [[24,21],[24,26]], thickness: 1},{points: [[22,12],[20,13],[25,28]]}], z: 31}, + "skin-shadow": {union: [{rect: [32,17,3,12], round: 1},{line: [[14,23],[14,28],[16,31]], thickness: 1},{line: [[27,14],[29,17],[31,19]]},{points: [[12,23],[13,25],[35,23],[35,26],[20,17]]}], z: 32}, + "skin-warm": {union: [{rect: [19,25,4,4], round: 1},{rect: [27,25,4,4], round: 1},{line: [[23,26],[25,28],[27,27]], thickness: 1},{points: [[17,16],[18,14],[21,12],[29,26]]}], z: 33}, + "nose-shadow": {union: [{line: [[26,23],[27,26],[29,27],[27,28]]},{points: [[22,28],[23,29],[26,29]]}], z: 34}, + // A hand-placed forelock sweeps from the crown across the right temple. + "hair-forelock": {union: [{points: [[32,10],[33,10],[34,10],[33,11],[34,11],[33,12],[34,12],[34,13]]},{rect: [27,5,5,9], round: 2},{line: [[29,9],[31,13],[35,16],[38,16]], thickness: 3},{line: [[16,8],[14,11],[13,16]], thickness: 1},{line: [[29,10],[30,13],[32,16],[35,18]], thickness: 3},{line: [[35,18],[37,17],[38,16]]}], z: 36}, + "hair-wave": {union: [{line: [[12,13],[13,9],[17,6],[21,6]]},{line: [[18,4],[21,3],[25,3],[28,5]]},{line: [[29,7],[30,11],[33,14],[37,15]]},{line: [[11,15],[10,17],[11,19]]},{line: [[10,21],[9,23],[11,24],[12,27]]},{line: [[37,16],[39,17],[37,19]]},{line: [[37,21],[38,22],[36,24],[36,26],[35,28]]},{line: [[31,12],[32,14],[35,16],[37,16]]}], z: 37}, + "hair-silver": {union: [{line: [[13,10],[15,7],[18,6]]},{line: [[20,3],[23,2],[26,3]]},{line: [[32,13],[35,15],[38,14]]},{line: [[10,24],[11,25],[12,27]]},{points: [[10,17],[9,19],[14,9],[17,5],[29,5],[38,17],[37,23],[36,26]]}], z: 38}, + // Gentle brows and grey-green eyes; bright pixels are intentionally sparse. + brow: {union: [{line: [[15,18],[18,17],[21,18]]},{line: [[28,18],[30,17],[33,18]]}], z: 40}, + "eye-white": {union: [{line: [[16,21],[18,20],[20,21]]},{line: [[28,21],[30,20],[32,21]]}], z: 41}, + iris: {union: [{rect: [18,20,2,2]},{rect: [29,20,2,2]}], z: 42}, + pupil: {points: [[18,21],[29,21]], z: 43}, + // Full beard silhouette, progressively stepping inward at the bottom. + beard: {union: [{rect: [12,28,24,8], round: 3},{rect: [13,34,22,5], round: 2},{rect: [15,38,18,4]},{rect: [17,41,14,3]},{rect: [20,44,8,2]}], z: 50}, + "beard-shadow": {union: [{line: [[13,30],[13,35],[16,40],[20,44]], thickness: 1},{line: [[35,30],[34,36],[31,41],[27,45]], thickness: 1},{line: [[23,36],[23,40],[25,44]], thickness: 1}], z: 51}, + "beard-mid": {union: [{line: [[15,31],[16,35],[18,39],[20,42]]},{line: [[32,32],[31,36],[29,39]]},{line: [[19,34],[20,38],[21,41]]},{line: [[27,34],[27,38],[26,42]]},{points: [[16,33],[18,37],[30,38],[28,42],[22,43],[24,45]]}], z: 52}, + // Grey-white streaks are irregular short polylines, not a solid grey chin. + "beard-grey": {union: [{line: [[17,33],[18,36],[20,39],[20,42]]},{line: [[21,35],[21,38],[22,40],[21,43]]},{line: [[26,35],[25,38],[26,41],[25,44]]},{line: [[31,33],[30,36],[29,38],[29,41]]},{line: [[15,34],[16,37]]},{points: [[14,31],[18,32],[33,31],[32,36],[18,40],[23,44],[27,43],[31,39]]}], z: 53}, + "beard-white": {union: [{line: [[19,35],[20,37]]},{line: [[22,39],[23,42]]},{line: [[26,37],[27,39]]},{line: [[29,35],[28,37]]},{points: [[17,35],[18,38],[20,40],[21,42],[24,44],[25,40],[27,42],[30,38]]}], z: 54}, + // Rev 3 face planes are restored; only a small central philtrum is open. + "skin-under-nose": {rect: [24,28,2,2], z: 55}, + // Smiling mouth with a broken warm ivory row, under a parted moustache. + mouth: {union: [{rect: [19,30,12,3], round: 1},{line: [[18,30],[20,33],[25,34],[29,33],[32,30]]}], z: 60}, + teeth: {rect: [23,31,4,1], z: 61}, + lip: {line: [[21,33],[24,34],[27,33],[29,32]], z: 62}, + moustache: {union: [ + // Restore rev 3 contours and old one-pixel strand appearance explicitly. + {rect: [21,28,3,2]},{rect: [26,28,3,2]}, + {line: [[22,29],[20,29],[18,31],[17,33]], thickness: 1}, + {line: [[27,29],[29,30],[31,32]], thickness: 1}, + {line: [[17,32],[17,34],[16,35]]},{line: [[31,32],[32,34],[33,35]]}, + {points: [[19,32],[18,34],[16,33],[30,33],[32,32],[31,35]]}, + // Brown overhang leaves only the small ivory sliver below it. + {rect: [20,30,10,1]}], z: 63}, + "moustache-grey": {union: [{line: [[21,29],[19,30]]},{line: [[28,30],[29,31]]},{line: [[18,32],[17,33]]},{points: [[22,28],[27,29],[31,33]]}], z: 64}, + // Thin rounded metal frames; eyes remain legible inside the lenses. + frame: {union: [{line: [[13,21],[14,19],[16,18],[20,18],[22,19],[23,21],[23,23],[22,25],[20,26],[16,26],[14,25],[13,23],[13,21]]},{line: [[26,21],[27,19],[29,18],[33,18],[35,19],[36,21],[36,23],[35,25],[33,26],[29,26],[27,25],[26,23],[26,21]]},{line: [[23,20],[26,20]]},{line: [[10,19],[13,19]]},{line: [[36,19],[38,21]]}], z: 70}, + "frame-light": {union: [{line: [[16,26],[20,26]]},{line: [[29,26],[33,26]]},{points: [[13,22],[14,24],[36,22],[35,24],[24,20]]}], z: 71}, + "lens-glint": {union: [{line: [[15,20],[16,19]]},{line: [[28,20],[29,19]]}], z: 72} + } +} diff --git a/tests/stiwi_example.rs b/tests/stiwi_example.rs new file mode 100644 index 0000000..e53e814 --- /dev/null +++ b/tests/stiwi_example.rs @@ -0,0 +1,35 @@ +//! Keep the editable portrait sources and published renders in agreement. +use std::path::Path; +use std::process::Command; + +#[test] +fn stiwi_sources_validate_and_match_saved_renders() { + let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/stiwi"); + let output = tempfile::tempdir().expect("temporary render directory"); + for (size, preview_scale) in [(48, 11), (32, 16)] { + let stem = format!("stiwi-hand-{size}"); + let source = examples.join(format!("{stem}.pxl")); + let validate = Command::new(env!("CARGO_BIN_EXE_pxl")) + .args(["validate", "--strict"]) + .arg(&source) + .output() + .expect("run strict validator"); + assert!(validate.status.success(), "{stem}: {validate:?}"); + for (scale, suffix) in [(1, ".png"), (preview_scale, "-preview.png")] { + let name = format!("{stem}{suffix}"); + let rendered = output.path().join(&name); + let result = Command::new(env!("CARGO_BIN_EXE_pxl")) + .arg("render") + .arg(&source) + .args(["--strict", "--scale", &scale.to_string(), "-o"]) + .arg(&rendered) + .output() + .expect("run renderer"); + assert!(result.status.success(), "{name}: {result:?}"); + let actual = image::open(rendered).expect("read new PNG").into_rgba8(); + let expected = image::open(examples.join(&name)).expect("read saved PNG").into_rgba8(); + assert_eq!(actual.dimensions(), (size * scale, size * scale), "{name}"); + assert_eq!(actual, expected, "{name}: source and saved pixels differ"); + } + } +}