How it was made · Industry demo 382
The Blast Line — pricing a surface standard before the phone rings
Hardstand Blast & Coat is a fictional Kwinana Beach abrasive blasting and protective coatings business. Its customers are fabricators, earthmoving contractors and maintenance managers who already know the standard on the drawing and only want to know what it costs and when they get the steel back. The page has one job: turn “how much to blast and coat this” into a phone call with the surface standard, the coating system and the square metres already priced.
Palette — argued from inside the booth
Every colour comes from a blast booth with the light on the work: the booth is near-black, the blasted metal is bone, the WA garnet is warm in the plume, and the prepared steel reads cold and bright where the nozzle has been. Two accents, not one, keep the page off the near-black-plus-single-acid-accent default. Every ratio below was measured with WCAG arithmetic from the shipped tokens during Pass 1, not estimated.
paint-order: stroke, and measures 12.41:1 over the drawn water and 13.63:1 over the land. The same halo is applied by attribute — never by stylesheet — to every label inside the booth, because those elements also carry fill and font-size presentation attributes and a CSS rule would silently discard them.Type — a stencil, a workhorse and a micron string
Anton (its only weight) carries every heading. It is an extremely heavy condensed grotesque and it was chosen because it is the letterform already painted on a blast booth door and stencilled on bay numbers — the page's display face is the shop's own signage. Barlow does all the reading: controls, ledger, FAQ, form. Space Mono appears in exactly one role — surface-standard and micron strings (Sa 2½ · EPOXY + PU · 35 m², 125 µm, the DFT rule's tick figures) — because those are read as data, not prose.
The typographic moment is the stamped figure in the hero: the live total set at Anton's full weight at up to 4.2rem with the spec string running beneath it in mono at 0.2em tracking. It is the rest-state total, re-derived from the running model on every state change, and on desktop it is ordered directly under the lede so the price lands inside the first 750 px of the 1200-wide capture.
The signature — the Blast Line
The whole site is a wipe. An SVG blast booth holds the work on trestles; a nozzle tracks slowly across it, and behind the nozzle the steel is mill scale and rust while ahead of it the steel is blasted to the selected standard and washed in the selected topcoat. The line between the two states never stops moving, so both halves of the argument are always on screen.
How it is drawn
Each item is a list of normalised shape descriptors (rects, polygons, circles, ellipses) which get rendered three times per frame budget: once into a <clipPath> that becomes the item's silhouette, once in corroded fills, and once in prepared fills plus a translucent topcoat wash. The corroded copy is clipped by a rect running from the left edge to the nozzle; the prepared copy is clipped by its complement. Moving the nozzle is therefore a two-attribute update, and the rust speckles, blast texture and masking tape all clip to the same silhouette so nothing ever paints outside the steel.
The DFT gauge — the second read
At the right of the booth a narrow inset column draws the coating as real layers in microns against a constant rule: 75 µm of epoxy primer, plus 50 µm of polyurethane, plus 125 µm of epoxy MIO where the system calls for it, with the inorganic zinc silicate primer drawn in a distinct hatch. Each band is named inside itself in mono at a size solved to fit the band, and the label ink is chosen by luminance so it always clears its own fill. The substrate under the stack carries a blast profile whose tooth count and amplitude sharpen with the surface standard. The point the gauge exists to make is that a coating system is a stack, not a colour swap — which is exactly what a customer arguing about price needs to see.
View fit and the honest metre
The item is drawn at √(area ÷ the item's preset area) about its own centre, inside a zoom-to-fit frame that floors at 6 m wide and ceilings at 24 m. The constant reference is a one-metre rule drawn on the booth floor in scene units, so it visibly shrinks as the item grows. The widest reachable state is a 200 m² handrail run: √(200 ÷ 15) = 3.65×, which draws 21.9 m wide and still sits inside the 24 m ceiling, so the ceiling was left alone.
Badge, measured
The job tag is an HTML overlay rather than SVG text, so the type stays crisp at any frame width and the price is a real text node. Its width and height are measured every layout and fed back into the drawing: the badge fraction of the frame sets where the item zone starts (x0 = frac × W + gutter), the pinned x-side guard W = max(1080, itemW ÷ (1 − frac)) is evaluated every draw, and the sheet's height is max(headroom + floor band, tagHeight + 2·gutter + 118, 0.40 × W) — a function of the measured badge, never a fixed aspect. At 640 px and below the tag docks directly beneath the artwork and the frame crops to 760 units so the subject grows.
dt = min(max(now − last, 0) ÷ 1000, 0.05)) so a vsync timestamp that precedes the scheduling call can never drive a negative accumulator into geometry. The nozzle's position stays inside 0.08–0.92 of the item, so both faces are visible at every instant — a visibility floor rather than phase arithmetic. Reduced motion parks the nozzle at 55 % with both states drawn, which is the strongest single frame anyway. The loop stops on document.hidden.The model — two rates a square metre, one minimum
Blasting is priced by the surface standard, coating by the system, and both multiply the same actual area. A $980 minimum job charge is billed instead of that product when the product is lower, never on top; handling is added outside the minimum. Everything is GST-inclusive and every reachable cell is a whole dollar by construction.
| Control | Option | What it is | Rate |
|---|---|---|---|
| Prep | Sweep blast, Sa 1 | Scale knocked back, ghosting left | $28/m² |
| Prep | Commercial, Sa 2 | Light shadowing acceptable | $38/m² |
| Prep | Near-white, Sa 2½ | Even bright grey — the C5-M floor | $48/m² |
| Prep | White metal, Sa 3 | Uniform bright face, immersion grade | $62/m² |
| System | Epoxy primer only | 75 µm | $26/m² |
| System | Epoxy + polyurethane | 75 + 50 = 125 µm | $48/m² |
| System | C5-M three-coat | 75 + 125 + 50 = 250 µm | $78/m² |
| System | Inorganic zinc silicate | 75 + 125 + 50 = 250 µm over a zinc-rich primer | $96/m² |
| Area | 5–200 m² in fives | Multiplies the prep line and the system line | — |
| Handling | Drop off at the booth | Added once, outside the minimum | $0 |
| Handling | Metro pick-up & return | One truck movement each way | +$260 |
| Handling | Site attendance | Containment and access are site-specific | quoted |
| Clamp | Minimum job | Billed instead of the blast-and-coat product | $980 |
Worked figures, measured from the running page
| Scenario | Arithmetic | Measured |
|---|---|---|
| Rest state — bucket, 35 m², Sa 2½, epoxy + PU, drop off | 35 × (48 + 48) | $3,360 |
| 5 m², sweep + epoxy primer, drop off | 5 × 54 = 270 → minimum | $980 |
| Storage tank, 120 m², Sa 2½ + C5-M | 120 × 126 | $15,120 |
| Handrail run, 15 m², Sa 2 + epoxy + PU, metro pick-up | 15 × 86 + 260 | $1,550 |
| Beams, 60 m², Sa 3 + zinc silicate | 60 × 158 | $9,480 |
| Chassis, 25 m², Sa 2½ + C5-M, site attendance | 25 × 126, travel not computable | $3,150 + site attendance quoted |
| Cheapest output anywhere in the model | the minimum, at 5 m² | $980 |
Forces, locks and what happens on the way back
- System → prep. Choosing the C5-M three-coat or the zinc silicate system forces prep to at least near-white Sa 2½ and disables sweep and commercial with the reason on the page. The prior prep is remembered and restored when the system drops back to epoxy-primer-only or epoxy + PU. Where prep is already Sa 2½ or Sa 3 the force is a no-op and nothing is remembered.
- Partial unwind. With sweep remembered and Sa 2½ forced under C5-M, switching to the zinc silicate system holds Sa 2½ because it is still a high system; switching to epoxy + PU restores sweep. Driven end to end: 40 m² sweep + epoxy = $2,160 → C5-M $5,040 → zinc $5,760 → epoxy + PU $3,040.
- Memory invalidation. Any explicit click on an enabled prep chip clears the restore memory, so a deliberate choice while the control is constrained is never undone later. Driven: force Sa 2½ with sweep remembered, click Sa 3, drop the system back — prep stays Sa 3 and the total is $4,400.
- Mobile → system, and the two-step chain. Site attendance disables inorganic zinc silicate (it needs controlled cure conditions) and, when zinc was the selected system, forces C5-M and remembers zinc; returning to either shop handling chip restores it. The forced C5-M in turn re-evaluates the prep force, so the chain runs two steps deep. Driven at 40 m²: sweep + epoxy $2,160 → zinc (prep forced to Sa 2½, sweep remembered) $5,760 → site attendance (system forced to C5-M, zinc remembered, the sweep memory survives) $5,040 + site attendance quoted → drop off (zinc restored) $5,760 → epoxy + PU (sweep restored) $3,040.
- Item presets. Each item chip sets the area stepper to its preset — 25 / 15 / 35 / 60 / 120 m², all of which land on the five-metre grid — but it is a starting point, not a lock. The stepper stays live and a user's own figure survives until the item changes again.
- The quoted state. Site attendance prints <computable subtotal> + site attendance quoted everywhere the total appears — never a bare “quoted”. The $0-subtotal case is unreachable because the $980 minimum floors every computable subtotal.
The reachability claims, enumerated
The prose on this page makes claims that are really arithmetic, so they were run over the whole grid in an independently reimplemented oracle rather than spot-checked. Over the twelve reachable prep × system pairs and the forty area steps:
- The $980 minimum can bind only at 5, 10 or 15 m². At 5 m² every reachable pair binds; at 10 m² seven of twelve do; at 15 m² only sweep + epoxy and commercial + epoxy do; from 20 m² no combination in the model falls under it.
- The cheapest output anywhere in the model is $980, which equals the services grid's floor.
- No reachable cell produces a non-integer dollar figure, so the whole-dollar display rule never has to round anything.
- No reachable quoted state has a $0 computable subtotal, so no bare-quote badge string is needed and none is defined.
Declarations that carry three surfaces
Three statements ride with the estimate at every width — the $980 minimum, what is included in the rate, and the limit of the estimate itself. They appear on the job tag's carbon copy inside the signature container, in the ledger, and in the visually-hidden live-region mirror. At 641 px and above the carbon copy carries the full strings; at 640 px and below it carries the pinned short forms (“$980 minimum job”, “masking, abrasive and the report in”, “area confirmed on the hardstand”) while the ledger and the mirror keep the full templates. A fourth line — the minimum-bind note — is a template whose arithmetic scales with the state: it prints the computed figure the minimum replaced, and it is present at 5 m² and absent at 10 m² exactly as the model says.
Conversion decisions
- Why the price is in the artwork. The 1200 × 750 capture is the commercial argument, so the computed total lives on a steel job tag drawn inside the signature's container, not only in a ledger below the fold. The hero also stamps the same figure at display size directly under the lede, so the price is the second thing read after the value proposition.
- Why the estimator comes before the services grid. This customer already knows their standard. Giving them the number first, then the rate card to check it against, respects that — and the estimator's cheapest output ($980) is exactly the grid's minimum-job floor, so the page never argues with itself.
- Why these form fields. Name, mobile, yard suburb, what the item is, the surface area, and an optional wanted-back date. The area and item arrive pre-filled from the Blast Line and the spec textarea is written for them, so the enquiry lands with the standard, the system, the microns and the handling already stated — a warm lead rather than “please call me”. Because two fields arrive pre-filled, an empty submit legitimately raises three validation signals rather than five; the confirmation panel stays hidden until a genuinely valid submit.
- Why the callback promise is static copy as well as a confirmation. The promise beside the form does conversion work before anyone submits; the designed confirmation panel is a separate element that is hidden on load, stays hidden on an empty submit, and only appears after a valid one.
- Why the mobile bar carries a call and a quote. Most of this traffic is a phone in a workshop. The bar is fixed inside the bottom 140 px at 375, both targets clear 44 px, and the header keeps its own
tel:link so the number is never more than one thumb away. - Why site attendance refuses a rate. Containment, access and spent-abrasive capture genuinely vary per yard. Publishing a mobile rate would be the dishonest choice, so the control prints the computable part of the job and declares the rest — which reads as competence, not evasion.
The three iteration passes
Pass 1 — Craft
- Found: the entire drawing was invisible and every assert was green. The scene's entrance stagger faded each group in with a CSS class and an
opacitytransition. In headless Chrome — the same renderer the capture pipeline uses — every group after the first two never painted, whilegetComputedStylereported opacity: 1,getBBoxreturned correct geometry and 460-odd state asserts passed. Serialising the same SVG into a canvas rendered the item correctly, which located the fault in the live paint rather than the DOM. Two further attempts failed the same way (stepped inline opacity, then a staggered DOM insertion): anything inserted into this SVG after the rAF loop has started does not get its own paint invalidation. Changed: the drawing is now assembled synchronously in one task and the entrance moved into the animation loop instead — the nozzle makes one fast pass across the work on load, then settles into its slow cycle. Only pixels found this; no assert could have. - Found: garnet ink at 4.15:1 on the job tag. The tag's body is a gradient whose lightest stop is #2B3134, where the base garnet ink fails. Changed: minted a lifted garnet ink #E8938B (5.64:1 at the worst stop) for the tag's note, keeping the base ink for panels and the ground. On a dark site the two-token remedy runs lighter, not darker.
- Found: the mono spec string was uppercased by CSS, rendering the surface standard as “SA 2½”. Changed: dropped the transform and set the string in its real case.
- Found: the quoted-state badge set “+ site attendance quoted” in Anton at display size, which wrapped to three shouting lines in a 200 px tag. Changed: the figure keeps Anton and the qualifier is a separate Barlow line in the lifted garnet ink — the same treatment in the tag and in the ledger total.
- Found: the price stamp sat 866 px down the page at 1200 wide — outside the capture. Changed: on desktop the hero copy is ordered so the stamp follows the lede, and the display h1 was pulled from 3.42rem to 2.92rem; on phones the stamp stays after the CTA so the value proposition, both CTAs and the trust line all clear the 375 fold.
- Measured: thirty-eight token/ground pairs with WCAG arithmetic. Every token that actually sets text clears 4.5:1 on every ground it sits on — the lowest is the lifted garnet ink on the tag's palest gradient stop at 5.64:1. Raw garnet measures 3.24:1 as text, which is why it is confined to scene marks and never labels the map.
Pass 2 — Depth
- Enriched the signature. The DFT gauge's layers are now named inside the bands they occupy — ZINC, EPOXY, MIO, PU — at a size solved against the band's own width so nothing overflows at any frame, with the label ink chosen by luminance against each fill. The gauge stopped being a bar chart and became the coating schedule.
- Second read added: masking tape drawn on the prepared face. It is the “masking is in the rate” declaration, drawn — noticed on a second visit, and it quietly justifies a line in the price.
- Second read added: the fresh fall of garnet on the dust bed now tracks the nozzle, so the floor tells you where the work has just been.
- Micro-interaction: the job tag takes the stamp again — a 340 ms lift-and-settle — whenever the figure moves, and never when it doesn't. It is suppressed entirely under reduced motion.
- Keyboard depth: arrow-key roving inside every chip group, with Home and End, skipping disabled chips. Driven in the harness: ArrowRight from the first system chip lands on epoxy + PU, End lands on zinc silicate, and under site attendance End correctly skips the disabled zinc chip and lands on C5-M.
- Motion timing: the nozzle cycle was lengthened to 22 s and the entrance hands over at the exact point where the eased first pass and the sine cycle agree (p = 0.92), so there is no seam. At the seven-second capture the nozzle sits at about 54 % across, which is the frame the concept asks for — but the visibility floor, not that arithmetic, is what guarantees both faces are always drawn.
Pass 3 — Hardening
- Found: a label collision at 1000 px. “MILL SCALE & RUST” and “Sa 2½ · EPOXY + PU” ran into each other head-on at tablet-landscape widths, where the badge takes a larger share of the frame. Changed: both labels are anchored to the item zone's edges rather than the item's, and a measured guard using
getComputedTextLength()lifts the corroded label one line when the two would still meet. Asserts could not see this; the mid-width screenshot could. - Found: the hose struck through the labels at 375. Changed: every label inside the booth now carries a booth-ground halo, applied as
paint-order/strokeattributes rather than a stylesheet rule, because those same elements setfillandfont-sizeas presentation attributes and any CSS rule would discard them. - Found: the docked tag's eyelet floated to the corner of the scene at 375. Setting the tag to
position: staticfor the dock left its absolutely-positioned eyelet resolving against the scene wrapper. Changed toposition: relative— still in flow beneath the artwork, still never over the drawn subject. - Found: the gauge's layer names and its top label overflowed their own box at 375, where scene units are large relative to the frame. Changed: band labels are size-solved, the micron ticks are right-anchored inside the box, and the gauge's title is anchored to the box's right edge.
- Found: the hero's garnet ambient wash painted over the panel. As the last child of the hero, the
::aftergradient sat above the controls. Changed: both washes are pinned behind the content with an explicit stacking order. - Removed one ornament (the Chanel rule): the third dust-mound layer on the booth floor, redundant once the fall started tracking the nozzle. A decorative wire tying the tag to the trestle was also drawn and then cut — it read as a stray line across the labels.
- No-JS floor: the carbon copy's three conditions are seeded in the markup and re-rendered per width, so the honest statements are readable even if the script never runs. Nothing on the page is left at zero opacity by a script that fails.
- Verified: 494 asserts green with zero console errors and zero failed requests — 79 of them an independent oracle reimplementation of the pinned anchors, plus every worked example, both force chains, the partial unwind, the memory invalidation, the minimum boundary at 5 and 10 m², the item presets, two rapid-fire bursts, the keyboard path, an empty and a filled submit, four form
patternattributes driven against deliberately invalid values, and a per-chip pixel check that both sides of the nozzle differ for all twelve reachable prep × system combinations.
Local SEO we'd implement for the real business
This page carries only WebPage, CreativeWork, WebSite and BreadcrumbList schema, because marking up a fictional business with LocalBusiness and Review data would put dishonest entities into a real index. For a real Kwinana Beach blasting and coatings business we would ship the block below, with genuine reviews pulled from the client's Google Business Profile rather than written as demo copy. schema.org has no surface-treatment subtype, so the specialty is named through additionalType, knowsAbout and a real hasOfferCatalog.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"additionalType": "https://en.wikipedia.org/wiki/Abrasive_blasting",
"name": "Hardstand Blast & Coat",
"description": "Abrasive blasting to ISO 8501-1 and protective coating systems to AS/NZS 2312 and ISO 12944, on a Kwinana Beach hardstand and mobile across the Perth metro.",
"url": "https://hardstandblastandcoat.com.au/",
"telephone": "+61 8 9000 0382",
"image": "https://hardstandblastandcoat.com.au/img/booth.jpg",
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "Unit 4, 118 Office Road",
"addressLocality": "Kwinana Beach",
"addressRegion": "WA",
"postalCode": "6167",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -32.2340, "longitude": 115.7690 },
"openingHoursSpecification": [
{ "@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday"],
"opens": "06:30", "closes": "16:30" },
{ "@type": "OpeningHoursSpecification",
"dayOfWeek": "Friday", "opens": "06:30", "closes": "14:00" }
],
"areaServed": [
{ "@type": "City", "name": "Kwinana" },
{ "@type": "City", "name": "Henderson" },
{ "@type": "City", "name": "Rockingham" },
{ "@type": "City", "name": "Bibra Lake" },
{ "@type": "City", "name": "Welshpool" },
{ "@type": "City", "name": "Canning Vale" },
{ "@type": "City", "name": "Malaga" }
],
"knowsAbout": [
"Abrasive blasting", "Garnet blasting", "ISO 8501-1 Sa 2.5",
"Two-pack epoxy coatings", "Polyurethane topcoats",
"Inorganic zinc silicate", "ISO 12944 C5-M", "AS/NZS 2312"
],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Blasting and coating services",
"itemListElement": [
{ "@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Abrasive blasting to ISO 8501-1" },
"priceSpecification": { "@type": "UnitPriceSpecification",
"price": "28.00", "priceCurrency": "AUD",
"unitText": "per square metre", "minPrice": "28.00" } },
{ "@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Epoxy + polyurethane coating system" },
"priceSpecification": { "@type": "UnitPriceSpecification",
"price": "48.00", "priceCurrency": "AUD", "unitText": "per square metre" } },
{ "@type": "Offer",
"itemOffered": { "@type": "Service", "name": "C5-M three-coat system" },
"priceSpecification": { "@type": "UnitPriceSpecification",
"price": "78.00", "priceCurrency": "AUD", "unitText": "per square metre" } }
]
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9", "reviewCount": "68"
},
"review": [
{ "@type": "Review",
"author": { "@type": "Person", "name": "Priya N." },
"datePublished": "2026-05-14",
"reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" },
"reviewBody": "Forty-two metres of handrail blasted Sa 2.5 and coated to C5-M with film readings on every run." }
]
}
</script>
Alongside the markup we would ship the usual local stack: a verified Google Business Profile with the hardstand's real hours and the roller-door photo set, service pages for each surface standard rather than one “services” page, an ISO 12944 environment-category explainer aimed at the “which standard do I actually need” query, and NAP consistency across the profile, the site and the industry directories fabricators actually search.
What would change for a real client. Their booth and hardstand photography replaces the procedural scene where a photograph reads better — though the Blast Line itself would stay, because the wipe is the argument. Reviews come from a live Google feed rather than demo copy. The enquiry form posts into whatever already runs the yard: simPRO for job tracking and the hardstand schedule, Fulcrum or Google Forms for the ITP and film-thickness records, Xero for the invoice, and the coating report generated from the same record rather than typed twice.
Build notes
- One HTML file, all CSS and JS inline. No build step, no framework, no libraries — the entire scene is hand-built SVG driven by one rAF loop.
- No raster assets anywhere: the booth, the item, the coating stack, the map and the favicon are all vector or gradient.
- Fonts are the only external request — Anton, Barlow and Space Mono from Google Fonts.
- State is exposed on
window.__sandblastingfor verification: the full control vector, the computed lines, the raw product, whether the minimum binds, both restore memories, the disabled chip sets, the measured badge, the current viewBox and asettledflag so a harness can gate on state instead of on seconds.