Compare commits

...
Author SHA1 Message Date
30d87187cd Fix: 404 beim Nachladen auf jamulix.de/ beseitigt
Unter / lädt Next.js die RSC-Segmente ohne basePath (/__next.*.txt).
Die Deploy-Skripte verlinken diese Dateien jetzt wie index.html in den
Webroot. Vercel Analytics entfernt (funktioniert nur auf Vercel).
README angepasst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:44:55 +02:00
74b8d18991 Deploy: index.html wieder als Symlink auf landingpage/index.html
nginx folgt dem Link und liefert die Landingpage direkt unter
https://jamulix.de/ aus. Weiterleitungsseite webroot/index.html entfernt,
README angepasst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:07:24 +02:00
4971cd4fee Docs: README um Timeline-Illustrationen, Fehlerseiten und webroot/ ergänzt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:51:14 +02:00
054be8db07 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>
2026-10-07 19:45:01 +02:00
c1c0061492 Fix: jamulix.de/ leitet wieder auf /landingpage/ weiter
Statt Symlink index.html → landingpage/index.html legen die Deploy-Skripte
jetzt webroot/index.html (meta-refresh + location.replace) in den Webroot.
README: Deploy-Schritte aktualisiert, Fehlerseiten-Konfig auf nginx
(error_page) statt Apache korrigiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:21:48 +02:00
7 changed files with 1009 additions and 20 deletions

View file

