Update data

This commit is contained in:
Andras Schmelczer 2026-05-14 08:17:10 +01:00
parent a4103b0896
commit 273d7a83ee
15 changed files with 716 additions and 316 deletions

View file

@ -3,7 +3,6 @@ import {
hex,
vfrac,
type Activity,
type AdScene,
type Storyboard,
type TravelTimeFilter,
type VideoConfig,
@ -629,30 +628,6 @@ const AD_DEFAULT_FILTERS: Record<string, [number, number] | string[]> = {
'Outstanding primary schools within 2km': [0, 10],
};
/**
* Stable Unsplash CDN photo URLs. Each one is a 720-wide JPEG fetched at
* record time. The CDN serves with permissive CORS, no auth needed, and
* the IDs are stable URLs (Unsplash does not rotate them). If any photo
* stops resolving, dom.ts hides the broken image and the rest of the
* scene still renders, so a 404 here degrades to text-only rather than
* breaking the ad. To swap a photo, search unsplash.com for the theme
* and paste the `photo-{id}` slug from the URL bar.
*/
const PHOTO = {
terracedRow: 'https://images.unsplash.com/photo-1769344694490-66fb22a8d8cf?w=720&q=80&auto=format&fit=crop',
brickStreet: 'https://images.unsplash.com/photo-1689867373120-355ce130d485?w=720&q=80&auto=format&fit=crop',
woodAccentHouses: 'https://images.unsplash.com/photo-1753198412280-b4a9729c1c51?w=720&q=80&auto=format&fit=crop',
colourfulRow: 'https://images.unsplash.com/photo-1718579019220-98697dc2fd72?w=720&q=80&auto=format&fit=crop',
busyTraffic: 'https://images.unsplash.com/photo-1645718171033-574c88494de2?w=720&q=80&auto=format&fit=crop',
cityTraffic: 'https://images.unsplash.com/photo-1714128949057-f7ac4cb71e6c?w=720&q=80&auto=format&fit=crop',
trafficLight: 'https://images.unsplash.com/photo-1680276553514-357f2edc46a1?w=720&q=80&auto=format&fit=crop',
leafySuburb: 'https://images.unsplash.com/photo-1663651884092-a2449ed3671a?w=720&q=80&auto=format&fit=crop',
suburbHomes: 'https://images.unsplash.com/photo-1768301346584-86e781872b82?w=720&q=80&auto=format&fit=crop',
trainPlatform: 'https://images.unsplash.com/photo-1684934899514-772e03714de5?w=720&q=80&auto=format&fit=crop',
trainClock: 'https://images.unsplash.com/photo-1657441629839-874d398b6e04?w=720&q=80&auto=format&fit=crop',
keysFrontDoor: 'https://images.unsplash.com/photo-1741156386380-0236c72eb6f9?w=720&q=80&auto=format&fit=crop',
};
const linger = (durationMs = 360): Activity[] => [{ kind: 'wait', durationMs }];
/**
@ -781,15 +756,6 @@ const ttDragAct = (toMin: number, durationMs = 1400): Activity => ({
toFraction: toMin / TT_SLIDER_MAX,
durationMs,
});
const showScene = (scene: AdScene): Activity => ({
kind: 'showAdScene',
scene,
durationMs: 0,
});
const hideScene = (durationMs = 320): Activity => ({
kind: 'hideAdScene',
durationMs,
});
const wait = (durationMs: number): Activity => ({ kind: 'wait', durationMs });
const mapZoomIn = (durationMs = 1400, steps = 5): Activity => ({
kind: 'mapZoom',
@ -860,17 +826,18 @@ const LONDON_VIEW = { lat: 51.4672, lon: -0.1276, zoom: 10.5 };
const AD_CONFIGS: DemoAdStoryboardConfig[] = [
// -------------------------------------------------------------------
// 01 — Search by sentence. Type the prompt on camera, narration runs
// simultaneously. Filters relevant: commute + crime + schools.
// simultaneously. Filters relevant: price + commute + crime + noise.
// -------------------------------------------------------------------
{
name: 'ad-01-london-prompt',
city: 'london',
promptText:
'Two bed in London, 35 min to centre, lower crime, lower noise',
'London flat under £600k, 35 min to centre, lower crime, lower noise',
filters: {
'Property type': ['Flats/Maisonettes'],
'Estimated current price': [0, 600000],
'Serious crime per 1k residents (avg/yr)': [0, 50],
'Road noise score (mean dB)': [0, 60],
'Noise (dB)': [0, 58],
},
travelTimeFilters: [
{ mode: 'transit', slug: 'london', label: 'London city centre', max: 35 },
@ -879,20 +846,20 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
posterTimeS: 8,
cues: [
{
text: 'Describe the London home you actually want.',
text: 'Stop searching listing by listing. Search by the area brief.',
during: [typeAct(
'Two bed in London, 35 min to centre, lower crime, lower noise',
'London flat under £600k, 35 min to centre, lower crime, lower noise',
2800
)],
tail: [wait(200)],
},
{
text: 'Hit search. The map answers in one second.',
text: 'Price, commute, crime and noise land on the map together.',
during: [submitAct(1100)],
tail: [wait(700)],
},
{
text: 'Every lit postcode fits all five rules at once.',
text: 'Every lit postcode is somewhere worth checking first.',
tail: [wait(600)],
},
],
@ -914,16 +881,16 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
posterTimeS: 5.5,
cues: [
{
text: 'Watch what one slider does to your shortlist.',
text: 'Your commute limit should change the map, not your patience.',
tail: [wait(200)],
},
{
text: 'Drag forty minutes down to fifteen.',
text: 'Drag forty minutes down to fifteen minutes.',
during: [ttDragAct(15, 1900)],
tail: [wait(700)],
},
{
text: 'Half the map just lost its place.',
text: 'The reachable postcodes disappear in front of you.',
tail: [wait(600)],
},
],
@ -946,17 +913,17 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
posterTimeS: 10,
cues: [
{
text: 'Type the brief. Map fills with matching areas.',
text: 'Type a family brief and watch matching areas appear.',
during: [typeAct('Family home in London, decent schools nearby', 2400), submitAct(900)],
tail: [wait(500)],
},
{
text: 'Zoom past the hexagons. Real postcodes break open.',
text: 'Zoom from area patterns into actual postcodes.',
during: [mapZoomIn(3000, 10)],
tail: [wait(400)],
},
{
text: 'Tap one. Sold prices, schools, crime, noise.',
text: 'Tap one for sold prices and street-level context.',
during: [
{ kind: 'cursorScale', scale: 1.3, durationMs: 200 },
clickHex(900),
@ -992,7 +959,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
posterTimeS: 6,
cues: [
{
text: 'Four hundred grand. London. Thirty minute commute.',
text: 'London under four hundred thousand, with a thirty minute commute.',
during: [typeAct(
'Flat in London under £400k, 30 min to centre, lower crime',
2800
@ -1000,61 +967,57 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
tail: [wait(400)],
},
{
text: 'Watch the filters stack and the map shrink.',
text: 'The active filters stack up as the map tightens.',
during: [scrollFilters(280, 900)],
tail: [wait(600)],
},
{
text: 'Every lit postcode hits all four rules.',
text: 'Now the cheap-looking areas have to pass the brief.',
tail: [wait(500)],
},
],
},
// -------------------------------------------------------------------
// 05 — Two streets apart. Photo split is the hook. Caption stays
// SHORT so it does not compete with the overlay's title text.
// 05 — Two streets apart. Product-led now: noise + crime filters are
// typed and submitted on screen instead of masking the product with
// generic street photos.
// -------------------------------------------------------------------
{
name: 'ad-05-two-streets-apart',
city: 'london',
promptText: 'Quieter London, lower road noise',
promptText: 'Quiet London streets, lower noise, lower serious crime',
filters: {
'Road noise score (mean dB)': [0, 58],
'Serious crime per 1k residents (avg/yr)': [0, 50],
'Noise (dB)': [0, 55],
'Serious crime per 1k residents (avg/yr)': [0, 45],
},
initialZoom: 10.6,
posterTimeS: 4,
cues: [
{
text: 'Two homes. Four hundred metres apart.',
during: [showScene({
mode: 'split',
accent: 'rose',
kicker: 'Two streets',
title: 'Same price tag.',
images: [PHOTO.terracedRow, PHOTO.busyTraffic],
left: { title: 'Street A', meta: 'Quiet', tone: 'good' },
right: { title: 'Street B', meta: 'Main road', tone: 'bad' },
transparent: false,
})],
text: 'Two streets can look identical in a listing photo.',
during: [typeAct(
'Quiet London streets, lower noise, lower serious crime',
2500
), submitAct(900)],
tail: [wait(400)],
},
{
text: 'Filter noise and serious crime before you book a viewing.',
during: [scrollFilters(220, 800)],
tail: [wait(500)],
},
{
text: 'Same price. Completely different lives.',
tail: [wait(500)],
},
{
text: 'The map knows the difference. The photos do not.',
during: [hideScene(360)],
tail: [wait(700)],
text: 'Now the quieter pockets are the ones left on screen.',
during: [mapZoomIn(1300, 4)],
tail: [wait(600)],
},
],
},
// -------------------------------------------------------------------
// 06 — Commute tax. Photo hook (train platform) opens; cue 1 hides
// the overlay and the travel-time slider drags from 60 → 20 min.
// 06 — Commute tax. Starts on the live commute layer and immediately
// proves the point with the travel-time slider.
// -------------------------------------------------------------------
{
name: 'ad-06-london-commute-tax',
@ -1068,38 +1031,30 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
posterTimeS: 4,
cues: [
{
text: 'Twenty minutes or sixty. Same asking price.',
during: [showScene({
mode: 'title',
accent: 'amber',
kicker: 'Commute tax',
image: PHOTO.trainClock,
title: 'Cheap, until you count the hours.',
})],
tail: [wait(400)],
text: 'A cheap home gets expensive when the commute is wrong.',
tail: [wait(300)],
},
{
text: 'Drag the slider. Watch the map shrink.',
during: [hideScene(320), ttDragAct(20, 1800)],
text: 'Drag sixty minutes down to twenty and watch the map shrink.',
during: [ttDragAct(20, 1900)],
tail: [wait(700)],
},
{
text: 'Time is the bill you pay every week.',
text: 'That weekly time bill is visible before the viewing.',
tail: [wait(600)],
},
],
},
// -------------------------------------------------------------------
// 07 — Quiet near London. Leafy-suburb photo opens; cue 1 hides it
// and the dashboard (already filtered for low noise) is revealed.
// 07 — Quiet near London. Uses the real prod Noise (dB) feature.
// -------------------------------------------------------------------
{
name: 'ad-07-quiet-near-london',
city: 'london',
promptText: 'Quieter London, lower road noise, good transit',
filters: {
'Road noise score (mean dB)': [0, 56],
'Noise (dB)': [0, 55],
'Estimated current price': [0, 700000],
},
travelTimeFilters: [
@ -1109,30 +1064,25 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
posterTimeS: 4,
cues: [
{
text: 'Quiet streets, near London. They do exist.',
during: [showScene({
mode: 'title',
accent: 'teal',
image: PHOTO.leafySuburb,
title: 'Yes, they exist.',
})],
text: 'Quiet near London is searchable, not just hopeful.',
during: [typeAct('Quieter London, lower road noise, good transit', 2500), submitAct(900)],
tail: [wait(400)],
},
{
text: 'You just have to filter for noise, not price.',
during: [hideScene(320), scrollFilters(220, 800)],
text: 'Filter for noise alongside price and travel time.',
during: [scrollFilters(220, 800)],
tail: [wait(500)],
},
{
text: 'The hidden pockets light up.',
text: 'The calmer pockets show up before you go anywhere.',
tail: [wait(500)],
},
],
},
// -------------------------------------------------------------------
// 08 — The postcode comes with the keys. Keys photo opens; map shows
// London filtered for family-friendly area.
// 08 — The postcode comes with the keys. Keeps the memorable premise,
// but shows the product doing the work instead of a keys stock photo.
// -------------------------------------------------------------------
{
name: 'ad-08-postcode-with-the-keys',
@ -1142,99 +1092,104 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
'Estimated current price': [0, 750000],
'Outstanding primary schools within 2km': [1, 10],
'Serious crime per 1k residents (avg/yr)': [0, 50],
'Noise (dB)': [0, 58],
},
travelTimeFilters: [
{ mode: 'transit', slug: 'london', label: 'London city centre', max: 45 },
],
initialZoom: 10.5,
posterTimeS: 3,
cues: [
{
text: 'You can renovate the kitchen.',
during: [showScene({
mode: 'title',
accent: 'lime',
image: PHOTO.keysFrontDoor,
title: 'You keep the postcode forever.',
})],
text: 'You can change the kitchen. You inherit the postcode.',
during: [typeAct(
'Family London, lower crime, good schools, lower noise',
2500
), submitAct(900)],
tail: [wait(400)],
},
{
text: 'You can not renovate the commute or the noise.',
text: 'So check commute, crime, schools and noise first.',
during: [scrollFilters(320, 900)],
tail: [wait(500)],
},
{
text: 'Pick the area first. The keys come second.',
during: [hideScene(320)],
during: [mapZoomIn(1200, 4)],
tail: [wait(600)],
},
],
},
// -------------------------------------------------------------------
// 09 — Waitrose distance. Niche filter that maps to social-class
// proxy. We type the brief, scroll the filter pane to surface the
// Waitrose-distance card explicitly.
// 09 — Amenities. Waitrose is the memorable example, but the copy
// frames it as practical amenity filtering rather than a throwaway gag.
// -------------------------------------------------------------------
{
name: 'ad-09-london-waitrose',
city: 'london',
promptText:
'London postcodes within walking distance of a Waitrose',
'London postcodes near Waitrose, tube and parks under £800k',
filters: {
'Distance to nearest Waitrose (km)': [0, 1],
'Distance to nearest tube station (km)': [0, 1.2],
'Distance to nearest park (km)': [0, 0.8],
'Estimated current price': [0, 800000],
},
initialZoom: 10.4,
posterTimeS: 7,
cues: [
{
text: 'How close is your nearest Waitrose. Yes, really.',
text: 'Amenities should be filters, not guesses from the photos.',
during: [typeAct(
'London postcodes within walking distance of a Waitrose',
'London postcodes near Waitrose, tube and parks under £800k',
2800
), submitAct(900)],
tail: [wait(400)],
},
{
text: 'The map highlights the lucky postcodes.',
during: [scrollFilters(180, 800)],
text: 'Waitrose, tube, parks and price can all count together.',
during: [scrollFilters(300, 900)],
tail: [wait(600)],
},
{
text: 'It is a real filter, not a meme.',
text: 'Now you know which postcodes actually match that lifestyle.',
tail: [wait(500)],
},
],
},
// -------------------------------------------------------------------
// 10 — Reform-voting councils. % Reform UK vote share as a filter.
// Politically tense — kept matter-of-fact, no spin in the copy.
// 10 — Local politics. Matter-of-fact and product-led; lower threshold
// keeps the map populated while still surfacing the Reform UK feature.
// -------------------------------------------------------------------
{
name: 'ad-10-reform-councils',
city: 'london',
city: 'leeds',
promptText:
'Areas where the council voted heavily for Reform UK',
'Areas with higher Reform UK vote share and lower prices',
filters: {
'% Reform UK': [25, 100],
'% Reform UK': [15, 100],
'Estimated current price': [0, 350000],
},
initialZoom: 9.5,
initialZoom: 10.5,
posterTimeS: 7,
cues: [
{
text: 'Want to know which way your future council voted.',
text: 'Local politics is part of the neighbourhood data too.',
during: [typeAct(
'Areas where the council voted heavily for Reform UK',
'Areas with higher Reform UK vote share and lower prices',
2600
)],
tail: [wait(300)],
},
{
text: 'Run the filter. See the map.',
text: 'Run the filter and see which areas stay in view.',
during: [submitAct(900), scrollFilters(180, 700)],
tail: [wait(500)],
},
{
text: 'Politics shapes the area too.',
text: 'No spin. Just another local signal before you buy.',
tail: [wait(500)],
},
],
@ -1247,76 +1202,67 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
name: 'ad-11-leeds-families',
city: 'leeds',
promptText:
'Three bed near Leeds, outstanding primary nearby, lower crime',
'Leeds family areas, good primary schools nearby, lower crime',
filters: {
'Estimated current price': [0, 380000],
'Outstanding primary schools within 2km': [2, 10],
'Good+ primary schools within 2km': [2, 10],
'Serious crime per 1k residents (avg/yr)': [0, 45],
},
initialZoom: 11.0,
posterTimeS: 6,
cues: [
{
text: 'Leeds, but only the school-run friendly bits.',
text: 'Find Leeds areas that work for the school run.',
during: [typeAct(
'Three bed near Leeds, outstanding primary nearby, lower crime',
'Leeds family areas, good primary schools nearby, lower crime',
2500
), submitAct(900)],
tail: [wait(300)],
},
{
text: 'Two outstanding primaries within walking distance.',
text: 'School quality and serious crime sit beside price.',
during: [scrollFilters(220, 800)],
tail: [wait(500)],
},
{
text: 'Every lit postcode is a real candidate.',
text: 'Every lit postcode is a better place to start.',
tail: [wait(500)],
},
],
},
// -------------------------------------------------------------------
// 12 — Pricing scarcity. Real prod numbers (verified via /api/pricing
// at render time): the £0.99 tier is sold out (50/50); the current
// £9.99 tier has 17 slots left before the next jump to £29.99. We
// surface those numbers in a structured rank scene over the live
// dashboard, since recording on the /pricing route would require a
// dashboard URL override and we want to ship this iteration.
// 12 — Pricing/value. Keeps the current £9.99 founder-price hook, but
// proves value through the product instead of a static scarcity card.
// -------------------------------------------------------------------
{
name: 'ad-12-pricing-scarcity',
city: 'london',
promptText: 'Quieter London, good schools, lower crime',
promptText: 'London under £700k, good schools, lower crime and lower noise',
filters: {
'Estimated current price': [0, 700000],
'Outstanding primary schools within 2km': [1, 10],
'Serious crime per 1k residents (avg/yr)': [0, 50],
'Noise (dB)': [0, 58],
},
initialZoom: 10.4,
posterTimeS: 3,
cues: [
{
text: 'Seventeen spots left at nine ninety nine.',
during: [showScene({
mode: 'rank',
accent: 'amber',
kicker: 'Founder pricing',
title: 'Cheap tier almost gone.',
items: [
{ label: '£0.99 / month', value: 'sold out', tone: 'bad' },
{ label: '£9.99 / month', value: '17 left', tone: 'warn' },
{ label: '£29.99 / month', value: 'next', tone: 'neutral' },
],
})],
text: 'Nine ninety nine beats one wasted viewing.',
during: [typeAct(
'London under £700k, good schools, lower crime and lower noise',
2700
), submitAct(900)],
tail: [wait(400)],
},
{
text: 'Then the price triples.',
text: 'Use the map before spending a Saturday in the wrong area.',
during: [scrollFilters(300, 900)],
tail: [wait(500)],
},
{
text: 'Get in before the next jump.',
during: [hideScene(360)],
text: 'The cheapest mistake is the one you skip.',
tail: [wait(600)],
},
],