Ajoute le pipeline PageSpec (validation, rendu, publication /p/{slug}),
les démos TipTap /demo, et le simulateur Carnot (modes frigo/PAC/moteur,
énergie kJ vs puissance W, unités K/°C/°F) avec correctifs d’équations KaTeX.
Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
7.9 KiB
TypeScript
154 lines
7.9 KiB
TypeScript
'use client'
|
||
|
||
import { useDarkMode } from '@/components/interactive-demo/demo-speak'
|
||
|
||
/**
|
||
* Carnot cycle animated scene — piston apparatus + live P-V diagram.
|
||
* Driven by AnimPlayerShell via `step` (0..4). Pure SVG, transitions on
|
||
* transform/opacity only (GPU-friendly, reduced-motion safe).
|
||
*/
|
||
|
||
const PISTON_Y = [105, 70, 135, 170, 110]
|
||
const GAS_HOT = [1, 0.55, 0.12, 0.6, 0.5]
|
||
const GAS_COLD = [0, 0.45, 0.9, 0.4, 0.4]
|
||
const T_LABEL = ['T_h', 'T_h → T_c', 'T_c', 'T_c → T_h', 'η']
|
||
|
||
// P–V anchors (illustrative)
|
||
const A = { x: 405, y: 50 }
|
||
const B = { x: 495, y: 159 }
|
||
const C = { x: 630, y: 212 }
|
||
const D = { x: 465, y: 175 }
|
||
const ANCHORS = [A, B, C, D]
|
||
const CYCLE = `${A.x},${A.y} ${B.x},${B.y} ${C.x},${C.y} ${D.x},${D.y}`
|
||
|
||
const EASE = 'transform 700ms cubic-bezier(0.22, 1, 0.36, 1), opacity 500ms ease'
|
||
|
||
export function CarnotCycleAnimView({ step, lang }: { step: number; lang: string }) {
|
||
const fr = lang.startsWith('fr')
|
||
const dark = useDarkMode()
|
||
const s = Math.min(step, 4)
|
||
|
||
const ink = dark ? '#EDE7DB' : '#242422'
|
||
const muted = dark ? '#A39C8D' : '#686762'
|
||
const plum = dark ? '#D07AA6' : '#9F3F70'
|
||
const blue = dark ? '#7FA8CC' : '#3F6F9F'
|
||
const line = dark ? '#3B352A' : '#D6D0C6'
|
||
const card = dark ? '#221E17' : '#FFFDF8'
|
||
const paper = dark ? '#17140F' : '#F4F0E8'
|
||
|
||
const pistonY = PISTON_Y[s]
|
||
const showHotPlate = s === 0
|
||
const showColdPlate = s === 2
|
||
const showInsulation = s === 1 || s === 3
|
||
const showQh = s === 0
|
||
const showQc = s === 2
|
||
const showWout = s === 0 || s === 1
|
||
const showWin = s === 2 || s === 3
|
||
const marker = ANCHORS[Math.min(s, 3)]
|
||
const segDone = s // segments A→B (0), B→C (1), C→D (2), D→A (3)
|
||
|
||
return (
|
||
<svg
|
||
viewBox="0 0 680 300"
|
||
className="w-full"
|
||
role="img"
|
||
aria-label={fr ? 'Animation du cycle de Carnot' : 'Carnot cycle animation'}
|
||
style={{ background: paper, display: 'block' }}
|
||
>
|
||
{/* ══ Piston apparatus ══ */}
|
||
<g>
|
||
{/* cylinder walls */}
|
||
<rect x={70} y={40} width={110} height={195} fill={card} stroke={ink} strokeWidth={2} rx={4} />
|
||
{/* gas: cold + hot crossfade */}
|
||
<g style={{ transform: `translateY(${pistonY - 45}px)`, transition: EASE }}>
|
||
<rect x={74} y={45} width={102} height={186 - (pistonY - 45)} fill={blue} opacity={GAS_COLD[s] * 0.35} style={{ transition: 'opacity 600ms ease' }} />
|
||
<rect x={74} y={45} width={102} height={186 - (pistonY - 45)} fill={plum} opacity={GAS_HOT[s] * 0.35} style={{ transition: 'opacity 600ms ease' }} />
|
||
</g>
|
||
{/* piston */}
|
||
<g style={{ transform: `translateY(${pistonY - 95}px)`, transition: EASE }}>
|
||
<rect x={74} y={95} width={102} height={14} fill={ink} rx={3} opacity={0.9} />
|
||
<rect x={118} y={60} width={14} height={36} fill={ink} rx={3} opacity={0.9} />
|
||
<rect x={105} y={48} width={40} height={12} fill={ink} rx={4} opacity={0.9} />
|
||
</g>
|
||
{/* hot plate */}
|
||
<rect x={60} y={240} width={130} height={12} rx={4} fill={plum} opacity={showHotPlate ? 0.9 : s === 4 ? 0 : 0.12} style={{ transition: 'opacity 500ms ease' }} />
|
||
{showHotPlate ? (
|
||
<text x={125} y={266} textAnchor="middle" fontSize={11} fill={plum}>
|
||
{fr ? 'Source chaude' : 'Hot'} T_h
|
||
</text>
|
||
) : null}
|
||
{/* cold plate */}
|
||
<rect x={60} y={240} width={130} height={12} rx={4} fill={blue} opacity={showColdPlate ? 0.9 : s === 4 ? 0 : 0.12} style={{ transition: 'opacity 500ms ease' }} />
|
||
{showColdPlate ? (
|
||
<text x={125} y={266} textAnchor="middle" fontSize={11} fill={blue}>
|
||
{fr ? 'Source froide' : 'Cold'} T_c
|
||
</text>
|
||
) : null}
|
||
{/* insulation */}
|
||
<rect x={60} y={240} width={130} height={12} rx={4} fill="none" stroke={muted} strokeWidth={2} strokeDasharray="6 4" opacity={showInsulation ? 1 : 0} style={{ transition: 'opacity 500ms ease' }} />
|
||
{showInsulation ? (
|
||
<text x={125} y={266} textAnchor="middle" fontSize={11} fill={muted}>
|
||
{fr ? 'Isolant (Q = 0)' : 'Insulated (Q = 0)'}
|
||
</text>
|
||
) : null}
|
||
{/* Q_h arrow in */}
|
||
<g opacity={showQh ? 1 : 0} style={{ transition: 'opacity 400ms ease' }}>
|
||
<line x1={95} y1={238} x2={95} y2={196} stroke={plum} strokeWidth={5} strokeLinecap="round" />
|
||
<polygon points="95,190 89,202 101,202" fill={plum} />
|
||
<text x={70} y={210} fontSize={12} fontWeight={700} fill={plum}>Q_h</text>
|
||
</g>
|
||
{/* Q_c arrow out */}
|
||
<g opacity={showQc ? 1 : 0} style={{ transition: 'opacity 400ms ease' }}>
|
||
<line x1={155} y1={196} x2={155} y2={238} stroke={blue} strokeWidth={5} strokeLinecap="round" />
|
||
<polygon points="155,244 149,232 161,232" fill={blue} />
|
||
<text x={164} y={226} fontSize={12} fontWeight={700} fill={blue}>Q_c</text>
|
||
</g>
|
||
{/* W arrows */}
|
||
<g opacity={showWout ? 1 : 0} style={{ transition: 'opacity 400ms ease' }}>
|
||
<line x1={210} y1={180} x2={210} y2={120} stroke={ink} strokeWidth={4} strokeLinecap="round" />
|
||
<polygon points="210,112 204,124 216,124" fill={ink} />
|
||
<text x={220} y={152} fontSize={12} fontWeight={700} fill={ink}>W</text>
|
||
</g>
|
||
<g opacity={showWin ? 1 : 0} style={{ transition: 'opacity 400ms ease' }}>
|
||
<line x1={210} y1={120} x2={210} y2={180} stroke={ink} strokeWidth={4} strokeLinecap="round" />
|
||
<polygon points="210,188 204,176 216,176" fill={ink} />
|
||
<text x={220} y={152} fontSize={12} fontWeight={700} fill={ink}>W</text>
|
||
</g>
|
||
{/* temperature label inside gas */}
|
||
<text x={125} y={pistonY + 40} textAnchor="middle" fontSize={13} fontWeight={700} fill={ink} style={{ transition: EASE }}>
|
||
{T_LABEL[s]}
|
||
</text>
|
||
</g>
|
||
|
||
{/* ══ P–V diagram ══ */}
|
||
<g>
|
||
<text x={340} y={30} fontSize={12} fontWeight={700} fill={muted} style={{ fontFamily: 'var(--pp-mono, monospace)' }}>
|
||
P–V
|
||
</text>
|
||
{/* axes */}
|
||
<line x1={330} y1={250} x2={660} y2={250} stroke={ink} strokeWidth={1.5} />
|
||
<line x1={330} y1={250} x2={330} y2={30} stroke={ink} strokeWidth={1.5} />
|
||
<text x={655} y={266} fontSize={11} fill={muted} textAnchor="end">V</text>
|
||
<text x={322} y={40} fontSize={11} fill={muted} textAnchor="end">P</text>
|
||
{/* isotherms */}
|
||
<path d="M 395 56 Q 460 130 660 218" fill="none" stroke={plum} strokeWidth={1} strokeDasharray="4 4" opacity={0.5} />
|
||
<text x={620} y={200} fontSize={10} fill={plum}>T_h</text>
|
||
<path d="M 400 175 Q 520 208 660 230" fill="none" stroke={blue} strokeWidth={1} strokeDasharray="4 4" opacity={0.5} />
|
||
<text x={620} y={242} fontSize={10} fill={blue}>T_c</text>
|
||
{/* cycle area (final beat) */}
|
||
<polygon points={CYCLE} fill={plum} opacity={s === 4 ? 0.14 : 0} style={{ transition: 'opacity 600ms ease' }} />
|
||
{s === 4 ? (
|
||
<text x={505} y={150} fontSize={15} fontWeight={800} fill={plum} textAnchor="middle">W</text>
|
||
) : null}
|
||
{/* cycle segments, revealed progressively */}
|
||
<polyline points={`${A.x},${A.y} ${B.x},${B.y}`} fill="none" stroke={ink} strokeWidth={2.5} opacity={segDone >= 0 ? 1 : 0.15} />
|
||
<polyline points={`${B.x},${B.y} ${C.x},${C.y}`} fill="none" stroke={ink} strokeWidth={2.5} opacity={segDone >= 1 ? 1 : 0.15} style={{ transition: 'opacity 500ms ease' }} />
|
||
<polyline points={`${C.x},${C.y} ${D.x},${D.y}`} fill="none" stroke={ink} strokeWidth={2.5} opacity={segDone >= 2 ? 1 : 0.15} style={{ transition: 'opacity 500ms ease' }} />
|
||
<polyline points={`${D.x},${D.y} ${A.x},${A.y}`} fill="none" stroke={ink} strokeWidth={2.5} opacity={segDone >= 3 ? 1 : 0.15} style={{ transition: 'opacity 500ms ease' }} />
|
||
{/* current state marker */}
|
||
<circle cx={marker.x} cy={marker.y} r={6} fill={plum} stroke={card} strokeWidth={2} style={{ transition: EASE }} />
|
||
</g>
|
||
</svg>
|
||
)
|
||
}
|