// 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 {p}; }; // ============ 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 */}
4 821 documents.
{/* 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
Rechercher
{/* 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) => (
{s.label}
{s.val}{s.u}
))}
{/* 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) => (
{s.idx}
{s.title}
{s.ref}
{s.score}%
))}
); } // ============ 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]
1
MSZ-AP_IM · p. 24
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 */}
F
Froid&Air
{['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 m1.
{[['HP','4,15','MPa'],['BP','1,64','MPa'],['Charge','1,15','kg']].map(([l,v,u],i) => (
{l}
{v}{u}
))}
{/* 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.
1
MSZ-AP50VG · p.24
{/* 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 */}
F
{/* 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) => (
{s.v}
{s.l}
))}
{/* 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 (
F
Froid&Air
); } // ============ Full composition ============ function Video() { return ( ); } ReactDOM.createRoot(document.getElementById('root')).render(