@ -20,18 +20,22 @@ Next.js 16 Landing Page für [jamulix.de](https://jamulix.de), statisch exportie
app/ app/
layout.tsx # Root Layout, Fonts, Theme-Init-Script layout.tsx # Root Layout, Fonts, Theme-Init-Script
page.tsx # Hauptseite (alle Sections zusammengesetzt) page.tsx # Hauptseite (alle Sections zusammengesetzt)
globals.css # CSS-Variablen (OKLCH), Tailwind, Fly.io-Farbschema globals.css # CSS-Variablen (OKLCH), Tailwind, Fly.io-Farbschema, Timeline-Animationen
impressum/ # Impressum-Seite impressum/ # Impressum-Seite
datenschutz/ # Datenschutzerklärung datenschutz/ # Datenschutzerklärung
not-found.tsx # 404-Seite
401/ 403/ 500/ 502/ 503/ 504/ # weitere Fehlerseiten
components/ components/
hero.tsx # Hero-Section mit Gradient-Überschrift hero.tsx # Hero-Section mit Gradient-Überschrift
about-section.tsx about-section.tsx # Motivation, Jamulix-Gesetze, Timeline
timeline-holo.tsx # Hologramm-Illustrationen zur Timeline (SVG)
projects-section.tsx projects-section.tsx
focus-section.tsx focus-section.tsx
philosophy-section.tsx philosophy-section.tsx
footer.tsx # Footer mit E-Mail (obfuskiert) und Forgejo-Link footer.tsx # Footer mit E-Mail (obfuskiert) und Forgejo-Link
header.tsx # Navigation + Theme-Toggle header.tsx # Navigation + Theme-Toggle
programmer-graphic.tsx # Foto (statischer Import wegen basePath) programmer-graphic.tsx # Foto (statischer Import wegen basePath)
error-page.tsx # Gemeinsames Layout der Fehlerseiten
theme-toggle.tsx theme-toggle.tsx
ui/ # shadcn/ui-Basiskomponenten ui/ # shadcn/ui-Basiskomponenten
public/ public/
@ -98,6 +102,9 @@ Die E-Mail im Footer (`footer.tsx`) wird als Prop von `app/page.tsx` übergeben:
- Öffnet eine SSH-Master-Verbindung (Passwort nur einmal eingeben) - Öffnet eine SSH-Master-Verbindung (Passwort nur einmal eingeben)
- rsync nach `jamulix.de:/var/www/jamulix.de/html/landingpage/` - rsync nach `jamulix.de:/var/www/jamulix.de/html/landingpage/`
- Setzt Symlink: `/var/www/jamulix.de/html/index.html` → `landingpage/index.html` - Setzt Symlink: `/var/www/jamulix.de/html/index.html` → `landingpage/index.html`
(nginx folgt dem Link und liefert die Landingpage direkt unter `https://jamulix.de/` aus)
- Setzt Symlinks `__next.*.txt` → `landingpage/__next.*.txt` (Next.js lädt unter `/` die
RSC-Segmente ohne basePath; ohne die Links gibt es 404 im Netzwerk-Tab)
- Kopiert die Fehlerseiten `landingpage/<code>.html` in den Webroot - Kopiert die Fehlerseiten `landingpage/<code>.html` in den Webroot
### Localhost (zum Testen) ### Localhost (zum Testen)
@ -108,6 +115,7 @@ Die E-Mail im Footer (`footer.tsx`) wird als Prop von `app/page.tsx` übergeben:
- rsync nach `/var/www/html/landingpage/` - rsync nach `/var/www/html/landingpage/`
- Symlink: `/var/www/html/index.html` → `landingpage/index.html` - Symlink: `/var/www/html/index.html` → `landingpage/index.html`
- Symlinks: `/var/www/html/__next.*.txt` → `landingpage/__next.*.txt`
- Kopiert die Fehlerseiten in den Webroot - Kopiert die Fehlerseiten in den Webroot
- `sudo chown -R www-data:www-data` - `sudo chown -R www-data:www-data`
@ -134,20 +142,19 @@ Gemeinsames Layout: `components/error-page.tsx`. Der Build erzeugt
`<webroot>/<code>.html`. Alle Asset-Pfade sind absolut (`/landingpage/_next/…`), `<webroot>/<code>.html`. Alle Asset-Pfade sind absolut (`/landingpage/_next/…`),
daher funktionieren die Kopien im Webroot genauso. daher funktionieren die Kopien im Webroot genauso.
Damit Apache sie ausliefert, braucht die vHost-Konfiguration (nicht Teil Der nginx-`server`-Block für jamulix.de (nicht Teil dieses Repos) enthält dafür:
dieses Repos) z. B.:
```apache ```nginx
ErrorDocument 401 /401.html error_page 401 /401.html;
ErrorDocument 403 /403.html error_page 403 /403.html;
ErrorDocument 404 /404.html error_page 404 /404.html;
ErrorDocument 500 /500.html error_page 500 /500.html;
ErrorDocument 502 /502.html error_page 502 /502.html;
ErrorDocument 503 /503.html error_page 503 /503.html;
ErrorDocument 504 /504.html error_page 504 /504.html;
``` ```
- `ErrorDocument 401` muss ein lokaler Pfad sein, keine vollständige URL. - Ziel muss ein lokaler Pfad sein, keine vollständige URL (sonst wird daraus ein Redirect).
- Die 401-Seite erscheint erst, wenn der Login-Dialog des Browsers abgebrochen wird. - Die 401-Seite erscheint erst, wenn der Login-Dialog des Browsers abgebrochen wird.
--- ---
@ -165,6 +172,23 @@ ErrorDocument 504 /504.html
Dark/Light Mode wird per `localStorage` gespeichert. Das Init-Script in Dark/Light Mode wird per `localStorage` gespeichert. Das Init-Script in
`app/layout.tsx` verhindert Flash beim Laden. `app/layout.tsx` verhindert Flash beim Laden.
### Timeline-Illustrationen
`components/timeline-holo.tsx` enthält acht SVG-Szenen, je eine pro
Timeline-Station (Lochkarte, PC, Atari ST, Unix, Linux, WWW, Server, KI).
Stil angelehnt an das Hero-Foto: dunkler Raum, Magenta-Deckenlicht,
Hologramm-Projektor mit Lichtkegel, schwebende Glas-Panels.
- Zuordnung über das Feld `art` im `timeline`-Array in `about-section.tsx`;
neue Szene = neuer `TimelineArtId` + Eintrag in `SCENES`.
- Desktop: Grafik auf der dem Text gegenüberliegenden Seite; mobil darunter.
- Die Szenen sind in beiden Themes dunkel (wie das Hero-Foto).
- Reines SVG, keine Bilddateien. Filter-/Gradient-IDs sind pro Szene
präfixiert (`holo-<id>-…`), da alle acht SVGs auf einer Seite stehen.
- Animationen (`tl-blink`, `tl-pulse`, `tl-flow`, `tl-flicker` …) stehen in
`app/globals.css` und werden bei `prefers-reduced-motion` abgeschaltet.
- Die Lochkarte ist echter Hollerith-Code („C JAMULIX 1976 FORTRAN IV“).
--- ---
## Bilder ## Bilder

View file

@ -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; }

