Generative Assets · Industry demo build guide
How the Catchline Rainwater demo was built
Catchline Rainwater is a fictional Mundaring operator supplying and installing rainwater tanks, pump kits and rain-to-mains plumbing across the Perth hills. The page has one job: an install request with the tank, roof, pump and extras already priced — and the honest annual-capture figure that sells it. The audience is the hills family off scheme water, the gardener sick of summer bills, the new build plumbing rain to the laundry.
The Catchment Calculator
The signature is a drawn SVG hills house in light rain: corrugated roof, gutter, a charged downpipe running to a poly tank on a pad, rain streaking down, water sheeting to the gutter and a drawn stream filling the tank while its level breathes upward on a slow loop. Every control redraws the scene: the roof stepper (50–400 m², ×25) widens the drawn roofline — the house grows to 250 m² and then holds while the dimension tag keeps printing the true figure; the tank chips swap between a squat 5 kL, the classic 13.5 kL round and the 23 kL flat-wall, each with its kL cast on the wall; the pump chips draw a gravity tap, a garden pump kit or the house-supply pump with twin filters and a pipe run into the wall; the toggles draw the first-flush diverter on the downpipe and the rain-to-mains valve box on the wall.
The price lives inside the artwork as a zinc gauge-plate riveted beside the tank: the installed price, the capture line, and — when the maths asks for it — the fills-a-year note. On phones the plate docks statically below the drawn scene so it never covers the tank. The whole scene is rebuilt synchronously in each control's handler (the animation token is bumped before the state swap), with entrance choreography on the first draw only and a snappy ~360 ms stagger cap on every redraw after that.
The honesty rules in the numbers
- Capture maths, declared on-card: roof m² × 950 mm × 0.8 collection factor = 760 L per m² a year (19 kL per 25 m² step — whole kilolitres for every stepper value). The 950 mm is hedged on the page ("about 950 mm a year around the hills lately") and the 0.8 is stated as a conservative factor, because a demo that pretended rainfall precision would be selling a lie.
- The roof stepper never prices. "Your roof size changes what you catch, not what we charge" is printed beside the stepper, and the harness asserts the decoupling both ways: roof 200→225 holds $5,660 while capture moves to ≈171,000 L; a tank swap moves the price while capture stands still.
- Fills-a-year note: fills = capture kL ÷ tank kL, rounded to the nearest whole (half up — the word "about" carries the honesty; on this grid an exact half can never occur, since capture is always a whole number and none of 5, 13.5 or 23 divides one to a half). The note appears at fills ≥ 4 on the two smaller tanks and never on the 23 kL — verified at the on-grid boundary pair 13.5 kL × 50 m² (2.81 → 3, absent) and × 75 m² (4.22 → 4, present).
- Switchover semantics: live only with the house-supply pump; on the other pumps it's disabled with the reason on the page ("the switchover rides on the house pump — add that first"), and stepping back to the house pump restores the visitor's prior choice — including a deliberate OFF.
- Designed price-less lines: tank pads, retaining, steel and underground tanks are "quoted after a site look" in the grid and FAQ — no invented figures. Compliance and backflow are declared included, never a line item. Delivery is declared included on the plate and in the ledger.
- Cross-surface consistency: the display moment ("152,000 LITRES A YEAR OFF YOUR OWN ROOF") is live — it always prints the running model's capture, so the huge type can never argue with the plate, the ledger, the stepper readout or the screen-reader mirror.
Worked anchors (all measured from the running page)
| Scenario | Price | Capture | Fills note |
|---|---|---|---|
| Rest: 23 kL · 200 m² · house pump · diverter · switchover | $5,660 | ≈ 152,000 L | absent (top tank) |
| 13.5 kL, same everything | $4,410 | ≈ 152,000 L | "about 11 times a year — the 23 kL…" |
| 5 kL · 50 m² · no pump · no extras (grid floor) | $1,800 | ≈ 38,000 L | "about 8 times a year — the 13.5 kL…" |
| 13.5 kL · 100 m² · garden kit · diverter (switchover locked) | $3,260 | ≈ 76,000 L | "about 6 times a year…" |
| 23 kL · 400 m² · house pump · both extras | $5,660 | ≈ 304,000 L | absent — roof never prices |
Palette — argued from wet galvanised iron
A hills winter morning: gum-grey ground, ironbark-dark ink, rain-blue for the water and the CTAs, zinc for the corrugation lines and the rain itself. Every ratio below was measured with WCAG arithmetic from the shipped hexes.
| Pair (as shipped) | Measured | Verdict |
|---|---|---|
| #242B29 body ink on #E9EDEA ground | 12.23:1 | spec said 12.2 — holds |
| #FFFFFF on #2E6E93 CTA / #265D7D hover | 5.57:1 / 7.13:1 | spec said 5.6 / 7.1 — holds |
| #2E6E93 links on ground · on white cards | 4.72:1 · 5.57:1 | spec said 4.7 / 5.6 — holds |
| #1F4E6B blue-ink on ground · white | 7.53:1 · 8.90:1 | spec said 7.5 / 8.9 — holds |
| #5A666D zinc-ink on ground · white · #F5F8F6 | 5.00:1 · 5.91:1 · 5.52:1 | holds everywhere it's used |
| Gauge-plate sub-label — zinc-ink on the plate metal | 4.17:1 | failed → minted plate-ink #414C54: 5.01:1 on the deepest plate tone |
| Selected-chip small copy #EAF2F7 on #2E6E93 | 4.92:1 | first draft #D8E7F0 measured 4.41 → re-minted |
| Footer copy #C9D2CE · trust strip #DCE4E0 on #242B29 | 9.36:1 · 11.16:1 | holds |
Type
Bitter 600/700 for display — a sturdy slab that reads like a stencilled tank gauge; it carries the wordmark, the headings, the plate price and the kL cast on the tank wall. Mulish is the workhorse for everything else: controls, ledger, forms, footer. The typographic bravery moment is the display line over the drawn house — the running capture figure set huge in slab caps ("152,000 LITRES A YEAR OFF YOUR OWN ROOF"), rebuilt live from the model so the bravado is always arithmetically true.
Techniques
- Procedural SVG scene (no raster anywhere): the house, roof, gutter, charged downpipe, tank, pump kits, diverter and valve box are drawn from ~40 primitives per state by a single
drawScene(), positioned by computed coordinates only — no SVGtransformattributes, so CSS never collides with presentation attributes (the W16/W17 rule). - Rain as a rAF loop: 64 zinc streaks with per-drop speed, wrapped at the frame edge, with a gust that rolls through every ~15 s (the second-read detail); the delta is clamped at zero and capped, the loop pauses on
document.hidden, and streak opacity floors at 0.45 so the thumbnail can never catch an empty sky. The water streams (roof sheet, downpipe, inlet) animatestroke-dashoffsetin CSS against attribute-set dash arrays. - Reduced motion renders a settled wet scene: static rain field, the tank level held mid-breath, streams still, entrance transitions disabled.
- Dual-speed redraw: first draw staggers in at 26 ms per element (1.2 s cap); every later redraw runs 8 ms/element (360 ms cap) behind a
firstDrawflag, so chip taps feel snappy, not ceremonial. - State probe
window.__rainwatertanksexposes the scenario, price, capture, fills, note state, stored switchover choice, a frame counter and asettledflag — QA gates on state, never on seconds. - Calculator→form handoff: the request form's setup textarea mirrors the live scenario until the visitor edits it, so the enquiry lands pre-qualified.
Conversion decisions
- At 375 the first screen is: name, trade + area value proposition, the book CTA, the call CTA and the licence/warranty trust line — the calculator opens immediately below, and the sticky call/book bar rides the bottom edge.
- The plate carries the price inside the artwork because the 1200×750 thumbnail is the commercial argument — price, capture and (when present) the upsell note all intersect the first 750px.
- The fills-a-year note is the page's honest upsell: it only ever suggests the next tank when the roof genuinely fills the current one four or more times a year, and it never appears on the top tank.
- Form fields are the ones a tank installer actually needs before a site look: suburb (route planning), property type (truck choice), access notes (hills driveways), and the calculator's setup line.
Local SEO we'd implement for the real business
The demo page ships only WebPage/BreadcrumbList schema naming it a demonstration — fictional structured data in a real index would be dishonest markup. For the real client we'd ship Plumber (the install trade is plumbing — the exact schema.org organization subtype, not a generic LocalBusiness) with real NAP, geo, hours and a genuine review feed:
{
"@context": "https://schema.org",
"@type": "Plumber",
"name": "Catchline Rainwater",
"description": "Rainwater tanks, pumps and plumbing across the Perth hills.",
"url": "https://catchlinerainwater.com.au",
"telephone": "+61 8 XXXX XXXX",
"image": "https://catchlinerainwater.com.au/media/install-23kl.jpg",
"address": {
"@type": "PostalAddress",
"addressLocality": "Mundaring",
"addressRegion": "WA",
"postalCode": "6073",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.90, "longitude": 116.17 },
"areaServed": ["Mundaring", "Darlington", "Glen Forrest", "Mount Helena",
"Chidlow", "Kalamunda", "Roleystone"],
"openingHoursSpecification": [{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "07:00", "closes": "16:30"
}],
"hasCredential": "Licensed plumbing contractor — Plumbers Licensing Board WA",
"aggregateRating": { "@type": "AggregateRating",
"ratingValue": "4.9", "reviewCount": "87" },
"review": [{
"@type": "Review",
"author": { "@type": "Person", "name": "Real Customer" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "Pulled from the live review feed, never hand-written."
}]
}
What else changes for the real client: their install photos replace the drawn scene's supporting role (the calculator stays — it converts), reviews come from their Google profile feed, the request form books straight into ServiceM8 or Tradify as a site-look job, invoicing runs through Xero, tank stock and lead times come from supplier feeds (the Bushmans/Kingspan kind), and the capture widget reads live BoM rainfall for their actual gauge rather than a hedged long-term average.
Iteration log
-
Pass 1 — Craft
- Measured all 34 shipped colour pairs with WCAG arithmetic. The spec's computed figures held to the second decimal (12.23, 5.57, 7.13, 4.72, 7.53, 8.90, 5.00), but two pairs the spec never saw failed: the gauge-plate's sub-label (zinc-ink on plate metal, 4.17:1) got a minted plate-ink
#414C54(5.01:1 on the deepest plate tone), and the selected chip's small copy (4.41:1) was lifted to#EAF2F7(4.92:1). - Masthead book button rendered a double word-gap (flex gap + a stray
) — restructured into long/short label spans ("Book a site look" / "Site look"). - Scene label collisions fixed at draw-coordinate level: the mains line originally rose through the window at small roof widths (re-routed to enter the valve box from below), and the first-flush label sat on the charged pipe run (moved below the ground line, beside the flush point).
- Measured all 34 shipped colour pairs with WCAG arithmetic. The spec's computed figures held to the second decimal (12.23, 5.57, 7.13, 4.72, 7.53, 8.90, 5.00), but two pairs the spec never saw failed: the gauge-plate's sub-label (zinc-ink on plate metal, 4.17:1) got a minted plate-ink
-
Pass 2 — Depth
- Screenshot review caught the ship-blocker no assert saw: the gauge plate covered the entire 23 kL tank at desktop widths. Widened the viewBox 980→1080 to give the plate its own margin, re-anchored it so it overlaps only the tank's top corner (per-tank bottom offsets keep it "riveted" to each tank height), and docked it statically below the scene on phones — the drawn subject is never covered at any width.
- Moved the gravity tap from the tank's left base (where it tangled with the charged riser) to a clear stand on the right, and labelled it and the flush point under the ground line.
- Second-read detail: a rain gust rolls through roughly every fifteen seconds — streak speed swells and settles; the tank level breathes upward on a slow sine with an inlet ripple whose radius pulses.
- Drove every worked example against an independently reimplemented oracle: 36-cell full sweep (3 tanks × 3 pumps × 2 × 2 toggles at 150 m²) plus the pinned scenarios, the fills boundary pair (13.5 kL × 50 absent / × 75 present), the switchover lock/restore in both directions (restores a deliberate OFF too), and the price-holds-while-capture-moves decoupling both ways. Zero model corrections — every spec figure survived.
-
Pass 3 — Hardening
- 375 masthead checked first: wordmark right edge clears the call button; the tag line and phone number drop out below 640; no horizontal overflow on either page (offender probe excludes
overflow-x:autoancestors). - Rapid-fire drive — ten chip/stepper/toggle clicks in one evaluate, twice over with no settle — passed clean; the scene rebuild is synchronous in the handler with the token bumped before the swap, so there is no in-flight animation to race.
- Reduced-motion path renders the settled wet scene with the model live and
settledarriving in ~30 ms; empty submit raises validation with the confirmation panel held hidden, and a filled submit shows the designed panel (verified on a hard reload so the pre-filled setup line can't mask validation). - Chanel rule: cut the freestanding "mains" pipe label and a planned splash burst at the tank inlet — the dashed line into the valve box and the breathing ripple already carry the meaning.
- Final copy read: Australian English, no filler; the guide's tables wrapped in
.table-scroll; both pages carry the fictional-business colophon.
- 375 masthead checked first: wordmark right edge clears the call button; the tag line and phone number drop out below 640; no horizontal overflow on either page (offender probe excludes
Catchline Rainwater is a fictional business — a rainwater tank, pump and plumbing industry demonstration designed & built by Sapience Analytics, part of the Generative Assets collection. Back to the site · Index.