videos
This commit is contained in:
parent
6c6780fc60
commit
f7e0814a38
8 changed files with 206 additions and 92 deletions
|
|
@ -148,7 +148,12 @@ async function suppressDevServerNoise(context: BrowserContext) {
|
||||||
iframe[id*="webpack"],
|
iframe[id*="webpack"],
|
||||||
[id*="webpack-dev-server-client"],
|
[id*="webpack-dev-server-client"],
|
||||||
[class*="error-overlay"],
|
[class*="error-overlay"],
|
||||||
[class*="webpack-error"] {
|
[class*="webpack-error"],
|
||||||
|
/* Recording-only: hide dashboard chrome that reads as noise/loading
|
||||||
|
on camera (the "Finding the Perfect Postcode" AI-status CTA). The
|
||||||
|
data attribute is inert in production — only this injected rule
|
||||||
|
targets it, and only during a recording. */
|
||||||
|
[data-video-hide] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
|
|
|
||||||
|
|
@ -146,12 +146,16 @@ export async function installCursor(
|
||||||
At the 540-wide CSS viewport this renders ~30px type → 60px in the
|
At the 540-wide CSS viewport this renders ~30px type → 60px in the
|
||||||
published 1080-wide mp4: a proper social-video hook size. */
|
published 1080-wide mp4: a proper social-video hook size. */
|
||||||
body.__demo-aspect-vertical #__demo-caption {
|
body.__demo-aspect-vertical #__demo-caption {
|
||||||
top: 8%;
|
top: 14%;
|
||||||
max-width: 88vw;
|
max-width: 88vw;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
padding: 12px 18px;
|
padding: 12px 18px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#__demo-caption.visible {
|
#__demo-caption.visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
@ -209,6 +213,13 @@ export async function installCursor(
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
#__demo-outro-subtitle {
|
||||||
|
font: 500 22px/1.34 "Inter", system-ui, sans-serif;
|
||||||
|
color: #cbd5e1;
|
||||||
|
max-width: 30ch;
|
||||||
|
margin: 2px auto 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
#__demo-outro-url {
|
#__demo-outro-url {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -228,6 +239,7 @@ export async function installCursor(
|
||||||
comfortably inside the platform-safe centre column. */
|
comfortably inside the platform-safe centre column. */
|
||||||
body.__demo-aspect-vertical #__demo-outro-brand { font-size: 64px; }
|
body.__demo-aspect-vertical #__demo-outro-brand { font-size: 64px; }
|
||||||
body.__demo-aspect-vertical #__demo-outro-tagline { font-size: 26px; max-width: 22ch; }
|
body.__demo-aspect-vertical #__demo-outro-tagline { font-size: 26px; max-width: 22ch; }
|
||||||
|
body.__demo-aspect-vertical #__demo-outro-subtitle { font-size: 20px; max-width: 24ch; }
|
||||||
body.__demo-aspect-vertical #__demo-outro-url { font-size: 30px; padding: 16px 22px; }
|
body.__demo-aspect-vertical #__demo-outro-url { font-size: 30px; padding: 16px 22px; }
|
||||||
|
|
||||||
.__ad-scene {
|
.__ad-scene {
|
||||||
|
|
@ -816,10 +828,11 @@ export async function showOutro(
|
||||||
page: Page,
|
page: Page,
|
||||||
brand: string,
|
brand: string,
|
||||||
tagline: string,
|
tagline: string,
|
||||||
url: string
|
url: string,
|
||||||
|
subtitle?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
({ brand, tagline, url }) => {
|
({ brand, tagline, url, subtitle }) => {
|
||||||
document.getElementById('__demo-caption')?.classList.remove('visible');
|
document.getElementById('__demo-caption')?.classList.remove('visible');
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.id = '__demo-outro';
|
el.id = '__demo-outro';
|
||||||
|
|
@ -835,14 +848,22 @@ export async function showOutro(
|
||||||
urlEl.id = '__demo-outro-url';
|
urlEl.id = '__demo-outro-url';
|
||||||
// Drop the protocol so the CTA reads as a bare domain.
|
// Drop the protocol so the CTA reads as a bare domain.
|
||||||
urlEl.textContent = url.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
urlEl.textContent = url.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||||
card.append(brandEl, taglineEl, urlEl);
|
// Optional spoken-outro line, burned in for muted social viewers.
|
||||||
|
if (subtitle && subtitle.length > 0) {
|
||||||
|
const subtitleEl = document.createElement('div');
|
||||||
|
subtitleEl.id = '__demo-outro-subtitle';
|
||||||
|
subtitleEl.textContent = subtitle;
|
||||||
|
card.append(brandEl, taglineEl, subtitleEl, urlEl);
|
||||||
|
} else {
|
||||||
|
card.append(brandEl, taglineEl, urlEl);
|
||||||
|
}
|
||||||
el.appendChild(card);
|
el.appendChild(card);
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => el.classList.add('visible'));
|
requestAnimationFrame(() => el.classList.add('visible'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ brand, tagline, url }
|
{ brand, tagline, url, subtitle }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,38 @@ class NarrationLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const narrationLog = new NarrationLog();
|
export const narrationLog = new NarrationLog();
|
||||||
|
|
||||||
|
function formatVttTimestamp(ms: number): string {
|
||||||
|
const clamped = Math.max(0, Math.round(ms));
|
||||||
|
const totalSeconds = Math.floor(clamped / 1000);
|
||||||
|
const millis = clamped % 1000;
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
const pad = (n: number, width: number): string =>
|
||||||
|
n.toString().padStart(width, '0');
|
||||||
|
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}.${pad(millis, 3)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render narration cues as a WebVTT document.
|
||||||
|
*
|
||||||
|
* Pure helper: takes cues (already ordered by the caller) and produces a
|
||||||
|
* caption sidecar string. Cue text is trimmed and any internal blank lines
|
||||||
|
* are collapsed so a single cue stays a single VTT block.
|
||||||
|
*/
|
||||||
|
export function cuesToVtt(cues: NarrationCue[]): string {
|
||||||
|
let out = 'WEBVTT\n\n';
|
||||||
|
for (const cue of cues) {
|
||||||
|
const start = formatVttTimestamp(cue.videoTimeMs);
|
||||||
|
const end = formatVttTimestamp(cue.videoTimeMs + cue.durationMs);
|
||||||
|
const text = cue.text
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
.join('\n')
|
||||||
|
.trim();
|
||||||
|
out += `${start} --> ${end}\n${text}\n\n`;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { AUTH_STATE_PATH, LEAD_IN_S, OUTPUT_DIR } from './config.js';
|
import { AUTH_STATE_PATH, LEAD_IN_S, OUTPUT_DIR } from './config.js';
|
||||||
import { assertHardwareWebGL, launchRecordingBrowser } from './browser.js';
|
import { assertHardwareWebGL, launchRecordingBrowser } from './browser.js';
|
||||||
import { narrationLog } from './narration.js';
|
import { cuesToVtt, narrationLog } from './narration.js';
|
||||||
import { installDemoRoutes } from './routes.js';
|
import { installDemoRoutes } from './routes.js';
|
||||||
import type { Storyboard } from './script.js';
|
import type { Storyboard } from './script.js';
|
||||||
import { storyboards } from './storyboard.js';
|
import { storyboards } from './storyboard.js';
|
||||||
|
|
@ -105,6 +105,11 @@ async function recordOne(storyboard: Storyboard): Promise<void> {
|
||||||
console.log(
|
console.log(
|
||||||
`[${storyboard.name}] wrote ${cues.length} narration cues → ${join(dir, 'narration.json')}`
|
`[${storyboard.name}] wrote ${cues.length} narration cues → ${join(dir, 'narration.json')}`
|
||||||
);
|
);
|
||||||
|
const vttPath = join(dir, 'narration.vtt');
|
||||||
|
writeFileSync(vttPath, cuesToVtt(cues));
|
||||||
|
console.log(
|
||||||
|
`[${storyboard.name}] wrote ${cues.length} WebVTT captions → ${vttPath}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ async function stubAiFilters(page: Page, storyboard: Storyboard) {
|
||||||
filters: storyboard.content.stubbedFilters,
|
filters: storyboard.content.stubbedFilters,
|
||||||
travel_time_filters: storyboard.content.stubbedTravelTimeFilters,
|
travel_time_filters: storyboard.content.stubbedTravelTimeFilters,
|
||||||
notes: '',
|
notes: '',
|
||||||
match_count: 1247,
|
match_count: storyboard.content.matchCount ?? 1247,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -379,7 +379,7 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'showOutro':
|
case 'showOutro':
|
||||||
await showOutro(ctx.page, step.brand, step.tagline, step.url);
|
await showOutro(ctx.page, step.brand, step.tagline, step.url, step.subtitle);
|
||||||
return;
|
return;
|
||||||
case 'showAdScene':
|
case 'showAdScene':
|
||||||
await showAdScene(ctx.page, step.scene);
|
await showAdScene(ctx.page, step.scene);
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ export type Activity =
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
}
|
}
|
||||||
/** Reveal the closing brand card. */
|
/** Reveal the closing brand card. */
|
||||||
| { kind: 'showOutro'; brand: string; tagline: string; url: string; durationMs: number }
|
| { kind: 'showOutro'; brand: string; tagline: string; url: string; subtitle?: string; durationMs: number }
|
||||||
/** Reveal a full-screen ad-style overlay over the live map. */
|
/** Reveal a full-screen ad-style overlay over the live map. */
|
||||||
| { kind: 'showAdScene'; scene: AdScene; durationMs: number }
|
| { kind: 'showAdScene'; scene: AdScene; durationMs: number }
|
||||||
/** Fade the ad overlay away. */
|
/** Fade the ad overlay away. */
|
||||||
|
|
@ -318,6 +318,13 @@ export interface ContentConfig {
|
||||||
travelTimeDragFromMin: number;
|
travelTimeDragFromMin: number;
|
||||||
travelTimeDragToMin: number;
|
travelTimeDragToMin: number;
|
||||||
brand: BrandConfig;
|
brand: BrandConfig;
|
||||||
|
/**
|
||||||
|
* Stubbed "{{count}} properties match" total shown in the AI summary.
|
||||||
|
* Set PER storyboard so different searches/cities don't all display the
|
||||||
|
* same number (an identical count across cuts reads as fake). Falls back
|
||||||
|
* to a default in routes.ts when omitted.
|
||||||
|
*/
|
||||||
|
matchCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TravelTimeFilter {
|
export interface TravelTimeFilter {
|
||||||
|
|
|
||||||
|
|
@ -148,21 +148,21 @@ const RECORDING_LOCALIZATIONS: Record<RecordingLocale, RecordingLocalization> =
|
||||||
colourMapTitle: 'Colour map',
|
colourMapTitle: 'Colour map',
|
||||||
brand: {
|
brand: {
|
||||||
name: 'Perfect Postcode',
|
name: 'Perfect Postcode',
|
||||||
tagline: 'Find the area before the house.',
|
tagline: 'Find the hidden-gem postcode.',
|
||||||
url: BRAND_URL,
|
url: BRAND_URL,
|
||||||
},
|
},
|
||||||
cues: {
|
cues: {
|
||||||
hook: "You can't view your way to the right area.",
|
hook: "A postcode's reputation is priced in. Its value isn't.",
|
||||||
brief:
|
brief:
|
||||||
'So start with the brief. Price, commute, schools, even how quiet the street is.',
|
'So start with the brief. Budget, commute, schools, even how quiet the street is.',
|
||||||
search: 'One search, and England shrinks to the postcodes that fit.',
|
search: 'One brief, and England narrows to the postcodes worth your money.',
|
||||||
commute: 'Push the commute down, and the map answers in seconds.',
|
commute: 'Tighten the commute, and the keepers narrow further in seconds.',
|
||||||
streets: 'Down at street level, the strongest streets start to stand out.',
|
streets: 'Down at street level, the strongest streets start to stand out.',
|
||||||
open:
|
open:
|
||||||
'Open one, and it shows its work. Sold prices, schools, crime, noise, broadband.',
|
'Open one, and it shows its work. Sold prices, schools, crime, noise, broadband.',
|
||||||
shortlist:
|
shortlist:
|
||||||
'Shortlist the winners, export them, and only spend Saturdays where it counts.',
|
'Keep the best-value few, export them, and scout where it actually counts.',
|
||||||
outro: 'Stop guessing postcodes. Start choosing them.',
|
outro: 'Stop paying for the name. Find the value.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
|
|
@ -183,21 +183,22 @@ const RECORDING_LOCALIZATIONS: Record<RecordingLocale, RecordingLocalization> =
|
||||||
colourMapTitle: 'Karte einfärben',
|
colourMapTitle: 'Karte einfärben',
|
||||||
brand: {
|
brand: {
|
||||||
name: 'Perfect Postcode',
|
name: 'Perfect Postcode',
|
||||||
tagline: 'Erst die Gegend, dann das Haus.',
|
tagline: 'Finde die unterschätzte Postleitzahl.',
|
||||||
url: BRAND_URL,
|
url: BRAND_URL,
|
||||||
},
|
},
|
||||||
cues: {
|
cues: {
|
||||||
hook: 'Das richtige Viertel findest du nicht durch Besichtigungen.',
|
hook: 'Der Ruf einer Gegend steckt im Preis. Ihr wahrer Wert nicht.',
|
||||||
brief:
|
brief:
|
||||||
'Fang mit dem Briefing an. Preis, Pendelzeit, Schulen, sogar wie ruhig die Straße ist.',
|
'Also sag, was du suchst. Budget, Arbeitsweg, Schulen, sogar wie ruhig die Straße ist.',
|
||||||
search: 'Eine Suche, und England schrumpft auf die Postleitzahlen, die passen.',
|
search:
|
||||||
commute: 'Verkürze den Arbeitsweg, und die Karte antwortet in Sekunden.',
|
'Eine Vorgabe, und England schrumpft auf die Postleitzahlen, die dein Geld wert sind.',
|
||||||
|
commute: 'Verschärf den Arbeitsweg, und in Sekunden bleiben nur die besten übrig.',
|
||||||
streets: 'Auf Straßenebene stechen die besten Ecken von selbst hervor.',
|
streets: 'Auf Straßenebene stechen die besten Ecken von selbst hervor.',
|
||||||
open:
|
open:
|
||||||
'Öffne eine, und sie zeigt ihre Daten. Verkaufspreise, Schulen, Kriminalität, Lärm, Internet.',
|
'Öffne eine, und sie zeigt die Belege. Verkaufspreise, Schulen, Kriminalität, Lärm, Internet.',
|
||||||
shortlist:
|
shortlist:
|
||||||
'Speichere die Favoriten, exportiere sie, und verbringe deine Samstage nur dort, wo es zählt.',
|
'Behalte die paar mit dem besten Preis-Leistungs-Verhältnis, exportier sie und schau sie dir vor Ort an.',
|
||||||
outro: 'Hör auf, Postleitzahlen zu raten. Fang an, sie zu wählen.',
|
outro: 'Zahl nicht für den Namen. Finde den Wert.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
|
|
@ -216,18 +217,18 @@ const RECORDING_LOCALIZATIONS: Record<RecordingLocale, RecordingLocalization> =
|
||||||
colourMapTitle: '地图着色',
|
colourMapTitle: '地图着色',
|
||||||
brand: {
|
brand: {
|
||||||
name: 'Perfect Postcode',
|
name: 'Perfect Postcode',
|
||||||
tagline: '先选对街区,再选房子。',
|
tagline: '发现那个被低估的邮区。',
|
||||||
url: BRAND_URL,
|
url: BRAND_URL,
|
||||||
},
|
},
|
||||||
cues: {
|
cues: {
|
||||||
hook: '光靠看房,看不出哪个街区适合你。',
|
hook: '一个邮区的名气,早就算进了房价里。它真正的价值,却没有。',
|
||||||
brief: '先列条件。预算、通勤、学校,甚至街道安不安静。',
|
brief: '所以,把你的需求告诉它。预算、通勤、学校,甚至这条街够不够安静。',
|
||||||
search: '搜索一次,全英格兰只剩下符合条件的邮编。',
|
search: '一份需求,整个英格兰就缩小到那些真正值这个价的邮区。',
|
||||||
commute: '把通勤时间调短,地图几秒内就给出答案。',
|
commute: '把通勤再收紧一点,几秒钟,留下的好选择又少了一批。',
|
||||||
streets: '放大到街道层面,好街区自己浮现出来。',
|
streets: '放大到街道层面,好街区自己浮现出来。',
|
||||||
open: '点开一个,它会摆出证据。成交价、学校、犯罪率、噪音、宽带。',
|
open: '点开一个,它把依据都摆给你看。成交价、学校、治安、噪音、宽带。',
|
||||||
shortlist: '把心仪的区域导出,周末只去值得去的地方。',
|
shortlist: '留下最划算的那几个,导出来,去真正值得的地方实地看房。',
|
||||||
outro: '别再猜邮编,开始挑邮编。',
|
outro: '别再为名气买单。找到真正的价值。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hi: {
|
hi: {
|
||||||
|
|
@ -247,21 +248,21 @@ const RECORDING_LOCALIZATIONS: Record<RecordingLocale, RecordingLocalization> =
|
||||||
colourMapTitle: 'मानचित्र रंगें',
|
colourMapTitle: 'मानचित्र रंगें',
|
||||||
brand: {
|
brand: {
|
||||||
name: 'Perfect Postcode',
|
name: 'Perfect Postcode',
|
||||||
tagline: 'Find the area before the house.',
|
tagline: 'Find the hidden-gem postcode.',
|
||||||
url: BRAND_URL,
|
url: BRAND_URL,
|
||||||
},
|
},
|
||||||
cues: {
|
cues: {
|
||||||
hook: "You can't view your way to the right area.",
|
hook: "A postcode's reputation is priced in. Its value isn't.",
|
||||||
brief:
|
brief:
|
||||||
'So start with the brief. Price, commute, schools, even how quiet the street is.',
|
'So start with the brief. Budget, commute, schools, even how quiet the street is.',
|
||||||
search: 'One search, and England shrinks to the postcodes that fit.',
|
search: 'One brief, and England narrows to the postcodes worth your money.',
|
||||||
commute: 'Push the commute down, and the map answers in seconds.',
|
commute: 'Tighten the commute, and the keepers narrow further in seconds.',
|
||||||
streets: 'Down at street level, the strongest streets start to stand out.',
|
streets: 'Down at street level, the strongest streets start to stand out.',
|
||||||
open:
|
open:
|
||||||
'Open one, and it shows its work. Sold prices, schools, crime, noise, broadband.',
|
'Open one, and it shows its work. Sold prices, schools, crime, noise, broadband.',
|
||||||
shortlist:
|
shortlist:
|
||||||
'Shortlist the winners, export them, and only spend Saturdays where it counts.',
|
'Keep the best-value few, export them, and scout where it actually counts.',
|
||||||
outro: 'Stop guessing postcodes. Start choosing them.',
|
outro: 'Stop paying for the name. Find the value.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -432,8 +433,8 @@ function createCues(locale: RecordingLocale, formFactor: FormFactor): Storyboard
|
||||||
{
|
{
|
||||||
kind: 'mapZoom',
|
kind: 'mapZoom',
|
||||||
target: mapFocus,
|
target: mapFocus,
|
||||||
steps: 6,
|
steps: 5,
|
||||||
durationMs: 1800,
|
durationMs: 1300,
|
||||||
direction: 'out',
|
direction: 'out',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -468,17 +469,19 @@ function createCues(locale: RecordingLocale, formFactor: FormFactor): Storyboard
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: copy.cues.outro,
|
text: copy.cues.outro,
|
||||||
gapBeforeMs: 450,
|
gapBeforeMs: 350,
|
||||||
during: [
|
during: [
|
||||||
{
|
{
|
||||||
kind: 'showOutro',
|
kind: 'showOutro',
|
||||||
brand: copy.brand.name,
|
brand: copy.brand.name,
|
||||||
tagline: copy.brand.tagline,
|
tagline: copy.brand.tagline,
|
||||||
url: copy.brand.url,
|
url: copy.brand.url,
|
||||||
|
// Burn the spoken closer for sound-off / accessibility.
|
||||||
|
subtitle: copy.cues.outro,
|
||||||
durationMs: 0,
|
durationMs: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tail: [{ kind: 'wait', durationMs: 1300 }],
|
tail: [{ kind: 'wait', durationMs: 1700 }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -523,8 +526,10 @@ function buildVideoConfig(formFactor: FormFactor): VideoConfig {
|
||||||
minDurationS: 10,
|
minDurationS: 10,
|
||||||
maxDurationS: 75,
|
maxDurationS: 75,
|
||||||
// Right-pane inspection: London map, filters applied, data pane
|
// Right-pane inspection: London map, filters applied, data pane
|
||||||
// open — the clearest paused-state preview.
|
// FULLY populated (Street View + price history). 31s caught the pane
|
||||||
posterTimeS: 31,
|
// mid-load on prod (empty white + spinner); 35s lands a few seconds into
|
||||||
|
// the "open" cue once the evidence has rendered — the strongest preview.
|
||||||
|
posterTimeS: 35,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -537,11 +542,13 @@ function createRecordingStoryboard(
|
||||||
formFactor: FormFactor
|
formFactor: FormFactor
|
||||||
): Storyboard {
|
): Storyboard {
|
||||||
const copy = RECORDING_LOCALIZATIONS[locale];
|
const copy = RECORDING_LOCALIZATIONS[locale];
|
||||||
// Mobile bumps the initial zoom from 11.5 → 12 so the narrower 540-wide
|
// Both form factors open at zoom 12 so the cold-open frame lands on a
|
||||||
// viewport still shows an inner-London slice densely populated with
|
// densely-populated inner-London slice rather than the low-density outer
|
||||||
// hexagons (otherwise the visible map gets dominated by the low-density
|
// edges. Desktop was 11.5, but the opening lean-in panned the visible map
|
||||||
// outer edges with no matches).
|
// toward the sparse NW suburbs (Wembley/Ealing) — a soft first frame for
|
||||||
const initialZoom = formFactor === 'mobile' ? 12 : 11.5;
|
// the hero. 12 keeps central London (rich default-density colouring) in
|
||||||
|
// shot from t=0.
|
||||||
|
const initialZoom = 12;
|
||||||
|
|
||||||
// On mobile the MobileBottomSheet covers the bottom ~44% of the
|
// On mobile the MobileBottomSheet covers the bottom ~44% of the
|
||||||
// viewport, so the map's geographic centre sits roughly at the seam
|
// viewport, so the map's geographic centre sits roughly at the seam
|
||||||
|
|
@ -609,6 +616,9 @@ function createRecordingStoryboard(
|
||||||
travelTimeDragFromMin: TT_DRAG_FROM_MIN,
|
travelTimeDragFromMin: TT_DRAG_FROM_MIN,
|
||||||
travelTimeDragToMin: TT_DRAG_TO_MIN,
|
travelTimeDragToMin: TT_DRAG_TO_MIN,
|
||||||
brand: copy.brand,
|
brand: copy.brand,
|
||||||
|
// A tight 7-filter inner-London search; a plausible total (not the
|
||||||
|
// shared stub default) so the on-screen count reads as real.
|
||||||
|
matchCount: 1843,
|
||||||
},
|
},
|
||||||
pre: buildPre(),
|
pre: buildPre(),
|
||||||
cues: createCues(locale, formFactor),
|
cues: createCues(locale, formFactor),
|
||||||
|
|
@ -665,6 +675,8 @@ interface DemoAdStoryboardConfig {
|
||||||
travelTimeFilters?: TravelTimeFilter[];
|
travelTimeFilters?: TravelTimeFilter[];
|
||||||
posterTimeS?: number;
|
posterTimeS?: number;
|
||||||
initialZoom?: number;
|
initialZoom?: number;
|
||||||
|
/** Plausible per-city "{{count}} properties match" total for the AI summary. */
|
||||||
|
matchCount?: number;
|
||||||
/** Activities to run before the cue loop starts (silent — keep short). */
|
/** Activities to run before the cue loop starts (silent — keep short). */
|
||||||
prePrime?: Activity[];
|
prePrime?: Activity[];
|
||||||
/** Spoken line during the outro card. Must NOT repeat the card's text. */
|
/** Spoken line during the outro card. Must NOT repeat the card's text. */
|
||||||
|
|
@ -688,7 +700,7 @@ const AD_VIDEO: VideoConfig = {
|
||||||
|
|
||||||
const AD_BRAND = {
|
const AD_BRAND = {
|
||||||
name: 'Perfect Postcode',
|
name: 'Perfect Postcode',
|
||||||
tagline: 'Search the area before you search the listings.',
|
tagline: 'Find the hidden-gem postcode.',
|
||||||
url: BRAND_URL,
|
url: BRAND_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -825,6 +837,7 @@ function createDemoAdStoryboard(ad: DemoAdStoryboardConfig): Storyboard {
|
||||||
travelTimeDragFromMin: TT_DRAG_FROM_MIN,
|
travelTimeDragFromMin: TT_DRAG_FROM_MIN,
|
||||||
travelTimeDragToMin: TT_DRAG_TO_MIN,
|
travelTimeDragToMin: TT_DRAG_TO_MIN,
|
||||||
brand: AD_BRAND,
|
brand: AD_BRAND,
|
||||||
|
matchCount: ad.matchCount,
|
||||||
},
|
},
|
||||||
pre: ad.prePrime ?? AD_PRE,
|
pre: ad.prePrime ?? AD_PRE,
|
||||||
cues: [
|
cues: [
|
||||||
|
|
@ -844,10 +857,14 @@ function createDemoAdStoryboard(ad: DemoAdStoryboardConfig): Storyboard {
|
||||||
brand: AD_BRAND.name,
|
brand: AD_BRAND.name,
|
||||||
tagline: AD_BRAND.tagline,
|
tagline: AD_BRAND.tagline,
|
||||||
url: AD_BRAND.url,
|
url: AD_BRAND.url,
|
||||||
|
// Burn the spoken closer onto the card so muted social viewers
|
||||||
|
// (the ads' real channel) still read the CTA.
|
||||||
|
subtitle: ad.outroLine,
|
||||||
durationMs: 0,
|
durationMs: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tail: [wait(900)],
|
// Hold the brand card long enough to read the URL + closer with sound off.
|
||||||
|
tail: [wait(2200)],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
@ -865,6 +882,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
name: 'ad-01-say-it',
|
name: 'ad-01-say-it',
|
||||||
|
matchCount: 2410,
|
||||||
city: 'london',
|
city: 'london',
|
||||||
promptText: 'Flat under £600k, 35 min to central London, low crime, quiet street',
|
promptText: 'Flat under £600k, 35 min to central London, low crime, quiet street',
|
||||||
filters: {
|
filters: {
|
||||||
|
|
@ -878,23 +896,25 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
],
|
],
|
||||||
initialZoom: 10.2,
|
initialZoom: 10.2,
|
||||||
posterTimeS: 8,
|
posterTimeS: 8,
|
||||||
outroLine: 'Your area is on this map. Go find it.',
|
outroLine: 'The underpriced twin is on this map. Find it.',
|
||||||
cues: [
|
cues: [
|
||||||
{
|
{
|
||||||
text: 'This is my whole house brief. One sentence.',
|
text: 'My whole house brief is one plain sentence.',
|
||||||
caption: 'One sentence. Every postcode.',
|
caption: "Reputation is priced in. Value isn't.",
|
||||||
during: [
|
during: [
|
||||||
typeAct('Flat under £600k, 35 min to central London, low crime, quiet street', 2600),
|
typeAct('Flat under £600k, 35 min to central London, low crime, quiet street', 2600),
|
||||||
],
|
],
|
||||||
tail: [wait(150)],
|
tail: [wait(150)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "And that's every postcode in England that fits it.",
|
text: 'Every postcode in England that fits, sorted by value.',
|
||||||
|
caption: 'All of England, by value',
|
||||||
during: [submitSettled(1400)],
|
during: [submitSettled(1400)],
|
||||||
tail: [sheetDown(800), wait(250)],
|
tail: [sheetDown(800), wait(250)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Now I scroll a map, not nine hundred listings.',
|
text: 'Same schools, same commute, the price quietly drops nearby.',
|
||||||
|
caption: 'The cheaper twin nearby',
|
||||||
during: [mapZoomIn(1400, 4)],
|
during: [mapZoomIn(1400, 4)],
|
||||||
tail: [wait(400)],
|
tail: [wait(400)],
|
||||||
},
|
},
|
||||||
|
|
@ -907,6 +927,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
name: 'ad-02-twenty-minute-map',
|
name: 'ad-02-twenty-minute-map',
|
||||||
|
matchCount: 38600,
|
||||||
city: 'london',
|
city: 'london',
|
||||||
promptText: 'Within an hour of central London',
|
promptText: 'Within an hour of central London',
|
||||||
filters: {},
|
filters: {},
|
||||||
|
|
@ -915,7 +936,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
],
|
],
|
||||||
initialZoom: 10.2,
|
initialZoom: 10.2,
|
||||||
posterTimeS: 9,
|
posterTimeS: 9,
|
||||||
outroLine: 'Check the commute before you fall in love.',
|
outroLine: 'The commute is priced in. The bargain is not.',
|
||||||
prePrime: [
|
prePrime: [
|
||||||
{ kind: 'clearVignette', durationMs: 0 },
|
{ kind: 'clearVignette', durationMs: 0 },
|
||||||
{ kind: 'cursorScale', scale: 0, durationMs: 0 },
|
{ kind: 'cursorScale', scale: 0, durationMs: 0 },
|
||||||
|
|
@ -932,17 +953,19 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
cues: [
|
cues: [
|
||||||
{
|
{
|
||||||
text: 'Every colour on this map is a commute to central London.',
|
text: 'Every colour on this map is a commute to central London.',
|
||||||
caption: 'The 20-minute map',
|
caption: 'Same commute, cheaper postcode',
|
||||||
during: [wait(400)],
|
during: [wait(400)],
|
||||||
tail: [wait(150)],
|
tail: [wait(150)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Here's what twenty minutes actually leaves you.",
|
text: "Here's what twenty minutes actually leaves you.",
|
||||||
|
caption: 'What 20 minutes buys',
|
||||||
during: [sheetUp(700), touchShow(), ttDragAct(20, 1700)],
|
during: [sheetUp(700), touchShow(), ttDragAct(20, 1700)],
|
||||||
tail: [touchHide(), sheetDown(800), wait(300)],
|
tail: [touchHide(), sheetDown(800), wait(300)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "If it's not lit, you'd live on the train. Now you know before you view.",
|
text: "Same twenty minutes wherever it's lit, but not the same price.",
|
||||||
|
caption: 'Same ride, lower price',
|
||||||
during: [mapZoomIn(1300, 3)],
|
during: [mapZoomIn(1300, 3)],
|
||||||
tail: [wait(400)],
|
tail: [wait(400)],
|
||||||
},
|
},
|
||||||
|
|
@ -955,6 +978,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
name: 'ad-03-postcode-files',
|
name: 'ad-03-postcode-files',
|
||||||
|
matchCount: 1290,
|
||||||
city: 'london',
|
city: 'london',
|
||||||
initialFilters: {
|
initialFilters: {
|
||||||
'Estimated current price': [0, 700000],
|
'Estimated current price': [0, 700000],
|
||||||
|
|
@ -962,7 +986,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
},
|
},
|
||||||
initialZoom: 10.6,
|
initialZoom: 10.6,
|
||||||
posterTimeS: 11,
|
posterTimeS: 11,
|
||||||
outroLine: "Read the area's file before you book the viewing.",
|
outroLine: 'Every postcode, proven on the numbers, not its reputation.',
|
||||||
prePrime: [
|
prePrime: [
|
||||||
{ kind: 'clearVignette', durationMs: 0 },
|
{ kind: 'clearVignette', durationMs: 0 },
|
||||||
{ kind: 'cursorScale', scale: 0, durationMs: 0 },
|
{ kind: 'cursorScale', scale: 0, durationMs: 0 },
|
||||||
|
|
@ -971,8 +995,8 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
],
|
],
|
||||||
cues: [
|
cues: [
|
||||||
{
|
{
|
||||||
text: 'Estate agents say, up and coming. The data is more specific.',
|
text: 'One name costs more. The block next door scores the same.',
|
||||||
caption: 'Every postcode has a file',
|
caption: 'See the proof, not the postcode',
|
||||||
during: [
|
during: [
|
||||||
{
|
{
|
||||||
// Zoom around the strongest visible match so the destination
|
// Zoom around the strongest visible match so the destination
|
||||||
|
|
@ -991,11 +1015,13 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Sold prices, schools, crime, even Street View, for this exact postcode.',
|
text: 'Sold prices, schools, crime, even Street View, for this exact postcode.',
|
||||||
|
caption: 'The whole postcode file',
|
||||||
during: [touchShow(), tapHex(1000), scrollDrawer(420, 850)],
|
during: [touchShow(), tapHex(1000), scrollDrawer(420, 850)],
|
||||||
tail: [wait(200)],
|
tail: [wait(200)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Thirty seconds here saves you a Saturday on the wrong street.',
|
text: 'The same evidence a pricey postcode has, sitting quietly cheaper here.',
|
||||||
|
caption: 'Same proof, less money',
|
||||||
during: [scrollDrawer(820, 900), touchHide()],
|
during: [scrollDrawer(820, 900), touchHide()],
|
||||||
tail: [wait(350)],
|
tail: [wait(350)],
|
||||||
},
|
},
|
||||||
|
|
@ -1007,6 +1033,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
name: 'ad-04-quiet-streets',
|
name: 'ad-04-quiet-streets',
|
||||||
|
matchCount: 6420,
|
||||||
city: 'london',
|
city: 'london',
|
||||||
promptText: 'Quiet London street under 55 decibels, low crime',
|
promptText: 'Quiet London street under 55 decibels, low crime',
|
||||||
filters: {
|
filters: {
|
||||||
|
|
@ -1015,21 +1042,23 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
},
|
},
|
||||||
initialZoom: 10.6,
|
initialZoom: 10.6,
|
||||||
posterTimeS: 7,
|
posterTimeS: 7,
|
||||||
outroLine: 'Quiet is searchable now.',
|
outroLine: 'The quiet street nobody bid up.',
|
||||||
cues: [
|
cues: [
|
||||||
{
|
{
|
||||||
text: 'Listing photos are silent. Main roads are not.',
|
text: 'Listing photos are silent. Main roads are not.',
|
||||||
caption: "You can't hear a photo",
|
caption: "The quiet that's overlooked",
|
||||||
during: [typeAct('Quiet London street under 55 decibels, low crime', 2400)],
|
during: [typeAct('Quiet London street under 55 decibels, low crime', 2400)],
|
||||||
tail: [wait(150)],
|
tail: [wait(150)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'This is London under fifty-five decibels.',
|
text: 'This is London under fifty-five decibels.',
|
||||||
|
caption: 'Under 55 decibels',
|
||||||
during: [submitSettled(1400)],
|
during: [submitSettled(1400)],
|
||||||
tail: [sheetDown(800), wait(250)],
|
tail: [sheetDown(800), wait(250)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "The quiet streets were always there. Now they're searchable.",
|
text: 'Nearby, just as quiet, and overlooked.',
|
||||||
|
caption: 'Quiet, and overlooked',
|
||||||
during: [mapZoomIn(1600, 5)],
|
during: [mapZoomIn(1600, 5)],
|
||||||
tail: [wait(400)],
|
tail: [wait(400)],
|
||||||
},
|
},
|
||||||
|
|
@ -1041,6 +1070,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
name: 'ad-05-school-run',
|
name: 'ad-05-school-run',
|
||||||
|
matchCount: 3380,
|
||||||
city: 'leeds',
|
city: 'leeds',
|
||||||
promptText: 'Family home in Leeds under £350k, good primary schools, low crime',
|
promptText: 'Family home in Leeds under £350k, good primary schools, low crime',
|
||||||
filters: {
|
filters: {
|
||||||
|
|
@ -1049,24 +1079,26 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
'Serious crime (avg/yr)': [0, 30],
|
'Serious crime (avg/yr)': [0, 30],
|
||||||
},
|
},
|
||||||
initialZoom: 11.0,
|
initialZoom: 11.0,
|
||||||
posterTimeS: 7,
|
posterTimeS: 13,
|
||||||
outroLine: 'Pick the area first. Then the house.',
|
outroLine: 'The schools are real. The premium is optional.',
|
||||||
cues: [
|
cues: [
|
||||||
{
|
{
|
||||||
text: 'Good primary school, low crime, under three hundred and fifty. The actual family brief.',
|
text: 'Good primary, low crime, under three hundred and fifty. The actual family brief.',
|
||||||
caption: 'The school-run map',
|
caption: 'Catchment, minus the premium',
|
||||||
during: [
|
during: [
|
||||||
typeAct('Family home in Leeds under £350k, good primary schools, low crime', 2800),
|
typeAct('Family home in Leeds under £350k, good primary schools, low crime', 2800),
|
||||||
],
|
],
|
||||||
tail: [wait(150)],
|
tail: [wait(150)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Everything still on the map passes all three.',
|
text: 'Everything still on the map passes all three filters.',
|
||||||
|
caption: 'Passes all three',
|
||||||
during: [submitSettled(1400)],
|
during: [submitSettled(1400)],
|
||||||
tail: [sheetDown(800), wait(250)],
|
tail: [sheetDown(800), wait(250)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'No more falling for a house, then googling the catchment at midnight.',
|
text: 'Same primary, same low crime, without the name everyone else is bidding up.',
|
||||||
|
caption: 'Same catchment, less money',
|
||||||
during: [mapZoomIn(1600, 5)],
|
during: [mapZoomIn(1600, 5)],
|
||||||
tail: [wait(400)],
|
tail: [wait(400)],
|
||||||
},
|
},
|
||||||
|
|
@ -1079,6 +1111,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
name: 'ad-06-waitrose-test',
|
name: 'ad-06-waitrose-test',
|
||||||
|
matchCount: 880,
|
||||||
city: 'london',
|
city: 'london',
|
||||||
promptText: 'Walking distance to a Waitrose, a tube station and a park',
|
promptText: 'Walking distance to a Waitrose, a tube station and a park',
|
||||||
filters: {
|
filters: {
|
||||||
|
|
@ -1088,21 +1121,23 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
},
|
},
|
||||||
initialZoom: 10.4,
|
initialZoom: 10.4,
|
||||||
posterTimeS: 7,
|
posterTimeS: 7,
|
||||||
outroLine: 'Filter for the life, not just the floor plan.',
|
outroLine: 'Same Waitrose. Same tube. Cheaper postcode.',
|
||||||
cues: [
|
cues: [
|
||||||
{
|
{
|
||||||
text: "Near a Waitrose, a tube stop and a park. Yes, that's a real search.",
|
text: 'A Waitrose, a tube stop, a park nearby. Search by what you want.',
|
||||||
caption: 'The Waitrose test',
|
caption: 'The Waitrose test',
|
||||||
during: [typeAct('Walking distance to a Waitrose, a tube station and a park', 2800)],
|
during: [typeAct('Walking distance to a Waitrose, a tube station and a park', 2800)],
|
||||||
tail: [wait(150)],
|
tail: [wait(150)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'London, cut down to the postcodes that live the way you do.',
|
text: 'London, narrowed to the postcodes that fit the way you live.',
|
||||||
|
caption: 'Fits how you live',
|
||||||
during: [submitSettled(1400)],
|
during: [submitSettled(1400)],
|
||||||
tail: [sheetDown(800), wait(250)],
|
tail: [sheetDown(800), wait(250)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Snobby? Maybe. Efficient? Extremely.',
|
text: 'Same amenities, lower price nearby.',
|
||||||
|
caption: 'Same life, lower price',
|
||||||
during: [mapZoomIn(1400, 4)],
|
during: [mapZoomIn(1400, 4)],
|
||||||
tail: [wait(400)],
|
tail: [wait(400)],
|
||||||
},
|
},
|
||||||
|
|
@ -1114,6 +1149,7 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
name: 'ad-07-renters-map',
|
name: 'ad-07-renters-map',
|
||||||
|
matchCount: 2160,
|
||||||
city: 'london',
|
city: 'london',
|
||||||
promptText: 'Rent under £1,600, 30 min to central London, quiet street',
|
promptText: 'Rent under £1,600, 30 min to central London, quiet street',
|
||||||
filters: {
|
filters: {
|
||||||
|
|
@ -1124,22 +1160,24 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
{ mode: 'transit', slug: 'london', label: 'Central London', max: 30 },
|
{ mode: 'transit', slug: 'london', label: 'Central London', max: 30 },
|
||||||
],
|
],
|
||||||
initialZoom: 10.3,
|
initialZoom: 10.3,
|
||||||
posterTimeS: 7,
|
posterTimeS: 13,
|
||||||
outroLine: 'Know the postcode before the viewing queue.',
|
outroLine: 'The name costs more. The street does not.',
|
||||||
cues: [
|
cues: [
|
||||||
{
|
{
|
||||||
text: 'Rent under sixteen hundred, half an hour to work, on a quiet street.',
|
text: 'Rent under sixteen hundred, half an hour to central, on a quiet street.',
|
||||||
caption: 'Renters get a map too',
|
caption: 'Stop renting the reputation',
|
||||||
during: [typeAct('Rent under £1,600, 30 min to central London, quiet street', 2800)],
|
during: [typeAct('Rent under £1,600, 30 min to central London, quiet street', 2800)],
|
||||||
tail: [wait(150)],
|
tail: [wait(150)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every letting site shows you flats. This shows you areas.',
|
text: 'Letting sites rank flats. This ranks the streets around them.',
|
||||||
|
caption: 'Areas, not just flats',
|
||||||
during: [submitSettled(1400)],
|
during: [submitSettled(1400)],
|
||||||
tail: [sheetDown(800), wait(250)],
|
tail: [sheetDown(800), wait(250)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Turn up to the right viewings, and skip the rest.',
|
text: 'Same commute, same quiet, lower rent nearby.',
|
||||||
|
caption: 'Same street, lower rent',
|
||||||
during: [mapZoomIn(1600, 5)],
|
during: [mapZoomIn(1600, 5)],
|
||||||
tail: [wait(400)],
|
tail: [wait(400)],
|
||||||
},
|
},
|
||||||
|
|
@ -1147,10 +1185,11 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
},
|
},
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// 08 — Value. £9.99 against the real cost of a wasted viewing.
|
// 08 — Value. The cost of overpaying for a name vs the one-off fee.
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
name: 'ad-08-cheap-insurance',
|
name: 'ad-08-value',
|
||||||
|
matchCount: 1540,
|
||||||
city: 'london',
|
city: 'london',
|
||||||
promptText: 'Under £500k, 35 min to central London, low crime, good schools',
|
promptText: 'Under £500k, 35 min to central London, low crime, good schools',
|
||||||
filters: {
|
filters: {
|
||||||
|
|
@ -1163,21 +1202,23 @@ const AD_CONFIGS: DemoAdStoryboardConfig[] = [
|
||||||
],
|
],
|
||||||
initialZoom: 10.4,
|
initialZoom: 10.4,
|
||||||
posterTimeS: 7,
|
posterTimeS: 7,
|
||||||
outroLine: 'Skip the wrong viewings.',
|
outroLine: 'Buy value, not reputation.',
|
||||||
cues: [
|
cues: [
|
||||||
{
|
{
|
||||||
text: 'A bad viewing costs a train ticket and half a weekend.',
|
text: 'Two streets, same schools, same commute, priced tens of thousands apart.',
|
||||||
caption: '£9.99 vs a wasted Saturday',
|
caption: 'Two streets. One is overpriced.',
|
||||||
during: [typeAct('Under £500k, 35 min to central London, low crime, good schools', 2600)],
|
during: [typeAct('Under £500k, 35 min to central London, low crime, good schools', 2600)],
|
||||||
tail: [wait(150)],
|
tail: [wait(150)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'This costs nine ninety-nine, and tells you where not to go.',
|
text: "You pay for the postcode's name; the value sits a postcode away.",
|
||||||
|
caption: 'You pay for the name',
|
||||||
during: [submitSettled(1400)],
|
during: [submitSettled(1400)],
|
||||||
tail: [sheetDown(800), wait(250)],
|
tail: [sheetDown(800), wait(250)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Cheap insurance, for the biggest purchase of your life.',
|
text: 'Pay once, find the bargain, stop overpaying for the name.',
|
||||||
|
caption: 'Find the value instead',
|
||||||
during: [mapZoomIn(1600, 5)],
|
during: [mapZoomIn(1600, 5)],
|
||||||
tail: [wait(400)],
|
tail: [wait(400)],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue