// Froid&Air animated product video — 60s story in 7 scenes
const { useState, useEffect, useMemo } = React;
// ============ Palette ============
const C = {
bg: '#fbfbfa',
bg2: '#f5f4f1',
bg3: '#efeeea',
ink: '#15150f',
ink2: '#33332a',
muted: '#6b6b60',
muted2: '#9a9a8e',
line: '#e7e5df',
line2: '#d9d7cf',
hair: '#f0efe9',
accent: '#0e8f84',
accentInk: '#0a564f',
accentSoft: '#def4f1',
accentSoft2: '#c9ede9',
cite: '#fff3c7',
citeInk: '#6b4e00',
citeEdge:'#ecd585',
ok: '#197a4e',
surface: '#ffffff',
};
const F = {
sans: '"Inter Tight", -apple-system, sans-serif',
serif: '"Instrument Serif", serif',
mono: '"JetBrains Mono", monospace',
};
// ============ Little primitives ============
const Icon = ({ name, size = 16, color = 'currentColor', stroke = 1.75 }) => {
const p = {
search: <>>,
sparkles: <>>,
doc: <>>,
book: <>>,
shield: ,
clock: <>>,
bolt: ,
check: ,
arrow: ,
mic: <>>,
folder: ,
}[name];
return ;
};
// ============ Scene 1 — Problem (0–9s) ============
// A technician overwhelmed by PDFs
function ScenePaperChaos() {
const { localTime, duration, progress } = useSprite();
const papers = [
{ x: 180, y: 120, rot: -8, title: "Manuel MSZ-AP50VG", ref: "MSZ-AP_IM · 56 p.", color: "#fff" },
{ x: 520, y: 90, rot: 4, title: "Fiche PUHZ-ZRP250", ref: "2025 · 12 p.", color: "#fff" },
{ x: 900, y: 140, rot: -3, title: "NF EN 378-2", ref: "Norme · 84 p.", color: "#fff" },
{ x: 1320, y: 110, rot: 7, title: "Directive F-Gas 517/2014", ref: "Régl. · 142 p.", color: "#fff" },
{ x: 280, y: 480, rot: 6, title: "SAV-PR-014", ref: "Procédure", color: "#fff" },
{ x: 680, y: 500, rot: -5, title: "Codes erreur série MSZ", ref: "Interne", color: "#fff" },
{ x: 1080, y: 470, rot: 3, title: "NF EN 14511", ref: "Norme", color: "#fff" },
{ x: 1440, y: 500, rot: -8, title: "Manuel MSZ-LN", ref: "rev. D", color: "#fff" },
{ x: 120, y: 780, rot: -4, title: "Fiche produit MSZ-HR", ref: "2024", color: "#fff" },
{ x: 560, y: 820, rot: 5, title: "Bulletin technique BT-219", ref: "avril 2025", color: "#fff" },
{ x: 980, y: 800, rot: -7, title: "Norme acoustique", ref: "EN 12102", color: "#fff" },
{ x: 1400, y: 830, rot: 4, title: "Procédure purge R-32", ref: "SAV", color: "#fff" },
];
// Papers spread from center outward during entry
const spread = Easing.easeOutCubic(clamp(localTime / 1.2, 0, 1));
// Title appears at 1.5s
const titleT = clamp((localTime - 1.2) / 0.7, 0, 1);
// Question appears at 4s
const qT = clamp((localTime - 3.2) / 0.6, 0, 1);
// Frustration animation - papers gently wobble
const wobble = Math.sin(localTime * 2) * 2;
return (
{/* subtle grid */}
{papers.map((p, i) => {
const cx = 960, cy = 540;
const x = cx + (p.x - cx) * spread;
const y = cy + (p.y - cy) * spread;
const exitT = clamp((localTime - 6.0) / 1.5, 0, 1);
const exitY = Easing.easeInCubic(exitT) * 1200;
const exitOp = 1 - exitT;
return (
PDF
{p.title}
{p.ref}
p. {(i%40)+1}
);
})}
{/* Title overlay */}
{/* Question */}
Où est la bonne réponse ?
);
}
// ============ Scene 2 — Solution reveal: search bar (9–15s) ============
function SceneSearchReveal() {
const { localTime } = useSprite();
const fullQ = "charge R-32 pour MSZ-AP50VG avec liaison 22 m ?";
// typing starts at 1s, 35 cps
const typeT = clamp((localTime - 0.8) * 22, 0, fullQ.length);
const typed = fullQ.slice(0, Math.floor(typeT));
const showCursor = Math.floor(localTime * 2) % 2 === 0;
// bar fades in immediately
const barT = Easing.easeOutCubic(clamp(localTime / 0.7, 0, 1));
// label fades before bar
const labelT = clamp((localTime) / 0.4, 0, 1);
// exit
const exitT = clamp((localTime - 5.0) / 1.0, 0, 1);
const exitOp = 1 - exitT;
return (
Base technique Froid&Air · prête en 1,8 s
{typed}
{showCursor && typeT < fullQ.length && }
Tous les documents
IA
Filtres
Source précise
{/* suggestions row */}
{["Procédure tirage au vide R-32", "Norme F-Gas >3 kg", "Code erreur E6 MSZ-HR35"].map((s, i) => (
{s}
))}
);
}
// ============ Scene 3 — Retrieval animation (15–23s) ============
function SceneRetrieval() {
const { localTime } = useSprite();
// query at top
const qT = Easing.easeOutCubic(clamp(localTime / 0.5, 0, 1));
// docs enter in cascade
const docs = [
{ title: "Manuel MSZ-AP50VG", ref: "p. 24", score: 94, color: C.accent },
{ title: "Fiche produit MSZ-AP", ref: "§3.2", score: 88, color: C.accent },
{ title: "Norme NF EN 378-2", ref: "Art. 6.2", score: 76, color: C.muted },
{ title: "Procédure SAV-PR-014", ref: "§4", score: 71, color: C.muted },
{ title: "BT-219 · liaisons", ref: "p. 3", score: 58, color: C.muted },
{ title: "Manuel MSZ-HR35", ref: "p. 41", score: 42, color: C.muted },
];
// "scanning" progress bar
const scanT = clamp((localTime - 0.4) / 2.0, 0, 1);
return (
{/* Query chip */}
charge R-32 pour MSZ-AP50VG avec liaison 22 m ?
{/* Scanning indicator */}
Lecture sémantique de 4 821 documents…
{Math.floor(scanT * 4821).toLocaleString('fr-FR')}
{/* Doc scan cards */}
{docs.map((d, i) => {
const enterAt = 0.8 + i * 0.15;
const eT = Easing.easeOutBack(clamp((localTime - enterAt) / 0.5, 0, 1));
const selected = d.score >= 71;
const selectAt = 3.5 + i * 0.1;
const selT = clamp((localTime - selectAt) / 0.5, 0, 1);
// relevant docs float up and glow; irrelevant fade
const lift = selected ? -selT * 20 : 0;
const opacity = selected ? 1 : 1 - selT * 0.55;
return (
0.3 ? C.accent : C.line),
borderRadius: 12,
padding: 16,
opacity: eT * opacity,
transform: `translateY(${(1-eT)*30 + lift}px) scale(${eT})`,
boxShadow: selected && selT > 0.3 ? `0 10px 30px rgba(14,143,132,0.2), 0 0 0 4px ${C.accentSoft}` : '0 2px 4px rgba(20,20,10,0.03)',
transition: 'all 200ms',
}}>
PDF
{d.title}
{d.ref}
{Math.round(Math.min(scanT*1.3, 1) * d.score)}%
);
})}
{/* Bottom caption */}
4 passages trouvés. Synthèse en cours…
);
}
// ============ Scene 4 — Answer with citations (23–35s) ============
function SceneAnswer() {
const { localTime } = useSprite();
// answer chunks appear sequentially
const chunks = [
{ at: 0.3, w: 0.6 }, // opening
{ at: 1.0, w: 0.6 }, // spec callout
{ at: 2.2, w: 0.6 }, // calc step 1
{ at: 3.0, w: 0.6 }, // calc step 2
{ at: 3.8, w: 0.6 }, // calc step 3
{ at: 4.8, w: 0.5 }, // final answer highlight
];
const op = (at) => Easing.easeOutCubic(clamp((localTime - at) / 0.5, 0, 1));
// pulse the final answer number
const pulse = localTime > 5.0 ? 1 + Math.sin((localTime - 5.0) * 6) * 0.03 : 1;
return (
{/* Left: answer */}
Réponse IA · 1,64 s
Charge R-32 pour MSZ-AP50VG
avec liaison 22 m ?
La charge usine est 1,15 kg pour une liaison ≤ 15 m1.
Au-delà, ajouter 20 g/m1.
{/* Step-by-step */}
{[
{ n: 1, body: <>Charge usine :
1,15 kg>, src: '[1]', at: chunks[2].at },
{ n: 2, body: <>Supplément :
(22 − 15) × 20 g = 140 g>, src: '[1]', at: chunks[3].at },
{ n: 3, body: <>Total à introduire :
1,29 kg de R-32>, src: '[2]', at: chunks[4].at },
].map((s, i) => (
{s.n}
{s.body}
{s.src.slice(1, -1)}
))}
{/* Right: spec callout + source cards */}
{/* Callout */}
{[
{ label:'HP', val:'4,15', u:'MPa' },
{ label:'BP', val:'1,64', u:'MPa' },
{ label:'Charge usine', val:'1,15', u:'kg' },
{ label:'Pour 22 m', val:'1,29', u:'kg', hi: true },
].map((s, i) => (
))}
{/* Sources */}
Sources consultées
{[
{ idx:1, title:'Manuel d\'installation MSZ-AP50VG', ref:'MSZ-AP_IM · p. 24', score:94, at: 1.6 },
{ idx:2, title:'Fiche produit série MSZ-AP — 2025', ref:'FP-MSZAP-2025 · §3.2', score:88, at: 1.9 },
{ idx:3, title:'Norme NF EN 378-2:2016', ref:'Art. 6.2.3', score:76, at: 2.2 },
{ idx:4, title:'Procédure SAV-PR-014', ref:'rev. B · §4', score:71, at: 2.5 },
].map((s, i) => (
))}
);
}
// ============ Scene 5 — Source trace (35–43s) ============
function SceneTrace() {
const { localTime } = useSprite();
// citation zoom in
const zoomT = Easing.easeOutCubic(clamp(localTime / 0.7, 0, 1));
// page flies in
const pageT = Easing.easeOutCubic(clamp((localTime - 0.8) / 0.7, 0, 1));
// highlight pulse
const hiT = clamp((localTime - 2.0) / 0.4, 0, 1);
const pulse = localTime > 2.4 ? 1 + Math.sin((localTime - 2.4) * 5) * 0.05 : 1;
// caption
const capT = clamp((localTime - 3.5) / 0.5, 0, 1);
// exit
const exitT = clamp((localTime - 6.2) / 0.8, 0, 1);
return (
{/* left: citation bubble */}
Citation [1]
Charge R-32 pré-conditionnée :
1,15 kg pour liaisons ≤ 15 m. Ajouter
20 g/m au-delà.
{/* Arrow connecting */}
{/* right: PDF page */}
MSZ-AP_IM · FR · rev. C
2.2 Circuit frigorifique
L'unité extérieure MSZ-AP50VG est livrée pré-conditionnée avec une charge en fluide frigorigène R-32.
| Paramètre |
Valeur |
| Fluide |
R-32 (GWP 675) |
| Charge usine (≤ 15 m) |
0 ? `0 0 0 ${hiT*3}px ${C.citeEdge}` : 'none',
transform: `scale(${pulse})`, display:'inline-block',
transition: 'box-shadow 200ms',
}}>
1,15 kg
|
| Charge additionnelle |
0 ? `0 0 0 ${hiT*3}px ${C.citeEdge}` : 'none',
transform: `scale(${pulse})`, display:'inline-block',
transition: 'box-shadow 200ms',
}}>
20 g / m
|
| Pression HP |
4,15 MPa |
| Pression BP |
1,64 MPa |
— 24 —
{/* bottom caption */}
Chaque réponse est traçable à sa source exacte.
);
}
// ============ Scene 6 — Device forms (43–51s) ============
function SceneDevices() {
const { localTime } = useSprite();
const deskT = Easing.easeOutCubic(clamp(localTime / 0.8, 0, 1));
const phoneT = Easing.easeOutCubic(clamp((localTime - 0.6) / 0.8, 0, 1));
const labelsT = clamp((localTime - 1.6) / 0.5, 0, 1);
return (
{/* Desktop mock */}
{['Accueil', 'Assistant', 'Historique', 'Importer'].map((l,i) => (
{l}
))}
Pression de charge R-32 ?
Réponse
Charge usine :
1,15 kg1 · +20 g/m au-delà de 15 m
1.
{[['HP','4,15','MPa'],['BP','1,64','MPa'],['Charge','1,15','kg']].map(([l,v,u],i) => (
))}
{/* Phone mock */}
Assistant
Froid&Air
Charge R-32 à 22 m ?
Réponse · 1,4 s
Total 1,29 kg — base 1,15 kg + 140 g pour 7 m en plus1.
{/* Labels */}
Bureau d'études
·
Terrain · SAV
Partout où vous travaillez.
);
}
// ============ Scene 7 — Closing (51–60s) ============
function SceneClosing() {
const { localTime } = useSprite();
const logoT = Easing.easeOutBack(clamp(localTime / 0.7, 0, 1));
const lineT = Easing.easeOutCubic(clamp((localTime - 0.7) / 0.6, 0, 1));
const statsT = clamp((localTime - 1.4) / 0.5, 0, 1);
const ctaT = clamp((localTime - 3.0) / 0.6, 0, 1);
return (
{/* Logo mark */}
{/* Name + tagline */}
Froid&Air
La base technique qui vous répond.
{/* Stats ticker */}
{[
{ v: '4 821', l: 'documents' },
{ v: '1,8 s', l: 'temps moyen' },
{ v: '94 %', l: 'réponses utiles' },
{ v: '24/7', l: 'disponible' },
].map((s, i) => (
))}
{/* CTA */}
Poser la première question
);
}
// ============ Captions ============
const CAPTIONS = [
{ at: 0.5, end: 8.5, text: "4 821 documents techniques. Des dizaines de marques, de normes, de procédures…" },
{ at: 9.2, end: 14.5, text: "Une seule question en langage naturel suffit." },
{ at: 15.2, end: 22.5, text: "Le moteur sémantique lit tous vos documents et sélectionne les passages pertinents." },
{ at: 23.2, end: 34.5, text: "La réponse arrive synthétisée — chiffres, étapes, calculs — avec citations numérotées." },
{ at: 35.2, end: 42.5, text: "Chaque affirmation renvoie à la source exacte. Traçabilité totale." },
{ at: 43.2, end: 50.5, text: "Bureau ou terrain, desktop ou mobile : la même base technique." },
{ at: 51.2, end: 60.0, text: "Froid&Air. La base technique qui vous répond." },
];
function Captions() {
const time = useTime();
const active = CAPTIONS.find(c => time >= c.at && time <= c.end);
if (!active) return null;
const fadeIn = Easing.easeOutCubic(clamp((time - active.at) / 0.4, 0, 1));
const fadeOut = 1 - Easing.easeInCubic(clamp((time - (active.end - 0.4)) / 0.4, 0, 1));
const op = Math.min(fadeIn, fadeOut);
return (
{active.text}
);
}
// ============ Chapter indicator ============
const CHAPTERS = [
{ at: 0, label: '01 · Le problème' },
{ at: 9, label: '02 · La requête' },
{ at: 15, label: '03 · Recherche sémantique' },
{ at: 23, label: '04 · La réponse' },
{ at: 35, label: '05 · Traçabilité' },
{ at: 43, label: '06 · Multi-support' },
{ at: 51, label: '07 · Froid&Air' },
];
function ChapterHUD() {
const time = useTime();
let ch = CHAPTERS[0];
for (const c of CHAPTERS) if (time >= c.at) ch = c;
return (
{ch.label}
);
}
function BrandCorner() {
return (
);
}
// ============ Full composition ============
function Video() {
return (
);
}
ReactDOM.createRoot(document.getElementById('root')).render(
);