Feature: Hologramm-Illustrationen zur Timeline
Acht SVG-Szenen im Stil des Hero-Bilds (dunkler Raum, Neon-Deckenlicht, Projektor mit Lichtkegel, schwebende Glas-Panels), jeweils gegenüber dem zugehörigen Timeline-Text; auf Mobilgeräten unter dem Text. Animationen in globals.css, respektieren prefers-reduced-motion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c1c0061492
commit
054be8db07
3 changed files with 960 additions and 4 deletions
|
|
@ -130,3 +130,17 @@
|
||||||
.dark ::selection {
|
.dark ::selection {
|
||||||
background: oklch(0.70 0.22 280 / 0.5);
|
background: oklch(0.70 0.22 280 / 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Timeline-Illustrationen (components/timeline-holo.tsx) ===== */
|
||||||
|
@keyframes tl-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
|
||||||
|
@keyframes tl-spin { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes tl-pulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
|
||||||
|
@keyframes tl-flow { to { stroke-dashoffset: -16; } }
|
||||||
|
@keyframes tl-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
|
||||||
|
@keyframes tl-flicker { 0%, 100% { opacity: 1; } 3% { opacity: 0.82; } 5% { opacity: 1; } 51% { opacity: 0.93; } 53% { opacity: 1; } }
|
||||||
|
.tl-art .tl-blink { animation: tl-blink 1.1s steps(1) infinite; }
|
||||||
|
.tl-art .tl-spin { transform-box: fill-box; transform-origin: center; animation: tl-spin 8s linear infinite; }
|
||||||
|
.tl-art .tl-pulse { animation: tl-pulse 2.4s ease-in-out infinite; }
|
||||||
|
.tl-art .tl-flow { stroke-dasharray: 4 4; animation: tl-flow 1s linear infinite; }
|
||||||
|
.tl-art .tl-bob { animation: tl-bob 4s ease-in-out infinite; }
|
||||||
|
.tl-art .tl-flicker { animation: tl-flicker 5s linear infinite; }
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,64 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
|
import { TimelineHolo, type TimelineArtId } from './timeline-holo'
|
||||||
|
|
||||||
const timeline = [
|
const timeline: { era: string; title: string; description: string; tech: string[]; art: TimelineArtId }[] = [
|
||||||
{
|
{
|
||||||
era: '1976',
|
era: '1976',
|
||||||
title: 'Der Anfang',
|
title: 'Der Anfang',
|
||||||
description: 'Erste Programme in Fortran IV. Lochkarten, Großrechner, Batch-Verarbeitung. Erste Programmierkurse im Rahmen des VWL-Studiums. Die Grundlagen des algorithmischen Denkens.',
|
description: 'Erste Programme in Fortran IV. Lochkarten, Großrechner, Batch-Verarbeitung. Erste Programmierkurse im Rahmen des VWL-Studiums. Die Grundlagen des algorithmischen Denkens.',
|
||||||
tech: ['Fortran IV'],
|
tech: ['Fortran IV'],
|
||||||
|
art: 'lochkarte',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
era: '1984',
|
era: '1984',
|
||||||
title: 'Der erste PC',
|
title: 'Der erste PC',
|
||||||
description: 'Erste Programme in Basic, C und Pascal auf dem Personal Computer unter DOS. Datenbanken, Textverarbeitung, Spiele.',
|
description: 'Erste Programme in Basic, C und Pascal auf dem Personal Computer unter DOS. Datenbanken, Textverarbeitung, Spiele.',
|
||||||
tech: ['Lotus 1-2-3', 'dBASE', 'QBasic', 'Turbo Pascal', 'Norton Commander', 'WordPerfect'],
|
tech: ['Lotus 1-2-3', 'dBASE', 'QBasic', 'Turbo Pascal', 'Norton Commander', 'WordPerfect'],
|
||||||
|
art: 'pc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
era: '1986',
|
era: '1986',
|
||||||
title: 'Systemnahe Programmierung',
|
title: 'Systemnahe Programmierung',
|
||||||
description: 'C und 68000-Assembler auf dem Atari ST. Direkte Hardware-Ansteuerung, Interrupt-Routinen, Speicherverwaltung von Hand. Pseudo-Multitasking. DFÜ mit Akustikkoppler. Maiboxen, FidoNet, MausNet.',
|
description: 'C und 68000-Assembler auf dem Atari ST. Direkte Hardware-Ansteuerung, Interrupt-Routinen, Speicherverwaltung von Hand. Pseudo-Multitasking. DFÜ mit Akustikkoppler. Maiboxen, FidoNet, MausNet.',
|
||||||
tech: ['C', '68000 ASM', 'GFA Basic', 'TOS'],
|
tech: ['C', '68000 ASM', 'GFA Basic', 'TOS'],
|
||||||
|
art: 'atari',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
era: '1990',
|
era: '1990',
|
||||||
title: 'Unix an der Universität',
|
title: 'Unix an der Universität',
|
||||||
description: 'AIX, HP-UX, BSD. Die Philosophie kleiner, zusammenarbeitender Programme. Shell-Scripting als Werkzeug. Internet. Sysop-Aufgaben im Uni-Rechenzentrum.',
|
description: 'AIX, HP-UX, BSD. Die Philosophie kleiner, zusammenarbeitender Programme. Shell-Scripting als Werkzeug. Internet. Sysop-Aufgaben im Uni-Rechenzentrum.',
|
||||||
tech: ['Unix', 'Shell', 'vi', 'Gopher'],
|
tech: ['Unix', 'Shell', 'vi', 'Gopher'],
|
||||||
|
art: 'unix',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
era: '1992',
|
era: '1992',
|
||||||
title: 'Linux entdeckt',
|
title: 'Linux entdeckt',
|
||||||
description: 'Slackware auf 40 Disketten, Debian, SUSE Linux ... Der Beginn einer dauerhaften Begeisterung für offene Systeme und Selbstbestimmung. C, C++, Pascal, Modula, Perl, Shell-Skript. Internet per Modem. Pentesting, Hacking.',
|
description: 'Slackware auf 40 Disketten, Debian, SUSE Linux ... Der Beginn einer dauerhaften Begeisterung für offene Systeme und Selbstbestimmung. C, C++, Pascal, Modula, Perl, Shell-Skript. Internet per Modem. Pentesting, Hacking.',
|
||||||
tech: ['Linux', 'GNU Tools', 'X11'],
|
tech: ['Linux', 'GNU Tools', 'X11'],
|
||||||
|
art: 'linux',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
era: '1995 ff.',
|
era: '1995 ff.',
|
||||||
title: 'World Wide Web',
|
title: 'World Wide Web',
|
||||||
description: 'Permanenter privater Internet-Zugang kommerzieller Anbieter und erste eigene Domain: linix.de. World Wide Web. Eigene Webseiten in HTML und JavaScript programmiert.',
|
description: 'Permanenter privater Internet-Zugang kommerzieller Anbieter und erste eigene Domain: linix.de. World Wide Web. Eigene Webseiten in HTML und JavaScript programmiert.',
|
||||||
tech: ['WWW', 'Netscape', 'HTML', 'Java', 'JavaScript', 'Modem'],
|
tech: ['WWW', 'Netscape', 'HTML', 'Java', 'JavaScript', 'Modem'],
|
||||||
|
art: 'www',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
era: '2000er',
|
era: '2000er',
|
||||||
title: 'Professionelle Entwicklung',
|
title: 'Professionelle Entwicklung',
|
||||||
description: 'Verschiedene Sprachen, verschiedene Domänen. Server-Administration, Automatisierung, Web-Entwicklung. Systemadministration, Networking. — Alles unter Linux.',
|
description: 'Verschiedene Sprachen, verschiedene Domänen. Server-Administration, Automatisierung, Web-Entwicklung. Systemadministration, Networking. — Alles unter Linux.',
|
||||||
tech: ['Perl', 'PHP', 'Bash', 'Python', 'Go', 'Proxmox', 'Docker/Podman'],
|
tech: ['Perl', 'PHP', 'Bash', 'Python', 'Go', 'Proxmox', 'Docker/Podman'],
|
||||||
|
art: 'server',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
era: '2022',
|
era: '2022',
|
||||||
title: 'KI-Fokus',
|
title: 'KI-Fokus',
|
||||||
description: 'Seit Ende 2022 intensive Beschäftigung mit KI-Tools und KI-Programmierung. LLMs als Arbeitswerkzeug und Forschungsgegenstand.',
|
description: 'Seit Ende 2022 intensive Beschäftigung mit KI-Tools und KI-Programmierung. LLMs als Arbeitswerkzeug und Forschungsgegenstand.',
|
||||||
tech: ['Python', 'Rust', 'LLMs', 'ML'],
|
tech: ['Python', 'Rust', 'LLMs', 'ML'],
|
||||||
|
art: 'ki',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -227,7 +236,7 @@ export function AboutSection() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className={`pl-8 sm:pl-12 lg:pl-0 ${index % 2 === 0 ? 'lg:pr-16 lg:text-right' : 'lg:col-start-2 lg:pl-16'}`}>
|
<div className={`pl-8 sm:pl-12 lg:pl-0 ${index % 2 === 0 ? 'lg:pr-16 lg:text-right' : 'lg:col-start-2 lg:row-start-1 lg:pl-16'}`}>
|
||||||
<div className={`lg:direction-ltr ${index % 2 === 0 ? 'lg:ml-auto lg:max-w-md' : 'lg:max-w-md'}`}>
|
<div className={`lg:direction-ltr ${index % 2 === 0 ? 'lg:ml-auto lg:max-w-md' : 'lg:max-w-md'}`}>
|
||||||
<span className="font-mono text-sm text-accent">{item.era}</span>
|
<span className="font-mono text-sm text-accent">{item.era}</span>
|
||||||
<h3 className="font-serif text-xl lg:text-2xl mt-2">{item.title}</h3>
|
<h3 className="font-serif text-xl lg:text-2xl mt-2">{item.title}</h3>
|
||||||
|
|
@ -247,8 +256,10 @@ export function AboutSection() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spacer for alternating layout */}
|
{/* Illustration on the opposite side */}
|
||||||
{index % 2 !== 0 && <div className="hidden lg:block" />}
|
<div className={`pl-8 sm:pl-12 lg:pl-0 ${index % 2 === 0 ? 'lg:pl-16' : 'lg:col-start-1 lg:row-start-1 lg:pr-16'}`}>
|
||||||
|
<TimelineHolo id={item.art} className={`max-w-md ${index % 2 === 0 ? '' : 'lg:ml-auto'}`} />
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
931
components/timeline-holo.tsx
Normal file
931
components/timeline-holo.tsx
Normal file
|
|
@ -0,0 +1,931 @@
|
||||||
|
// Timeline-Illustrationen: Hologramm-Szenen im Stil des Hero-Bilds
|
||||||
|
// (dunkler Raum, Neon-Deckenlicht, Glas-Panels, Projektor auf dem Tisch).
|
||||||
|
// Bewusst themenunabhängig dunkel – wie ein Foto.
|
||||||
|
|
||||||
|
export type TimelineArtId =
|
||||||
|
| 'lochkarte'
|
||||||
|
| 'pc'
|
||||||
|
| 'atari'
|
||||||
|
| 'unix'
|
||||||
|
| 'linux'
|
||||||
|
| 'www'
|
||||||
|
| 'server'
|
||||||
|
| 'ki'
|
||||||
|
|
||||||
|
const MONO = { fontFamily: 'var(--font-jetbrains-mono), ui-monospace, monospace', whiteSpace: 'pre' } as const
|
||||||
|
|
||||||
|
const CY = '#5ff3ff'
|
||||||
|
const CY_TXT = '#a8fbff'
|
||||||
|
const CY_DIM = '#4fc3d4'
|
||||||
|
const MAG = '#ff4fd8'
|
||||||
|
const SCREEN_DARK = '#062a33'
|
||||||
|
const SCREEN_LIGHT = '#d9fbff'
|
||||||
|
|
||||||
|
type U = (name: string) => string
|
||||||
|
|
||||||
|
function holo(u: U) {
|
||||||
|
return {
|
||||||
|
stroke: CY,
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: 1.1,
|
||||||
|
strokeLinecap: 'round' as const,
|
||||||
|
strokeLinejoin: 'round' as const,
|
||||||
|
filter: `url(#${u('glow')})`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function T({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
size = 6,
|
||||||
|
color = CY_TXT,
|
||||||
|
anchor,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
size?: number
|
||||||
|
color?: string
|
||||||
|
anchor?: 'start' | 'middle' | 'end'
|
||||||
|
className?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<text x={x} y={y} fontSize={size} fill={color} stroke="none" textAnchor={anchor} className={className} style={MONO}>
|
||||||
|
{children}
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Gemeinsame Bausteine: Raum, Projektor, Glas-Panel === */
|
||||||
|
|
||||||
|
function Defs({ u }: { u: U }) {
|
||||||
|
return (
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={u('bg')} x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stopColor="#060914" />
|
||||||
|
<stop offset="0.55" stopColor="#0b1226" />
|
||||||
|
<stop offset="1" stopColor="#070a14" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id={u('desk')} x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stopColor="#111a2e" />
|
||||||
|
<stop offset="1" stopColor="#04060b" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id={u('cone')} x1="0" y1="1" x2="0" y2="0">
|
||||||
|
<stop offset="0" stopColor={CY} stopOpacity="0.3" />
|
||||||
|
<stop offset="0.6" stopColor={CY} stopOpacity="0.06" />
|
||||||
|
<stop offset="1" stopColor={CY} stopOpacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id={u('glass')} x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stopColor="#8cf0ff" stopOpacity="0.16" />
|
||||||
|
<stop offset="1" stopColor="#285aa0" stopOpacity="0.05" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id={u('vignette')} cx="0.5" cy="0.45" r="0.75">
|
||||||
|
<stop offset="0.55" stopColor="#000" stopOpacity="0" />
|
||||||
|
<stop offset="1" stopColor="#000" stopOpacity="0.7" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id={u('tuxBody')} cx="0.35" cy="0.3" r="0.8">
|
||||||
|
<stop offset="0" stopColor="#4a505c" />
|
||||||
|
<stop offset="0.45" stopColor="#1a1d24" />
|
||||||
|
<stop offset="1" stopColor="#07080b" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id={u('tuxBelly')} cx="0.4" cy="0.35" r="0.75">
|
||||||
|
<stop offset="0" stopColor="#ffffff" />
|
||||||
|
<stop offset="1" stopColor="#c4c9d2" />
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id={u('tuxOrange')} x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stopColor="#ffd060" />
|
||||||
|
<stop offset="1" stopColor="#f08400" />
|
||||||
|
</linearGradient>
|
||||||
|
<pattern id={u('scan')} width="3" height="3" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="3" height="1" fill="#fff" fillOpacity="0.035" />
|
||||||
|
</pattern>
|
||||||
|
<pattern id={u('construction')} width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
||||||
|
<rect width="4" height="8" fill="#ffb547" />
|
||||||
|
</pattern>
|
||||||
|
<filter id={u('glow')} x="-30%" y="-30%" width="160%" height="160%">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="b" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="b" />
|
||||||
|
<feMergeNode in="b" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<filter id={u('soft')} x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="5" />
|
||||||
|
</filter>
|
||||||
|
<filter id={u('blur')} x="-100%" y="-100%" width="300%" height="300%">
|
||||||
|
<feGaussianBlur stdDeviation="12" />
|
||||||
|
</filter>
|
||||||
|
<filter id={u('grain')}>
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" stitchTiles="stitch" />
|
||||||
|
<feColorMatrix type="saturate" values="0" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const BOKEH: [number, number, number, string, number][] = [
|
||||||
|
[26, 70, 12, '#ff7a3d', 0.35],
|
||||||
|
[46, 128, 7, '#ff3d6e', 0.3],
|
||||||
|
[18, 186, 9, '#7a5cff', 0.25],
|
||||||
|
[374, 84, 11, '#3dd6ff', 0.3],
|
||||||
|
[356, 150, 6, '#ff7a3d', 0.22],
|
||||||
|
[388, 200, 8, MAG, 0.2],
|
||||||
|
]
|
||||||
|
|
||||||
|
function Backdrop({ u }: { u: U }) {
|
||||||
|
const rays = [-240, -120, 0, 100, 200, 300, 400, 520, 640]
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect width="400" height="290" fill={`url(#${u('bg')})`} />
|
||||||
|
|
||||||
|
{/* Rack-Silhouetten an der Rückwand */}
|
||||||
|
{[8, 36, 348, 376].map((x) => (
|
||||||
|
<rect key={x} x={x} y="34" width="22" height="204" fill="#0a1120" stroke="#16223a" strokeWidth="0.8" />
|
||||||
|
))}
|
||||||
|
{BOKEH.map(([x, y, r, c, o], i) => (
|
||||||
|
<circle key={i} cx={x} cy={y} r={r} fill={c} opacity={o} filter={`url(#${u('blur')})`} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Decke in Perspektive mit Neon-Leisten */}
|
||||||
|
<g stroke="#1a2846" strokeWidth="0.6">
|
||||||
|
{rays.map((x) => (
|
||||||
|
<path key={x} d={`M${x} 0 L${x + (200 - x) * 0.25} 30`} />
|
||||||
|
))}
|
||||||
|
<path d="M0 12 H400 M0 24 H400" />
|
||||||
|
</g>
|
||||||
|
<g filter={`url(#${u('soft')})`}>
|
||||||
|
<path d="M74 0 H170 L162 14 H86 Z" fill={MAG} opacity="0.7" />
|
||||||
|
<path d="M232 0 H328 L316 14 H240 Z" fill={MAG} opacity="0.5" />
|
||||||
|
<path d="M176 18 H226 L222 26 H180 Z" fill={CY} opacity="0.4" />
|
||||||
|
</g>
|
||||||
|
<path d="M80 2 H164 L158 11 H90 Z" fill="#ffd2f6" opacity="0.55" />
|
||||||
|
<path d="M238 2 H322 L313 11 H245 Z" fill="#ffd2f6" opacity="0.4" />
|
||||||
|
|
||||||
|
{/* Tisch */}
|
||||||
|
<path d="M0 238 H400 V290 H0 Z" fill={`url(#${u('desk')})`} />
|
||||||
|
<g stroke="#1b2740" strokeWidth="0.6">
|
||||||
|
{[-200, -60, 80, 200, 320, 460, 600].map((x) => (
|
||||||
|
<path key={x} d={`M${x} 290 L${x + (200 - x) * 0.55} 238`} />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
<path d="M0 238 H400" stroke={CY} strokeWidth="1.2" strokeOpacity="0.7" filter={`url(#${u('glow')})`} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Overlay({ u, caption }: { u: U; caption: string }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect width="400" height="290" fill={`url(#${u('scan')})`} pointerEvents="none" />
|
||||||
|
<rect width="400" height="290" filter={`url(#${u('grain')})`} opacity="0.06" pointerEvents="none" />
|
||||||
|
<rect width="400" height="290" fill={`url(#${u('vignette')})`} pointerEvents="none" />
|
||||||
|
<T x={14} y={280} size={6.5} color={CY_DIM}>
|
||||||
|
{caption}
|
||||||
|
</T>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Emitter({ u, cx, w = 100, top = 30 }: { u: U; cx: number; w?: number; top?: number }) {
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d={`M${cx - w / 2 + 6} 243 L${cx + w / 2 - 6} 243 L${cx + w * 1.05} ${top} L${cx - w * 1.05} ${top} Z`}
|
||||||
|
fill={`url(#${u('cone')})`}
|
||||||
|
className="tl-flicker"
|
||||||
|
/>
|
||||||
|
<ellipse cx={cx} cy="244" rx={w / 2} ry="6" fill="#0a2630" stroke={CY} strokeOpacity="0.8" filter={`url(#${u('glow')})`} />
|
||||||
|
<ellipse cx={cx} cy="243" rx={w / 2 - 14} ry="2.5" fill={CY} opacity="0.7" filter={`url(#${u('soft')})`} />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function GlassPanel({
|
||||||
|
u,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
tilt,
|
||||||
|
title,
|
||||||
|
lines = [],
|
||||||
|
size = 5.6,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
u: U
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
tilt: number
|
||||||
|
title: string
|
||||||
|
lines?: string[]
|
||||||
|
size?: number
|
||||||
|
children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${x} ${y}) skewY(${tilt})`} className="tl-flicker">
|
||||||
|
<rect width={w} height={h} rx="4" fill={`url(#${u('glass')})`} stroke={CY} strokeOpacity="0.6" filter={`url(#${u('glow')})`} />
|
||||||
|
<path d={`M0 11 H${w}`} stroke={CY} strokeOpacity="0.35" />
|
||||||
|
{[6, 12, 18].map((cx) => (
|
||||||
|
<circle key={cx} cx={cx} cy="5.5" r="1.6" fill={CY} opacity="0.5" />
|
||||||
|
))}
|
||||||
|
<T x={w - 5} y={8} size={5} color={CY_DIM} anchor="end">
|
||||||
|
{title}
|
||||||
|
</T>
|
||||||
|
{lines.map((line, i) => (
|
||||||
|
<T key={i} x={6} y={21 + i * (size + 3.4)} size={size}>
|
||||||
|
{line}
|
||||||
|
</T>
|
||||||
|
))}
|
||||||
|
{children}
|
||||||
|
<path d={`M${w * 0.55} 0 L${w * 0.75} 0 L${w * 0.35} ${h} L${w * 0.15} ${h} Z`} fill="#fff" opacity="0.04" />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 1976: Lochkarte === */
|
||||||
|
|
||||||
|
const CARD_TEXT = 'C JAMULIX 1976 FORTRAN IV'.padEnd(28, ' ')
|
||||||
|
|
||||||
|
// Zeilenindex: 0 = Zone 12, 1 = Zone 11, 2 = Ziffer 0, 3..11 = Ziffern 1..9
|
||||||
|
function hollerith(ch: string): number[] {
|
||||||
|
if (ch >= '0' && ch <= '9') return [2 + Number(ch)]
|
||||||
|
const c = ch.charCodeAt(0) - 65
|
||||||
|
if (c >= 0 && c < 9) return [0, 3 + c]
|
||||||
|
if (c >= 9 && c < 18) return [1, 3 + c - 9]
|
||||||
|
if (c >= 18 && c < 26) return [2, 4 + c - 18]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function Lochkarte({ u }: { u: U }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Emitter u={u} cx={238} w={120} />
|
||||||
|
<g transform="translate(238 132) skewX(-14) rotate(-5) translate(-110 -52)">
|
||||||
|
<g {...holo(u)} opacity="0.35" transform="translate(8 8)">
|
||||||
|
<path d="M14 0 H220 V104 H0 V14 Z" />
|
||||||
|
</g>
|
||||||
|
<g {...holo(u)} className="tl-flicker">
|
||||||
|
<path d="M14 0 H220 V104 H0 V14 Z" fill={CY} fillOpacity="0.07" />
|
||||||
|
{CARD_TEXT.split('').map((ch, i) => {
|
||||||
|
const x = 14 + i * 7.2
|
||||||
|
const holes = hollerith(ch)
|
||||||
|
return (
|
||||||
|
<g key={i}>
|
||||||
|
<T x={x} y={10} size={5} anchor="middle">
|
||||||
|
{ch}
|
||||||
|
</T>
|
||||||
|
{Array.from({ length: 12 }, (_, r) => {
|
||||||
|
const y = 17 + r * 7.3
|
||||||
|
return holes.includes(r) ? (
|
||||||
|
<rect key={r} x={x - 1.6} y={y - 2.8} width="3.2" height="5.6" fill={MAG} stroke="none" />
|
||||||
|
) : (
|
||||||
|
<circle key={r} cx={x} cy={y} r="0.55" fill={CY} stroke="none" opacity="0.45" />
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Bandspulen */}
|
||||||
|
<g {...holo(u)}>
|
||||||
|
<rect x="300" y="34" width="84" height="44" rx="3" fill={CY} fillOpacity="0.05" strokeOpacity="0.6" />
|
||||||
|
{[322, 362].map((cx, i) => (
|
||||||
|
<g key={cx} className="tl-spin" style={{ animationDuration: i ? '5s' : '7s' }}>
|
||||||
|
<circle cx={cx} cy="56" r="15" />
|
||||||
|
<circle cx={cx} cy="56" r="4" />
|
||||||
|
<path d={`M${cx} 41 V52 M${cx} 60 V71 M${cx - 15} 56 H${cx - 4} M${cx + 4} 56 H${cx + 15}`} strokeOpacity="0.5" />
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
<path d="M322 71 H362" stroke={MAG} />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<GlassPanel
|
||||||
|
u={u}
|
||||||
|
x={14}
|
||||||
|
y={44}
|
||||||
|
w={112}
|
||||||
|
h={98}
|
||||||
|
tilt={7}
|
||||||
|
title="HALLO.FOR"
|
||||||
|
lines={['C ERSTES PROGRAMM', ' PROGRAM HALLO', ' WRITE(6,10)', ' 10 FORMAT(5HHALLO)', ' STOP', ' END', '', '// JOB 0042 BATCH']}
|
||||||
|
size={5.4}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 1984: PC === */
|
||||||
|
|
||||||
|
function Pc({ u }: { u: U }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Emitter u={u} cx={210} w={130} />
|
||||||
|
<g {...holo(u)} className="tl-flicker">
|
||||||
|
<rect x="150" y="46" width="120" height="92" rx="6" fill={CY} fillOpacity="0.06" />
|
||||||
|
<rect x="160" y="54" width="100" height="72" rx="3" fill={SCREEN_DARK} fillOpacity="0.8" strokeOpacity="0.6" />
|
||||||
|
<T x={165} y={66} size={6}>
|
||||||
|
C:\>dir /w
|
||||||
|
</T>
|
||||||
|
<T x={165} y={76} size={6}>
|
||||||
|
TP.EXE DBASE.EXE
|
||||||
|
</T>
|
||||||
|
<T x={165} y={86} size={6}>
|
||||||
|
123.COM NC.EXE
|
||||||
|
</T>
|
||||||
|
<T x={165} y={96} size={6}>
|
||||||
|
WP.EXE QBASIC.EXE
|
||||||
|
</T>
|
||||||
|
<T x={165} y={114} size={6}>
|
||||||
|
C:\>
|
||||||
|
</T>
|
||||||
|
<rect x="181" y="109" width="4" height="6" fill={CY_TXT} stroke="none" className="tl-blink" />
|
||||||
|
<path d="M198 138 H222 L228 146 H192 Z" />
|
||||||
|
<rect x="128" y="146" width="164" height="24" rx="3" fill={CY} fillOpacity="0.05" />
|
||||||
|
<rect x="220" y="151" width="62" height="5" rx="1" />
|
||||||
|
<rect x="220" y="160" width="62" height="5" rx="1" />
|
||||||
|
<circle cx="140" cy="158" r="2" fill={MAG} stroke="none" className="tl-pulse" />
|
||||||
|
<path d="M124 178 H296 L304 204 H116 Z" fill={CY} fillOpacity="0.05" />
|
||||||
|
{Array.from({ length: 3 }, (_, r) =>
|
||||||
|
Array.from({ length: 14 }, (_, i) => (
|
||||||
|
<rect key={`${r}-${i}`} x={130 + i * 11.6 + r * 2} y={183 + r * 6.4} width="8" height="4" rx="0.8" strokeOpacity="0.45" strokeWidth="0.7" />
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<GlassPanel u={u} x={14} y={48} w={104} h={92} tilt={7} title="BUDGET.WK1">
|
||||||
|
{['A', 'B', 'C'].map((c, i) => (
|
||||||
|
<T key={c} x={26 + i * 30} y={22} size={5} color={CY_DIM}>
|
||||||
|
{c}
|
||||||
|
</T>
|
||||||
|
))}
|
||||||
|
{[
|
||||||
|
['Miete', '450', '450'],
|
||||||
|
['Disks', '89', '178'],
|
||||||
|
['Modem', '398', '398'],
|
||||||
|
['RAM', '640', '0'],
|
||||||
|
['Summe', '1577', '1026'],
|
||||||
|
].map((row, r) => (
|
||||||
|
<g key={r}>
|
||||||
|
{row.map((v, i) => (
|
||||||
|
<T key={i} x={8 + i * 30} y={34 + r * 10} size={5.2} color={r === 4 ? MAG : CY_TXT}>
|
||||||
|
{v}
|
||||||
|
</T>
|
||||||
|
))}
|
||||||
|
<path d={`M4 ${37 + r * 10} H100`} stroke={CY} strokeOpacity="0.15" />
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
<path d="M36 14 V88 M66 14 V88" stroke={CY} strokeOpacity="0.15" />
|
||||||
|
</GlassPanel>
|
||||||
|
|
||||||
|
<GlassPanel
|
||||||
|
u={u}
|
||||||
|
x={300}
|
||||||
|
y={40}
|
||||||
|
w={88}
|
||||||
|
h={74}
|
||||||
|
tilt={-7}
|
||||||
|
title="dBASE III+"
|
||||||
|
lines={['. USE KUNDEN', '. LIST NAME', ' 1 Meier', ' 2 Schulz', ' 3 Weber', '. _']}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 5,25"-Diskette */}
|
||||||
|
<g {...holo(u)} className="tl-bob">
|
||||||
|
<g transform="translate(330 150) rotate(12)">
|
||||||
|
<rect x="0" y="0" width="40" height="40" rx="1.5" fill={CY} fillOpacity="0.06" />
|
||||||
|
<circle cx="20" cy="20" r="6" />
|
||||||
|
<rect x="17" y="29" width="6" height="9" rx="2.5" strokeOpacity="0.5" />
|
||||||
|
<rect x="5" y="3" width="24" height="7" stroke={MAG} />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 1986: Atari ST === */
|
||||||
|
|
||||||
|
function Atari({ u }: { u: U }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Emitter u={u} cx={166} w={140} />
|
||||||
|
<g {...holo(u)} className="tl-flicker">
|
||||||
|
<rect x="100" y="36" width="132" height="104" rx="9" fill={CY} fillOpacity="0.06" />
|
||||||
|
<rect x="110" y="45" width="112" height="82" rx="2" fill={SCREEN_LIGHT} fillOpacity="0.85" stroke="none" />
|
||||||
|
<rect x="110" y="45" width="112" height="7" fill={SCREEN_DARK} stroke="none" />
|
||||||
|
<T x={113} y={50.5} size={4.6} color={SCREEN_LIGHT}>
|
||||||
|
Desk File View Options
|
||||||
|
</T>
|
||||||
|
<rect x="116" y="58" width="58" height="34" stroke={SCREEN_DARK} strokeWidth="0.7" />
|
||||||
|
<T x={119} y={70} size={4.6} color={SCREEN_DARK}>
|
||||||
|
move.l d0,(a1)+
|
||||||
|
</T>
|
||||||
|
<T x={119} y={78} size={4.6} color={SCREEN_DARK}>
|
||||||
|
dbra d7,loop
|
||||||
|
</T>
|
||||||
|
<T x={119} y={86} size={4.6} color={SCREEN_DARK}>
|
||||||
|
trap #1
|
||||||
|
</T>
|
||||||
|
<rect x="150" y="98" width="64" height="24" fill={SCREEN_LIGHT} stroke={SCREEN_DARK} strokeWidth="0.7" />
|
||||||
|
{[160, 171, 182].map((x) => (
|
||||||
|
<g key={x}>
|
||||||
|
<circle cx={x} cy="110" r="3.4" fill={SCREEN_DARK} stroke="none" />
|
||||||
|
<path d={`M${x + 2} 107.5 q2.5 -3.5 5 -1.5`} stroke={SCREEN_DARK} strokeWidth="0.8" />
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
<T x={197} y={112} size={5} color={SCREEN_DARK}>
|
||||||
|
OK
|
||||||
|
</T>
|
||||||
|
<rect x="150" y="140" width="32" height="7" />
|
||||||
|
<path d="M84 156 H248 L258 208 H74 Z" fill={CY} fillOpacity="0.05" />
|
||||||
|
{Array.from({ length: 10 }, (_, i) => (
|
||||||
|
<path key={i} d={`M${94 + i * 13} 166 l3 -5 h9 l-3 5 z`} strokeOpacity="0.6" strokeWidth="0.7" />
|
||||||
|
))}
|
||||||
|
{Array.from({ length: 3 }, (_, r) =>
|
||||||
|
Array.from({ length: 15 }, (_, i) => (
|
||||||
|
<rect key={`${r}-${i}`} x={88 + i * 10.8 - r * 1.4} y={174 + r * 8} width="8" height="5" rx="0.8" strokeOpacity="0.45" strokeWidth="0.7" />
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
<T x={232} y={204} size={5} color={CY_DIM}>
|
||||||
|
520ST
|
||||||
|
</T>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<GlassPanel
|
||||||
|
u={u}
|
||||||
|
x={282}
|
||||||
|
y={26}
|
||||||
|
w={104}
|
||||||
|
h={84}
|
||||||
|
tilt={-7}
|
||||||
|
title="BOOT.S"
|
||||||
|
lines={['; 68000 @ 8 MHz', 'vbl: movem.l d0-d7,-(sp)', ' addq.w #1,frames', ' movem.l (sp)+,d0-d7', ' rte', '; 3 Bomben = Adressfehler']}
|
||||||
|
size={5}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Akustikkoppler */}
|
||||||
|
{[0, 0.4, 0.8].map((d, i) => (
|
||||||
|
<path
|
||||||
|
key={i}
|
||||||
|
d={`M${314 - i * 10} ${158 - i * 9} Q336 ${148 - i * 14} ${358 + i * 10} ${158 - i * 9}`}
|
||||||
|
stroke={MAG}
|
||||||
|
strokeWidth="1.3"
|
||||||
|
fill="none"
|
||||||
|
filter={`url(#${u('glow')})`}
|
||||||
|
className="tl-pulse"
|
||||||
|
style={{ animationDelay: `${d}s` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<g {...holo(u)}>
|
||||||
|
<rect x="292" y="186" width="88" height="28" rx="4" fill={CY} fillOpacity="0.06" />
|
||||||
|
<circle cx="314" cy="186" r="10" fill="#0a1a26" />
|
||||||
|
<circle cx="358" cy="186" r="10" fill="#0a1a26" />
|
||||||
|
<path d="M302 178 Q302 166 314 168 H358 Q370 166 370 178" strokeWidth="4" />
|
||||||
|
<circle cx="302" cy="204" r="1.8" fill={MAG} stroke="none" className="tl-pulse" />
|
||||||
|
<T x={312} y={206} size={5}>
|
||||||
|
300 BAUD
|
||||||
|
</T>
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 1990: Unix === */
|
||||||
|
|
||||||
|
function Unix({ u }: { u: U }) {
|
||||||
|
const tools = ['grep', 'sort', 'uniq', 'wc']
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Emitter u={u} cx={318} w={110} />
|
||||||
|
<GlassPanel
|
||||||
|
u={u}
|
||||||
|
x={18}
|
||||||
|
y={34}
|
||||||
|
w={236}
|
||||||
|
h={118}
|
||||||
|
tilt={5}
|
||||||
|
title="tty1 — hp-ux:/home/ds"
|
||||||
|
lines={[
|
||||||
|
'$ who | wc -l',
|
||||||
|
' 23',
|
||||||
|
'$ grep -i err syslog | sort | uniq -c',
|
||||||
|
' 12 err: disk full',
|
||||||
|
' 3 err: no carrier',
|
||||||
|
'$ ls ~/src | wc -l',
|
||||||
|
' 117',
|
||||||
|
'$ vi pipes.sh',
|
||||||
|
'$',
|
||||||
|
]}
|
||||||
|
size={7}
|
||||||
|
>
|
||||||
|
<rect x="16" y="98" width="4.5" height="7.5" fill={CY_TXT} className="tl-blink" />
|
||||||
|
</GlassPanel>
|
||||||
|
|
||||||
|
<g {...holo(u)} className="tl-flicker">
|
||||||
|
{tools.map((t, i) => {
|
||||||
|
const y = 52 + i * 44
|
||||||
|
return (
|
||||||
|
<g key={t}>
|
||||||
|
<rect x="286" y={y} width="64" height="24" rx="4" fill={CY} fillOpacity="0.08" />
|
||||||
|
<T x={318} y={y + 15} size={8} anchor="middle">
|
||||||
|
{t}
|
||||||
|
</T>
|
||||||
|
{i < tools.length - 1 && (
|
||||||
|
<>
|
||||||
|
<path d={`M318 ${y + 24} V${y + 44}`} stroke={MAG} className="tl-flow" />
|
||||||
|
<T x={328} y={y + 37} size={8} color={MAG}>
|
||||||
|
|
|
||||||
|
</T>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
<T x={136} y={196} size={6.5} color={CY_DIM} anchor="middle">
|
||||||
|
Do one thing and do it well.
|
||||||
|
</T>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 1992: Linux === */
|
||||||
|
|
||||||
|
function Disk({ i, label }: { i: number; label?: boolean }) {
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${34 + i * 17} ${76 + i * 9}) rotate(${-16 + i * 6}) scale(0.74)`}>
|
||||||
|
<path d="M0 0 H84 L90 6 V90 H0 Z" fill="#0a2630" fillOpacity="0.55" />
|
||||||
|
<rect x="22" y="0" width="46" height="28" strokeOpacity="0.6" />
|
||||||
|
<rect x="48" y="4" width="11" height="20" strokeOpacity="0.4" />
|
||||||
|
<rect x="10" y="40" width="70" height="46" rx="1" strokeOpacity="0.5" />
|
||||||
|
{label && (
|
||||||
|
<>
|
||||||
|
<T x={15} y={56} size={8}>
|
||||||
|
SLACKWARE
|
||||||
|
</T>
|
||||||
|
<T x={15} y={68} size={7} color={CY_DIM}>
|
||||||
|
disk a1 / 40
|
||||||
|
</T>
|
||||||
|
<path d="M15 76 H72" stroke={MAG} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Linux({ u }: { u: U }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Emitter u={u} cx={112} w={130} />
|
||||||
|
<g {...holo(u)} className="tl-flicker">
|
||||||
|
{[0, 1, 2, 3, 4].map((i) => (
|
||||||
|
<Disk key={i} i={i} label={i === 4} />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<GlassPanel u={u} x={186} y={24} w={128} h={58} tilt={-4} title="setup" lines={['Installing series A ...', 'Bitte Disk a7 einlegen']}>
|
||||||
|
<rect x="6" y="38" width="116" height="6" rx="3" stroke={CY} strokeOpacity="0.5" />
|
||||||
|
<rect x="7" y="39" width="20" height="4" rx="2" fill={MAG}>
|
||||||
|
<animate attributeName="width" values="4;114;114" keyTimes="0;0.85;1" dur="7s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
<T x={6} y={53} size={5} color={CY_DIM}>
|
||||||
|
40 × 1,44 MB · Kernel 0.99
|
||||||
|
</T>
|
||||||
|
</GlassPanel>
|
||||||
|
|
||||||
|
{/* Tux, „echt“ auf dem Tisch */}
|
||||||
|
<ellipse cx="306" cy="240" rx="58" ry="6" fill="#000" opacity="0.7" filter={`url(#${u('soft')})`} />
|
||||||
|
<g transform="translate(306 238) scale(0.86) translate(-300 -225)">
|
||||||
|
<path
|
||||||
|
d="M300 64 C328 64 336 90 334 110 C356 132 356 188 346 208 C336 226 264 226 254 208 C244 188 244 132 266 110 C264 90 272 64 300 64 Z"
|
||||||
|
fill={`url(#${u('tuxBody')})`}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M266 110 C244 132 244 188 254 208"
|
||||||
|
stroke={CY}
|
||||||
|
strokeOpacity="0.55"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
fill="none"
|
||||||
|
filter={`url(#${u('glow')})`}
|
||||||
|
/>
|
||||||
|
<ellipse cx="300" cy="170" rx="31" ry="44" fill={`url(#${u('tuxBelly')})`} />
|
||||||
|
<ellipse cx="290" cy="94" rx="7" ry="9" fill={`url(#${u('tuxBelly')})`} />
|
||||||
|
<ellipse cx="310" cy="94" rx="7" ry="9" fill={`url(#${u('tuxBelly')})`} />
|
||||||
|
<circle cx="292" cy="96" r="3.2" fill="#0b0d12" />
|
||||||
|
<circle cx="308" cy="96" r="3.2" fill="#0b0d12" />
|
||||||
|
<circle cx="293" cy="94.8" r="1" fill="#fff" />
|
||||||
|
<circle cx="309" cy="94.8" r="1" fill="#fff" />
|
||||||
|
<path d="M286 110 Q300 102 314 110 Q300 123 286 110 Z" fill={`url(#${u('tuxOrange')})`} />
|
||||||
|
<ellipse cx="279" cy="220" rx="18" ry="7" fill={`url(#${u('tuxOrange')})`} />
|
||||||
|
<ellipse cx="321" cy="220" rx="18" ry="7" fill={`url(#${u('tuxOrange')})`} />
|
||||||
|
<ellipse cx="288" cy="80" rx="10" ry="5" fill="#fff" opacity="0.12" />
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 1995: WWW === */
|
||||||
|
|
||||||
|
const CHIRP = Array.from({ length: 110 }, (_, i) => {
|
||||||
|
const t = i / 109
|
||||||
|
const x = 244 + i * 1.25
|
||||||
|
const y = 212 + Math.sin(i * (0.25 + t * 0.9)) * (3 + 7 * t)
|
||||||
|
return `${x.toFixed(1)},${y.toFixed(1)}`
|
||||||
|
}).join(' ')
|
||||||
|
|
||||||
|
function Www({ u }: { u: U }) {
|
||||||
|
const cx = 128
|
||||||
|
const cy = 128
|
||||||
|
const r = 74
|
||||||
|
const nodes: [number, number][] = [
|
||||||
|
[96, 92],
|
||||||
|
[160, 84],
|
||||||
|
[178, 150],
|
||||||
|
[110, 176],
|
||||||
|
[134, 128],
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Emitter u={u} cx={cx} w={124} />
|
||||||
|
<g {...holo(u)} className="tl-flicker">
|
||||||
|
<circle cx={cx} cy={cy} r={r} fill={CY} fillOpacity="0.05" />
|
||||||
|
{[0, 1.5, 3, 4.5].map((d) => (
|
||||||
|
<ellipse key={d} cx={cx} cy={cy} rx={r} ry={r} strokeOpacity="0.4" strokeWidth="0.8">
|
||||||
|
<animate attributeName="rx" values={`${r};0;${r}`} dur="6s" begin={`-${d}s`} repeatCount="indefinite" />
|
||||||
|
</ellipse>
|
||||||
|
))}
|
||||||
|
{[-0.75, -0.4, 0, 0.4, 0.75].map((lat) => {
|
||||||
|
const y = cy + lat * r
|
||||||
|
const w = Math.sqrt(1 - lat * lat) * r
|
||||||
|
return <ellipse key={lat} cx={cx} cy={y} rx={w} ry={w * 0.12} strokeOpacity="0.35" strokeWidth="0.8" />
|
||||||
|
})}
|
||||||
|
<path d="M96 92 Q110 52 160 84" stroke={MAG} className="tl-flow" />
|
||||||
|
<path d="M160 84 Q210 114 178 150" stroke={MAG} className="tl-flow" />
|
||||||
|
<path d="M178 150 Q150 206 110 176" stroke={MAG} className="tl-flow" />
|
||||||
|
<path d="M110 176 Q70 140 96 92" className="tl-flow" />
|
||||||
|
{nodes.map(([x, y], i) => (
|
||||||
|
<circle key={i} cx={x} cy={y} r="3.4" fill={MAG} stroke="none" className="tl-pulse" style={{ animationDelay: `${i * 0.35}s` }} />
|
||||||
|
))}
|
||||||
|
<path d="M178 150 Q214 140 236 118" className="tl-flow" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<GlassPanel u={u} x={238} y={30} w={148} h={142} tilt={-7} title="Netscape">
|
||||||
|
<rect x="6" y="15" width="136" height="10" rx="2" stroke={CY} strokeOpacity="0.4" />
|
||||||
|
<T x={10} y={22.5} size={5.5}>
|
||||||
|
http://www.linix.de/
|
||||||
|
</T>
|
||||||
|
<rect x="8" y="32" width="78" height="8" fill={MAG} opacity="0.85" />
|
||||||
|
{[48, 55, 62, 69].map((y, i) => (
|
||||||
|
<path key={y} d={`M8 ${y} H${136 - i * 14}`} stroke={CY} strokeOpacity="0.35" />
|
||||||
|
))}
|
||||||
|
<T x={8} y={84} size={5.5} color="#8ab4ff" className="underline">
|
||||||
|
<a href="links.html">
|
||||||
|
</T>
|
||||||
|
<rect x="8" y="92" width="132" height="14" fill={`url(#${u('construction')})`} opacity="0.9" />
|
||||||
|
<rect x="30" y="94.5" width="88" height="9" fill="#0b1226" />
|
||||||
|
<T x={74} y={101} size={5} anchor="middle" color="#ffb547">
|
||||||
|
UNDER CONSTRUCTION
|
||||||
|
</T>
|
||||||
|
<T x={8} y={124} size={5.5} color={CY_DIM}>
|
||||||
|
Besucher:
|
||||||
|
</T>
|
||||||
|
<rect x="44" y="116" width="40" height="11" fill={SCREEN_DARK} stroke={CY} strokeOpacity="0.4" />
|
||||||
|
<T x={47} y={124} size={6.5}>
|
||||||
|
000042
|
||||||
|
</T>
|
||||||
|
</GlassPanel>
|
||||||
|
|
||||||
|
<polyline points={CHIRP} stroke={MAG} strokeWidth="1.1" fill="none" filter={`url(#${u('glow')})`} />
|
||||||
|
<T x={244} y={230} size={5.5} color={CY_DIM}>
|
||||||
|
ATDT … 28.8k CONNECT
|
||||||
|
</T>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 2000er: Server === */
|
||||||
|
|
||||||
|
function IsoBox({ cx, y0, s, label }: { cx: number; y0: number; s: number; label: string }) {
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<path d={`M${cx} ${y0} L${cx + s} ${y0 + s / 2} L${cx} ${y0 + s} L${cx - s} ${y0 + s / 2} Z`} fill={CY} fillOpacity="0.12" />
|
||||||
|
<path d={`M${cx - s} ${y0 + s / 2} L${cx} ${y0 + s} L${cx} ${y0 + 2 * s} L${cx - s} ${y0 + 1.5 * s} Z`} fill={MAG} fillOpacity="0.1" />
|
||||||
|
<path d={`M${cx} ${y0 + s} L${cx + s} ${y0 + s / 2} L${cx + s} ${y0 + 1.5 * s} L${cx} ${y0 + 2 * s} Z`} fill={CY} fillOpacity="0.05" />
|
||||||
|
<T x={cx - s / 2} y={y0 + 1.25 * s + 3} size={7} anchor="middle">
|
||||||
|
{label}
|
||||||
|
</T>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Server({ u }: { u: U }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Emitter u={u} cx={106} w={100} />
|
||||||
|
<Emitter u={u} cx={300} w={110} top={110} />
|
||||||
|
<g {...holo(u)} className="tl-flicker">
|
||||||
|
<rect x="70" y="38" width="72" height="192" rx="3" fill={CY} fillOpacity="0.05" />
|
||||||
|
{Array.from({ length: 7 }, (_, i) => {
|
||||||
|
const y = 48 + i * 25.5
|
||||||
|
return (
|
||||||
|
<g key={i}>
|
||||||
|
<rect x="78" y={y} width="56" height="18" rx="1.5" strokeOpacity="0.7" />
|
||||||
|
<circle
|
||||||
|
cx="85"
|
||||||
|
cy={y + 9}
|
||||||
|
r="2"
|
||||||
|
fill={i % 3 === 0 ? MAG : CY}
|
||||||
|
stroke="none"
|
||||||
|
className="tl-pulse"
|
||||||
|
style={{ animationDelay: `${(i * 0.37) % 2}s`, animationDuration: `${1.2 + (i % 4) * 0.4}s` }}
|
||||||
|
/>
|
||||||
|
<path d={`M92 ${y + 6} H110 M92 ${y + 12} H110`} strokeOpacity="0.4" />
|
||||||
|
<rect x="114" y={y + 5} width="16" height="8" rx="1" strokeOpacity="0.4" />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<path d="M134 99 C190 99 210 170 270 176" stroke={CY} fill="none" className="tl-flow" filter={`url(#${u('glow')})`} />
|
||||||
|
<path d="M134 150 C180 150 220 206 300 210" stroke={MAG} fill="none" className="tl-flow" filter={`url(#${u('glow')})`} />
|
||||||
|
|
||||||
|
<g {...holo(u)} className="tl-flicker">
|
||||||
|
<IsoBox cx={278} y0={172} s={26} label="db" />
|
||||||
|
<IsoBox cx={330} y0={172} s={26} label="web" />
|
||||||
|
<IsoBox cx={278} y0={146} s={26} label="app" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<GlassPanel
|
||||||
|
u={u}
|
||||||
|
x={232}
|
||||||
|
y={26}
|
||||||
|
w={150}
|
||||||
|
h={66}
|
||||||
|
tilt={-6}
|
||||||
|
title="ssh root@srv01"
|
||||||
|
lines={['$ podman ps --format', 'web Up 42 days', 'db Up 42 days', 'app Up 3 hours', '$ ansible-playbook site.yml']}
|
||||||
|
size={5.6}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 2022: KI === */
|
||||||
|
|
||||||
|
const BRAIN_NODES: [number, number][] = [
|
||||||
|
[78, 112], [96, 84], [122, 72], [150, 62], [176, 72], [200, 86], [214, 112], [204, 140], [178, 146],
|
||||||
|
[150, 156], [118, 152], [92, 138], [110, 108], [140, 96], [168, 108], [132, 130], [160, 132], [188, 120],
|
||||||
|
]
|
||||||
|
const BRAIN_EDGES: [number, number][] = []
|
||||||
|
BRAIN_NODES.forEach(([x1, y1], i) =>
|
||||||
|
BRAIN_NODES.forEach(([x2, y2], j) => {
|
||||||
|
if (j > i && Math.hypot(x2 - x1, y2 - y1) < 38) BRAIN_EDGES.push([i, j])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
function Ki({ u }: { u: U }) {
|
||||||
|
const probs: [string, number][] = [
|
||||||
|
['Linux', 0.71],
|
||||||
|
['Code', 0.17],
|
||||||
|
['Kaffee', 0.08],
|
||||||
|
['Windows', 0.01],
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Emitter u={u} cx={146} w={130} />
|
||||||
|
<g {...holo(u)} className="tl-flicker">
|
||||||
|
{/* Großhirn */}
|
||||||
|
<path
|
||||||
|
d="M70 120 C62 88 88 58 128 54 C160 46 200 54 220 80 C236 100 232 128 216 140 C206 148 192 148 182 146 C176 156 160 162 142 160 C122 160 104 156 92 148 C78 142 72 132 70 120 Z"
|
||||||
|
fill={CY}
|
||||||
|
fillOpacity="0.06"
|
||||||
|
/>
|
||||||
|
{/* Kleinhirn und Hirnstamm */}
|
||||||
|
<path d="M182 146 C188 162 206 166 216 158 C222 152 221 145 216 140" fill={CY} fillOpacity="0.06" />
|
||||||
|
<path d="M190 152 C198 157 208 157 214 151 M192 158 C200 161 208 161 212 157" strokeOpacity="0.35" strokeWidth="0.7" />
|
||||||
|
<path d="M168 158 C172 172 176 186 174 206" />
|
||||||
|
{/* Furchen */}
|
||||||
|
<g strokeOpacity="0.3" strokeWidth="0.9">
|
||||||
|
<path d="M98 128 C120 118 146 116 170 108" />
|
||||||
|
<path d="M150 56 C146 80 156 96 148 116" />
|
||||||
|
<path d="M86 98 C96 90 104 100 116 92" />
|
||||||
|
<path d="M176 66 C186 80 198 76 206 92" />
|
||||||
|
<path d="M120 140 C134 132 150 146 166 136" />
|
||||||
|
<path d="M184 116 C194 108 206 120 220 112" />
|
||||||
|
</g>
|
||||||
|
{BRAIN_EDGES.map(([a, b]) => (
|
||||||
|
<path
|
||||||
|
key={`${a}-${b}`}
|
||||||
|
d={`M${BRAIN_NODES[a][0]} ${BRAIN_NODES[a][1]} L${BRAIN_NODES[b][0]} ${BRAIN_NODES[b][1]}`}
|
||||||
|
stroke={(a + b) % 5 === 0 ? MAG : CY}
|
||||||
|
strokeOpacity={(a + b) % 5 === 0 ? 0.8 : 0.35}
|
||||||
|
strokeWidth="0.8"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{BRAIN_NODES.map(([x, y], i) => (
|
||||||
|
<circle
|
||||||
|
key={i}
|
||||||
|
cx={x}
|
||||||
|
cy={y}
|
||||||
|
r="2.6"
|
||||||
|
fill={i % 4 === 0 ? MAG : CY_TXT}
|
||||||
|
stroke="none"
|
||||||
|
className="tl-pulse"
|
||||||
|
style={{ animationDelay: `${(i * 0.23) % 2.4}s` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<path d="M214 112 C240 112 248 96 268 92" stroke={MAG} fill="none" className="tl-flow" filter={`url(#${u('glow')})`} />
|
||||||
|
|
||||||
|
<GlassPanel u={u} x={262} y={30} w={124} h={150} tilt={-6} title="next_token()">
|
||||||
|
<T x={6} y={24} size={5.5} color={CY_DIM}>
|
||||||
|
> Je schlauer, desto
|
||||||
|
</T>
|
||||||
|
{probs.map(([word, p], i) => {
|
||||||
|
const y = 44 + i * 24
|
||||||
|
return (
|
||||||
|
<g key={word}>
|
||||||
|
<T x={6} y={y} size={7} color={i === 0 ? MAG : CY_TXT}>
|
||||||
|
{word}
|
||||||
|
</T>
|
||||||
|
<T x={116} y={y} size={5.5} anchor="end" color={CY_DIM}>
|
||||||
|
{p.toFixed(2)}
|
||||||
|
</T>
|
||||||
|
<rect x="6" y={y + 5} width="110" height="4" rx="2" fill={CY} opacity="0.12" />
|
||||||
|
<rect x="6" y={y + 5} width={Math.max(2, p * 110)} height="4" rx="2" fill={i === 0 ? MAG : CY} filter={`url(#${u('glow')})`} />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<T x={6} y={142} size={5} color={CY_DIM}>
|
||||||
|
temperature 0.7 · top_p 0.9
|
||||||
|
</T>
|
||||||
|
</GlassPanel>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCENES: Record<TimelineArtId, { caption: string; alt: string; Scene: (p: { u: U }) => React.JSX.Element }> = {
|
||||||
|
lochkarte: {
|
||||||
|
caption: 'FIG.01 // 1976 // LOCHKARTE · HOLLERITH-CODE',
|
||||||
|
alt: 'Hologramm einer Lochkarte mit gestanzter Zeile „C JAMULIX 1976 FORTRAN IV“, daneben ein Fortran-Listing und Bandspulen',
|
||||||
|
Scene: Lochkarte,
|
||||||
|
},
|
||||||
|
pc: {
|
||||||
|
caption: 'FIG.02 // 1984 // PC UNTER DOS',
|
||||||
|
alt: 'Hologramm eines PCs mit DOS-Prompt, daneben schwebende Panels mit Lotus-Tabelle und dBASE',
|
||||||
|
Scene: Pc,
|
||||||
|
},
|
||||||
|
atari: {
|
||||||
|
caption: 'FIG.03 // 1986 // ATARI ST · 68000 · DFÜ',
|
||||||
|
alt: 'Hologramm eines Atari ST mit GEM-Desktop und Bomben-Dialog, 68000-Assembler-Panel und Akustikkoppler',
|
||||||
|
Scene: Atari,
|
||||||
|
},
|
||||||
|
unix: {
|
||||||
|
caption: 'FIG.04 // 1990 // UNIX-SHELL UND PIPES',
|
||||||
|
alt: 'Schwebendes Terminal mit Unix-Shell-Befehlen und eine Pipeline aus grep, sort, uniq und wc als Hologramm',
|
||||||
|
Scene: Unix,
|
||||||
|
},
|
||||||
|
linux: {
|
||||||
|
caption: 'FIG.05 // 1992 // SLACKWARE AUF 40 DISKETTEN',
|
||||||
|
alt: 'Hologramm gefächerter Slackware-Disketten mit Installationsfortschritt, daneben der Linux-Pinguin Tux auf dem Tisch',
|
||||||
|
Scene: Linux,
|
||||||
|
},
|
||||||
|
www: {
|
||||||
|
caption: 'FIG.06 // 1995 // WORLD WIDE WEB',
|
||||||
|
alt: 'Rotierender Hologramm-Globus mit Hyperlinks, Browser-Panel von linix.de mit Besucherzähler und Modem-Signal',
|
||||||
|
Scene: Www,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
caption: 'FIG.07 // 2000ER // RACK · CONTAINER · AUTOMATISIERUNG',
|
||||||
|
alt: 'Hologramm eines Server-Racks mit blinkenden LEDs, verbunden mit Containern, daneben ein SSH-Panel',
|
||||||
|
Scene: Server,
|
||||||
|
},
|
||||||
|
ki: {
|
||||||
|
caption: 'FIG.08 // 2022 // SPRACHMODELL · NÄCHSTES TOKEN',
|
||||||
|
alt: 'Gehirn aus Netzwerkknoten als Hologramm; ein Panel zeigt „Linux“ mit 71 Prozent als nächstes Token nach „Je schlauer, desto“',
|
||||||
|
Scene: Ki,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TimelineHolo({ id, className = '' }: { id: TimelineArtId; className?: string }) {
|
||||||
|
const { caption, alt, Scene } = SCENES[id]
|
||||||
|
const u: U = (name) => `holo-${id}-${name}`
|
||||||
|
return (
|
||||||
|
<figure
|
||||||
|
className={`tl-art rounded-2xl overflow-hidden ring-1 ring-white/10 shadow-[0_0_48px_-12px_oklch(0.75_0.14_200/0.45)] ${className}`}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 400 290" role="img" aria-label={alt} className="block w-full h-auto bg-[#070a14]">
|
||||||
|
<Defs u={u} />
|
||||||
|
<Backdrop u={u} />
|
||||||
|
<Scene u={u} />
|
||||||
|
<Overlay u={u} caption={caption} />
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue