Build guide · Wave 39 · Industry Demos XXVIII · site 352
How the Blackwattle Tattoo demo was built
A fictional private-studio custom tattoo shop on William Street, Northbridge, built as a single conversion page for a first-timer with a reference photo, a collector booking her next sitting, the bloke who wants the old name covered, and the pair who want matching fine-line. One job: a consult request that already carries the placement, size, style and extras — priced.
Concept
Blackwattle Tattoo is the wave's dark site. The direction is the studio after the last client: ink-black ground, bone ink, ox-blood calls to action, brass linework for the machine frame and lamp arms. Sharp, quiet, unhurried — a clean bench under a lamp. The signature is the Placement Map: a drawn standing figure (front and back by chip, the view flips with a drawn turn), placement chips that light the zone in ox-blood, a stencil scaled honestly to the chosen size on the drawn limb, the design redrawn in the chosen style, a drawn machine and hand tracing the stencil on a loop, and the studio's booking card pinned beside the figure carrying the sitting price. Every chip pre-fills the booking form.
Palette — measured, not assumed
All figures below are WCAG contrast ratios measured from the shipped hex tokens during Pass 1 (the spec's ratios were hypotheses).
Ox-blood copy on the ground fails (2.5:1), exactly as the spec warned, so it never carries text: the ledger notes and the needle trace use the pre-minted #E27068. Brass small copy passes on both grounds so the lighter brass-ink is used for links only. The booking card and confirmation panel invert to bone paper with ink-black type (14.9:1) — that's the stencil-paper move that lets a black-outline design read as a real stencil.
Type
DM Serif Display (400) for the wordmark, headings, the display line and the prices — a high-contrast serif with a shop-sign spine, and it makes a "$540" look like something painted on the studio window. Inter for everything else, 17px on mobile and 18px from 760px. The display moment is the piece set huge in caps above the map — A5 ON THE FOREARM, FINE‑LINE, THREE HOURS — with the style in brass italic; the hours are spelled as words from the pinned model (half an hour, an hour and a half, six hours…) so the line always reads as a sentence. Non-breaking hyphens keep FINE‑LINE and COIN‑SIZE from splitting at a line end.
Techniques
- Every pixel procedural: inline SVG for the figure, stencil, machine and metro map; CSS gradients for the lamp glow on the stage; no libraries beyond Google Fonts.
- Front and back figures share one outline path with a detail layer swapped per view (collarbone and kneecaps front; spine, shoulder blades and the small of the back behind).
- Two redraw speeds (W21): the first paint staggers the zone, paper, trace and machine in over ~1.5 s; every chip after that redraws in ~560 ms via a
--dduration multiplier on the SVG. - SVG positioning and CSS entrance animation live on separate nested groups (W16/W17): outer
<g transform>for position, inner group for the opacity/translate transition — the first build had them on one element and the CSS transform silently discarded the attribute (Pass 1 caught it: the stencil and machine sat at the origin). - rAF loop with
dt = min(max(now − last, 0)/1000, .05), paused ondocument.hidden, token-invalidated in every chip handler before the state swap (W25); reduced motion renders one settled frame with the machine parked at 35% of the trace.
The Placement Map — how the signature works
The figure is drawn once at ~583 px for a 1750 mm person, so 0.333 px per millimetre is the scale that makes the stencil honest: a coin is a 25 mm circle (8 px), a palm 80 × 80 mm, a hand-size piece 100 × 150 mm, A5 148 × 210 mm, A4 210 × 297 mm — the dashed stencil rectangle is those millimetres times the scale, rotated to the limb's axis, and the size tag under it prints the real dimensions with the pinned hours. It's a flat projection: a big piece overhangs a thin limb on the drawing the way an A4 wraps a real forearm. The stencil paper is bone at 86% so the skin shows through, which lets the cover-up toggle draw a faded old mark (a script-and-star squiggle) under the paper with the new design's strokes 1.6× heavier over it. Four paints of one rose: fine-line is hairline black outlines; traditional & blackwork is a bold outline with the leaves filled solid; colour is the same rose with ox-blood petals, teal leaves, a brass stem and a red spiral accent; realism drops the outline for radial-gradient shading. The machine is an upright coil machine (tube, L-frame, two coils, spring) with the artist's fist drawn around the grip, leaned 34° so the needle points to the trace head; each frame the trace head's position on the stencil perimeter (getPointAtLength on a pathLength="1000" path, mapped through getScreenCTM) becomes the machine's translate, with a 0.5 px needle buzz and an opacity breath between 0.85 and 1 — never below the 0.55 floor. Hard placements (ribs, hand & neck) draw a "+1 hr" tag beside the zone label; labels sit left of the figure, the size tag under the stencil, and the booking card is an HTML card pinned top-right so nothing collides with anything at any of the three widths.
Pricing model (pinned anchors)
| Anchor | Value | Semantic |
|---|---|---|
| Size → hours | coin ½ h · palm 1 h · hand 2 h · A5 3 h · A4 5 h | real dimensions printed on the size tag |
| Style → rate | fine-line $180/h · traditional & blackwork $180/h · colour $200/h · realism $220/h | sitting = total hours × rate; every rate is even so ½ h is a whole dollar |
| Hard placement | ribs, hand & neck: +1 h at the style rate | NOTE "ribs, hands and necks add an hour — the skin's tougher and slower, and we don't rush it" + drawn "+1 hr" tag; absent elsewhere |
| Cover-up / rework | +2 h at the style rate, live at every combination | NOTE template "cover-up — 2 hours added at $<rate>/h"; the old mark is drawn under the stencil |
| Shop minimum | $150 per sitting on the whole sitting price | NOTE "billed at the $150 shop minimum — needles, set-up and sterile everything cost the same for a coin as a palm"; only coin × any style with no hard placement and no cover-up falls under it |
| Sittings | total hours > 5 | NOTE only, no price change: "over five hours we split it across sittings — same total, priced per sitting" |
| Deposit | $100 booking deposit comes off the sitting | declaration on badge, ledger and a11y mirror — never a line item, never subtracted |
| Included | consult, drawing, stencil, one free touch-up within three months | declared on-card, never line items |
| Quoted at consultation | sleeves, back pieces, large cover-ups, hand-poke; flash days flat-priced on the day | designed price-less lines in the grid and FAQ |
Worked examples — all measured from the running page and cross-checked against a separately written oracle
| Scenario | Arithmetic | Measured | Notes asserted |
|---|---|---|---|
| Rest — forearm, A5, fine-line, cover-up off, front | 3 × 180 | $540 | none; deposit declaration on all three surfaces |
| Coin, forearm, fine-line | 0.5 × 180 = 90 → minimum | $150 | minimum note PRESENT (grid floor) |
| Coin, ribs, fine-line | 1.5 × 180 | $270 | minimum ABSENT, hard note + "+1 hr" tag PRESENT |
| Hand, calf, colour | 2 × 200 | $400 | none |
| Hand, calf, realism, cover-up | 4 × 220 | $880 | "cover-up — 2 hours added at $220/h" |
| A4, forearm, traditional | 5 × 180 | $900 | sittings ABSENT (5 is not > 5) |
| A4, ribs, realism | 6 × 220 | $1,320 | hard + sittings PRESENT |
| Rest + cover-up | 5 × 180 | $900 | sittings ABSENT |
| Rest + cover-up on ribs | 6 × 180 | $1,080 | sittings PRESENT (boundary both sides) |
| Rest → colour / realism / traditional | 3 × 200 / 220 / 180 | $600 / $660 / $540 | none |
| Back view, back placement, A5 fine-line | 3 × 180 | $540 | placement is price-inert except hard zones |
| Extra checks: coin hand & neck realism · palm chest colour · coin thigh colour · coin upper arm realism | 1.5 × 220 · 1 × 200 · 100 → min · 110 → min | $330 · $200 · $150 · $150 | as pinned |
Badge short forms below 640 px (ledger and mirror always carry the full template): hard → "+1 h — ribs, hands and necks"; minimum → "billed at the $150 shop minimum"; sittings → "split across sittings — same total"; the cover-up template is short enough to stay whole; deposit → "$100 deposit off the sitting".
Builder decisions the spec left open. (1) The view chip: chest is only drawn on the front and back only on the back, so choosing chest FORCES the front view and choosing back FORCES the back view (with the drawn turn); while a one-sided placement is selected the other view chip is DISABLED with the on-page reason "the chest is only drawn on the front view — pick another placement to turn the figure". Moving to a two-sided placement re-enables both chips and keeps whichever view is showing (no reset). (2) The hand & neck stencil is centred on the back of the hand at every size, and the neck is highlighted as a second dashed zone; a big stencil overhangs the drawn hand — honest, since an A5 doesn't fit on a hand. (3) The stencil is a flat projection of the real millimetres; wrapping isn't drawn.
Conversion decisions
- Above the fold at 375: wordmark, "Custom tattoo studio in Northbridge — drawn for you, once", the ox-blood Book a consult button, the call button and three trust chips all sit inside the first viewport; the map comes next. Header carries a
tel:link (icon-only under 460 px so the masthead never collides) and a fixed call/book bar shows under 760 px. - The map above the controls: the hero shows the priced scene first, then "Build the piece" — a first-timer sees a real number before touching a chip. The booking card lives inside the artwork so the 1200 × 750 thumbnail carries the price.
- Form fields: name, mobile, suburb, preferred artist (any / fine-line / traditional & blackwork / colour & realism — pre-selected from the style chip until the visitor changes it) and one textarea pre-filled with the map's placement, size, style, cover-up, hours, estimate and the deposit declaration, followed by "My idea / reference:" — the studio is drawing from the first phone call. Validation is front-end only, focus-visible rings in brass, and the confirmation panel is hidden until a valid filled submit; nothing is sent.
- Ledger + mirror: the itemised sitting estimate sits beside the chips, and the map is mirrored as a plain-text list under the stage so a screen reader gets the same booking in words (view, zone, stencil, style, notes, total, deposit).
- Honest price-less lines: sleeves and back pieces are "quoted at consultation" on the grid, in the ledger fine print and in the FAQ; flash days are "flat-priced on the day, posted the week before". Age 18+ with photo ID is stated as the WA rule it is.
Local SEO we'd implement for the real business
The live page carries only WebPage / WebSite / BreadcrumbList schema naming it a demonstration. For the real studio we'd ship this (their real reviews, hours and geo), plus a Google Business Profile with the same NAP, per-artist portfolio pages, and an aftercare page that answers the questions in the FAQ:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TattooParlor",
"name": "Blackwattle Tattoo",
"image": "https://blackwattletattoo.example/studio.jpg",
"url": "https://blackwattletattoo.example/",
"telephone": "+61 8 9000 0352",
"priceRange": "$150 – $1,500",
"address": {
"@type": "PostalAddress",
"streetAddress": "William Street",
"addressLocality": "Northbridge",
"addressRegion": "WA",
"postalCode": "6003",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.946, "longitude": 115.859 },
"openingHoursSpecification": [
{ "@type": "OpeningHoursSpecification", "dayOfWeek": ["Tuesday","Wednesday","Friday","Saturday"], "opens": "10:00", "closes": "18:00" },
{ "@type": "OpeningHoursSpecification", "dayOfWeek": "Thursday", "opens": "10:00", "closes": "20:00" }
],
"areaServed": ["Northbridge","Perth","Leederville","Mount Lawley","Subiaco","Victoria Park","Fremantle"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Tattoo work",
"itemListElement": [
{ "@type": "Offer", "itemOffered": { "@type": "Service", "name": "Custom fine-line" }, "priceSpecification": { "@type": "UnitPriceSpecification", "price": "180", "priceCurrency": "AUD", "unitText": "hour" } },
{ "@type": "Offer", "itemOffered": { "@type": "Service", "name": "Colour work" }, "priceSpecification": { "@type": "UnitPriceSpecification", "price": "200", "priceCurrency": "AUD", "unitText": "hour" } },
{ "@type": "Offer", "itemOffered": { "@type": "Service", "name": "Realism" }, "priceSpecification": { "@type": "UnitPriceSpecification", "price": "220", "priceCurrency": "AUD", "unitText": "hour" } }
]
},
"aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "184" },
"review": [
{
"@type": "Review",
"author": { "@type": "Person", "name": "Priya" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "My first one — the forearm was the easy spot, talked through every pass, stopped whenever I asked."
}
]
}
</script>
What would change for the real client
- Bookings and deposits: the form posts into Square Appointments or Fresha (per-artist calendars, the $100 deposit taken at booking, reschedule rules) with the map's scenario carried in the booking note; a real-time "next available with this artist" strip replaces the promise copy.
- Portfolio: the studio's Instagram feed per artist replaces the demo reviews grid's neighbour, and each style chip links to that artist's healed work; the drawn map stays as the estimator.
- Reviews: Google Business Profile reviews feed the reviews strip, and the real
TattooParlor+Reviewschema above goes live. - Accounts: deposits and sittings reconcile into Xero; flash-day sales through Square.
- Rate card as data: hours per size, rates per style, hard-placement and cover-up loadings and the shop minimum become a small config the studio edits, so the map, ledger and grid can't disagree.
Iteration log
Pass 1 — Craft
- Drove every chip, the cover-up toggle, both views, the form empty and filled, and read the badge, ledger and mirror at rest: $540 measured, three surfaces agreeing, no notes.
- First render had the stencil paper and the machine at the SVG origin: the entrance class put a CSS
transformon the same group that carried the positioningtransformattribute (the W16 collapse). Split into outer position group + inner animated group; re-verified the machine's transform is on the stencil edge. - Display line broke "FINE-LINE" at the hyphen at 1200 — replaced with non-breaking hyphens in the display strings.
- Zoomed the viewBox (880 × 640 → a 696 × 604 window) so the figure has presence next to the booking card; the card stays right of 66% so it never meets the machine (max reach ≈48%).
- Nav wrapped to two lines at 1000 px; nav now shows from 1120 px with nowrap links. Booking-card note printed a double dash (the "— " pseudo-element hit both the note and its short/long children) — scoped to direct children.
- Measured contrast for every token pair (table above); ox-blood copy stays off the ground; the pre-minted
#E27068measured 5.9:1 on ink and 5.3:1 on the card, so it carries the ledger notes and trace.
Pass 2 — Depth
- Redrew the machine as an upright coil machine (tube, L-frame, two coils, spring) with the artist's fist around the grip, leaned 34° to the trace head; added the 0.5 px needle buzz and the 0.85–1 opacity breath, both off under reduced motion; the loop is token-invalidated in the handler and paused on
visibilitychange. - Second-read details: the cover-up draws a faded script-and-star mark under the translucent paper and the new rose 1.6× heavier over it; the coin stencil is drawn at its true 25 mm (a dot the machine all but covers) while the tag prints the real size — the honesty is the detail. Hard placements get the "+1 hr" tag beside the label.
- Labels (zone, +1 hr tag, size tag) scale 1.55× under 480 px stage width and 1.25× under 700 px so they read at 375; re-laid on resize only when the scale band changes.
- Micro-interaction: the view turn — the figure scales to zero on X, swaps the detail layer and zone/stencil, and opens again in 200 ms; instant under reduced motion; a stale turn is dropped by the token if a chip lands mid-turn.
- Drove all sixteen worked scenarios plus the view semantics and the display-line words ("an hour and a half", "six hours") — every figure matched the oracle.
Pass 3 — Hardening
- 375: masthead checked first — wordmark right edge 266 px vs call button left 318 px; scrollWidth 375; no element past the right edge; sticky call bar inside the bottom 140 px; the badge (148 px tight variant, short-form notes and "$100 deposit off the sitting") clears the figure's right edge. Screenshots at 375 / 1000 / 1200 reviewed at native resolution — the label-collision sweep covered zone label vs "+1 hr" tag (stacked, 12 px apart), size tag under the stencil vs the needle tip (12 px clearance), and the machine vs booking card at every width (max reach ≈48% vs card from 66%); the two-line "hand & neck" label got its "+1 hr" tag lifted clear of the first line.
- 1200 × 750: stage top 272 px, badge bottom 414 px — the priced scene sits in the thumbnail.
- Reduced motion: settled frame with the paper at opacity 1, machine parked at 35% of the trace, loop not running, turn instant.
- Rapid-fire drive (twenty-plus clicks in one evaluate, twice) landed on the pinned rest state, settled, zero console errors.
- Empty submit on a fresh load flags three fields and keeps the confirmation hidden; filled submit shows the panel with "we've received / we'd call" and the $540 estimate; focus-visible rings on chips, toggle, inputs and buttons.
- Chanel rule: removed the second wrist line that ran across the figure's torso; kept one. Final copy read; harness 200+ asserts green across index and guide (guide probed for one h1, 375 overflow, table-scroll wrappers, colophon, the would-ship panel and no business types in live JSON-LD).
Blackwattle Tattoo is a fictional business — a tattoo studio industry demonstration designed & built by Sapience Analytics, part of the Generative Assets collection. Back to the site · Index.