View file

@ -1,6 +1,5 @@
import type { Metadata, Viewport } from 'next' import type { Metadata, Viewport } from 'next'
import { Inter, Plus_Jakarta_Sans, JetBrains_Mono } from 'next/font/google' import { Inter, Plus_Jakarta_Sans, JetBrains_Mono } from 'next/font/google'
import { Analytics } from '@vercel/analytics/next'
import './globals.css' import './globals.css'
const inter = Inter({ const inter = Inter({
@ -90,7 +89,6 @@ export default function RootLayout({
</head> </head>
<body className="font-sans antialiased min-h-screen"> <body className="font-sans antialiased min-h-screen">
{children} {children}
{process.env.NODE_ENV === 'production' && <Analytics />}
</body> </body>
</html> </html>
) )

View file

@ -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>

View 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:\&gt;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:\&gt;
</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">
&lt;a href=&quot;links.html&quot;&gt;
</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}>
&gt; 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>
)
}

View file

@ -18,6 +18,13 @@ echo "Setze symbolischen Link: index.html → landingpage/index.html"
sudo rm -f /var/www/html/index.html sudo rm -f /var/www/html/index.html
sudo ln -s landingpage/index.html /var/www/html/index.html sudo ln -s landingpage/index.html /var/www/html/index.html
# Next.js lädt unter / die RSC-Segmente ohne basePath (/__next.*.txt) — ebenfalls verlinken
echo "Setze symbolische Links: __next.*.txt → landingpage/__next.*.txt"
sudo rm -f /var/www/html/__next.*.txt
for f in landingpage/__next.*.txt; do
sudo ln -s "$f" "/var/www/html/$(basename "$f")"
done
echo "Kopiere Fehlerseiten: landingpage/<code>.html → <code>.html" echo "Kopiere Fehlerseiten: landingpage/<code>.html → <code>.html"
for c in $ERROR_PAGES; do for c in $ERROR_PAGES; do
sudo cp "landingpage/$c.html" "/var/www/html/$c.html" sudo cp "landingpage/$c.html" "/var/www/html/$c.html"

View file

@ -31,6 +31,10 @@ rsync -av --delete -e "ssh -S $SSH_SOCKET" landingpage/ "$REMOTE_USER@$REMOTE_HO
echo "Setze symbolischen Link: index.html → landingpage/index.html" echo "Setze symbolischen Link: index.html → landingpage/index.html"
ssh -S "$SSH_SOCKET" "$REMOTE_USER@$REMOTE_HOST" "rm -f '$REMOTE_DIR/index.html' && ln -s landingpage/index.html '$REMOTE_DIR/index.html'" ssh -S "$SSH_SOCKET" "$REMOTE_USER@$REMOTE_HOST" "rm -f '$REMOTE_DIR/index.html' && ln -s landingpage/index.html '$REMOTE_DIR/index.html'"
# Next.js lädt unter / die RSC-Segmente ohne basePath (/__next.*.txt) — ebenfalls verlinken
echo "Setze symbolische Links: __next.*.txt → landingpage/__next.*.txt"
ssh -S "$SSH_SOCKET" "$REMOTE_USER@$REMOTE_HOST" "cd '$REMOTE_DIR' && rm -f __next.*.txt && for f in landingpage/__next.*.txt; do ln -s \$f \$(basename \$f); done"
echo "Kopiere Fehlerseiten: landingpage/<code>.html → <code>.html" echo "Kopiere Fehlerseiten: landingpage/<code>.html → <code>.html"
ssh -S "$SSH_SOCKET" "$REMOTE_USER@$REMOTE_HOST" "for c in $ERROR_PAGES; do cp '$REMOTE_DIR'/landingpage/\$c.html '$REMOTE_DIR'/\$c.html; done" ssh -S "$SSH_SOCKET" "$REMOTE_USER@$REMOTE_HOST" "for c in $ERROR_PAGES; do cp '$REMOTE_DIR'/landingpage/\$c.html '$REMOTE_DIR'/\$c.html; done"