Files
the-maltemedia-puls/src/routes/+page.svelte

680 lines
21 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { onMount } from "svelte";
import { fade, fly } from "svelte/transition";
// ── Phase types ────────────────────────────────────────────────────────────
type Phase = "alpha" | "beta" | "gamma";
let phase = $state<Phase>("alpha");
let currentSlide = $state(0);
let slideKey = $state(0);
let progress = $state(0);
let glitchActive = $state(false);
let contentVisible = $state(false);
const ALPHA_MS = 2500;
const BETA_MS = 1000;
const GAMMA_MS = 16500;
// ── Slide content (from PDF) ───────────────────────────────────────────────
const slides = [
{
icon: "🔐",
tag: "Datenschutz",
title: "Sicherheit & Vertrauen",
body: "Maltemedia gibt dir die volle Kontrolle über deine Daten. Keine zentralisierten Server — Nachrichten werden Anonym von den Servern geladen.",
ticker: "Deine Daten. Deine Regeln. Deine Freiheit.",
},
{
icon: "🌐",
tag: "Dezentralität",
title: "Nostr-Protokoll",
body: "Ein freies, dezentrales Open-Source-Protokoll. So viele Relays wie gewünscht — fällt einer aus, sind alle Nachrichten auf den anderen synchronisiert.",
ticker: "Nostr: Freiheit ist kein Feature, sondern das Fundament.",
},
{
icon: "⚡",
tag: "Technologie",
title: "Rust-Powered Performance",
body: "Entwickelt mit der Programmiersprache Rust und dem Tauri-Framework für Multiplattform-Apps. Sicher. Schnell. Effizient — technisch fast unmöglich auszufallen.",
ticker: "Rust: Sicher. Schnell. Effizient.",
},
{
icon: "📦",
tag: "Open Source",
title: "Easy-Nostr",
body: "Meine eigene Rust-Bibliothek (Crate) für einfachere Nostr-Integration. Verbindung erstellen, Nachrichten senden, Relays hinzufügen — alles in einer einzigen Funktion.",
ticker: "Easy-Nostr: Eine eigene Bibliothek für die Zukunft.",
},
{
icon: "🤖",
tag: "KI-Integration",
title: "KI ohne Zentralisierung",
body: "Im News-Tab bindest du deine eigene KI per API-Key an. Aktuelle Nachrichten — analysiert von deiner Wahl, nicht von einem Konzern. Zusätzlich: RSS-Feeds beliebiger Webseiten.",
ticker: "AI-Intelligence ohne Zentralisierung.",
},
{
icon: "📰",
tag: "Features",
title: "Drei Tabs. Eine App.",
body: "Home-Tab: Echte Posts vom Nostr-Netzwerk. News-Tab: KI-generierte Artikel & RSS-Feeds. Greet-Tab: ~500 motivierende Sprüche — zufällig, lokal, persönlich.",
ticker: "Social Media neu gedacht.",
},
{
icon: "🔮",
tag: "Ausblick",
title: "Das Ökosystem der Zukunft",
body: "Nächste Schritte: Liken, Reposten, Kommentare. Ein ganzes Ökosystem an Rust-Apps rund um Nostr und Matrix — für Firmen, Schulen und alle, die Freiheit schätzen.",
ticker: "Dezentralität ist Widerstand gegen starre Strukturen.",
},
{
icon: "🏆",
tag: "Jugend forscht junior 2026",
title: "Malte Schröder · 14 Jahre",
body: "MikroMINT Schülerforschungszentrum Rostock · Fachgebiet: Mathematik/Informatik · Mecklenburg-Vorpommern · Betreut von Dr. Lisa-Madeleine Kohrt & Kay Mieske.",
ticker: "Innovation beginnt jetzt.",
},
];
// ── Phase metadata ─────────────────────────────────────────────────────────
const phaseLabel: Record<Phase, string> = {
alpha: "ALPHA — Bereit",
beta: "BETA — Transition",
gamma: "GAMMA — Inhalt",
};
const phaseColor: Record<Phase, string> = {
alpha: "#00d4b4",
beta: "#ff4466",
gamma: "#7c3aed",
};
// ── Canvas ref ─────────────────────────────────────────────────────────────
let canvas: HTMLCanvasElement;
// ── Main loop ──────────────────────────────────────────────────────────────
let phaseStart = -1;
let transitioning = false;
let rafId: number;
function runLoop(ts: number) {
rafId = requestAnimationFrame(runLoop);
if (transitioning) return;
if (phaseStart < 0) phaseStart = ts;
const elapsed = ts - phaseStart;
if (phase === "alpha") {
progress = Math.min((elapsed / ALPHA_MS) * 100, 100);
if (elapsed >= ALPHA_MS) {
phase = "beta";
glitchActive = true;
phaseStart = ts;
progress = 0;
}
} else if (phase === "beta") {
progress = Math.min((elapsed / BETA_MS) * 100, 100);
if (elapsed >= BETA_MS) {
phase = "gamma";
glitchActive = false;
contentVisible = true;
phaseStart = ts;
progress = 0;
}
} else if (phase === "gamma") {
progress = Math.min((elapsed / GAMMA_MS) * 100, 100);
if (elapsed >= GAMMA_MS) {
transitioning = true;
contentVisible = false;
progress = 100;
setTimeout(() => {
currentSlide = (currentSlide + 1) % slides.length;
slideKey++;
phase = "alpha";
phaseStart = -1;
progress = 0;
transitioning = false;
}, 500);
}
}
}
// ── Canvas background (nodes + connections) ────────────────────────────────
onMount(() => {
const ctx = canvas.getContext("2d")!;
const W = window.innerWidth;
const H = window.innerHeight;
canvas.width = W;
canvas.height = H;
interface Dot {
x: number;
y: number;
vx: number;
vy: number;
r: number;
pulse: number;
pulseSpeed: number;
}
const dots: Dot[] = Array.from({ length: 55 }, () => ({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 0.25,
vy: (Math.random() - 0.5) * 0.25,
r: Math.random() * 2.5 + 1,
pulse: Math.random() * Math.PI * 2,
pulseSpeed: Math.random() * 0.015 + 0.005,
}));
function drawBg() {
ctx.clearRect(0, 0, W, H);
for (let i = 0; i < dots.length; i++) {
for (let j = i + 1; j < dots.length; j++) {
const dx = dots[i].x - dots[j].x;
const dy = dots[i].y - dots[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 200) {
const alpha = (1 - dist / 200) * 0.2;
ctx.beginPath();
ctx.strokeStyle = `rgba(0,212,180,${alpha})`;
ctx.lineWidth = 0.8;
ctx.moveTo(dots[i].x, dots[i].y);
ctx.lineTo(dots[j].x, dots[j].y);
ctx.stroke();
}
}
}
for (const d of dots) {
d.x += d.vx;
d.y += d.vy;
if (d.x < 0 || d.x > W) d.vx *= -1;
if (d.y < 0 || d.y > H) d.vy *= -1;
d.pulse += d.pulseSpeed;
const brightness = 0.35 + Math.sin(d.pulse) * 0.25;
ctx.beginPath();
ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0,212,180,${brightness})`;
ctx.fill();
}
requestAnimationFrame(drawBg);
}
requestAnimationFrame(drawBg);
// Start phase loop
rafId = requestAnimationFrame(runLoop);
return () => cancelAnimationFrame(rafId);
});
// ── Seconds left in current phase ─────────────────────────────────────────
const phaseDuration: Record<Phase, number> = {
alpha: ALPHA_MS,
beta: BETA_MS,
gamma: GAMMA_MS,
};
let secondsLeft = $derived(
Math.ceil(((100 - progress) / 100) * (phaseDuration[phase] / 1000)),
);
</script>
<svelte:head>
<title>The Maltemedia Pulse</title>
</svelte:head>
<div class="scene">
<!-- Animated node/relay background -->
<canvas bind:this={canvas} class="bg-canvas"></canvas>
<!-- Radial vignette -->
<div class="vignette"></div>
<!-- HUD top bar -->
<div class="hud">
<div class="hud-left">
<span class="phase-dot" style="background: {phaseColor[phase]};"
></span>
<span class="phase-label">{phaseLabel[phase]}</span>
</div>
<div class="hud-center">
<span class="project-name">MALTEMEDIA PULSE</span>
</div>
<div class="hud-right">
<span class="slide-info"
>{currentSlide + 1}&thinsp;/&thinsp;{slides.length}</span
>
<span class="countdown">{secondsLeft}s</span>
</div>
</div>
<!-- Main stage -->
<div class="stage">
<!-- Glitch Hero -->
<div class="hero" class:glitching={glitchActive}>
<h1 class="logo" data-text="MALTEMEDIA">MALTEMEDIA</h1>
<p class="tagline">
Eine dezentrale Social&nbsp;Media News&nbsp;App
</p>
{#if phase === "alpha"}
<div class="pulse-ring"></div>
{/if}
</div>
<!-- Content card (gamma phase) -->
{#if contentVisible}
{#key slideKey}
<div
class="card"
in:fly={{ y: 40, duration: 700, delay: 100 }}
out:fade={{ duration: 400 }}
>
<div class="card-header">
<span class="card-icon"
>{slides[currentSlide].icon}</span
>
<span class="card-tag">{slides[currentSlide].tag}</span>
</div>
<h2 class="card-title">{slides[currentSlide].title}</h2>
<p class="card-body">{slides[currentSlide].body}</p>
<div class="card-divider"></div>
<p class="card-ticker">
<span class="ticker-arrow"></span>
{slides[currentSlide].ticker}
</p>
</div>
{/key}
{/if}
</div>
<!-- Timer bar at bottom -->
<div class="timer-track">
<div
class="timer-fill"
style="width: {progress}%; background: {phaseColor[phase]};"
></div>
</div>
</div>
<style>
/* ── Global reset ───────────────────────────────────────── */
:global(*),
:global(*::before),
:global(*::after) {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:global(body) {
background: #06060d;
overflow: hidden;
font-family: "Space Grotesk", system-ui, sans-serif;
}
/* ── Scene ──────────────────────────────────────────────── */
.scene {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
color: #fff;
}
/* ── Background canvas ──────────────────────────────────── */
.bg-canvas {
position: absolute;
inset: 0;
z-index: 0;
}
.vignette {
position: absolute;
inset: 0;
z-index: 1;
background: radial-gradient(
ellipse at 50% 50%,
transparent 30%,
#06060d 85%
);
}
/* ── HUD ────────────────────────────────────────────────── */
.hud {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 30;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.1rem 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
background: rgba(6, 6, 13, 0.6);
backdrop-filter: blur(12px);
font-size: 0.7rem;
letter-spacing: 0.15em;
text-transform: uppercase;
}
.hud-left,
.hud-right {
display: flex;
align-items: center;
gap: 0.6rem;
color: rgba(255, 255, 255, 0.4);
}
.hud-center {
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.35em;
color: rgba(255, 255, 255, 0.18);
}
.phase-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
animation: blink 1.8s ease-in-out infinite;
}
.countdown {
color: rgba(255, 255, 255, 0.25);
font-variant-numeric: tabular-nums;
min-width: 2.5rem;
text-align: right;
}
/* ── Stage ──────────────────────────────────────────────── */
.stage {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
gap: 3rem;
width: 100%;
max-width: 860px;
padding: 1rem 2rem;
}
/* ── Glitch Hero ────────────────────────────────────────── */
.hero {
text-align: center;
position: relative;
}
.logo {
font-size: clamp(3.5rem, 10vw, 7.5rem);
font-weight: 800;
letter-spacing: 0.12em;
color: #fff;
text-shadow: 0 0 50px rgba(0, 212, 180, 0.35);
position: relative;
animation: logo-breathe 4s ease-in-out infinite;
}
/* Glitch pseudo-layers */
.logo::before,
.logo::after {
content: attr(data-text);
position: absolute;
inset: 0;
width: 100%;
opacity: 0;
pointer-events: none;
}
.logo::before {
color: #ff0044;
}
.logo::after {
color: #00ccff;
}
/* Activate glitch layers */
.hero.glitching .logo {
animation: glitch-base 0.12s steps(1) infinite;
}
.hero.glitching .logo::before {
opacity: 1;
animation: glitch-r 0.12s steps(1) infinite;
}
.hero.glitching .logo::after {
opacity: 1;
animation: glitch-b 0.12s steps(1) infinite;
}
.tagline {
font-size: clamp(0.8rem, 2vw, 1.05rem);
color: rgba(255, 255, 255, 0.45);
letter-spacing: 0.25em;
text-transform: uppercase;
margin-top: 0.75rem;
}
.pulse-ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120%;
padding-bottom: 38%;
border-radius: 50%;
border: 1px solid rgba(0, 212, 180, 0.1);
animation: ring-pulse 3.5s ease-in-out infinite;
pointer-events: none;
}
/* ── Content Card ───────────────────────────────────────── */
.card {
width: 100%;
background: rgba(255, 255, 255, 0.035);
border: 1px solid rgba(0, 212, 180, 0.18);
border-radius: 1.4rem;
padding: 2.4rem 3rem;
backdrop-filter: blur(16px);
box-shadow:
0 0 0 1px rgba(0, 212, 180, 0.05),
0 8px 60px rgba(0, 0, 0, 0.4),
0 0 80px rgba(0, 212, 180, 0.04);
text-align: center;
}
.card-header {
display: flex;
align-items: center;
justify-content: center;
gap: 0.8rem;
margin-bottom: 1.2rem;
}
.card-icon {
font-size: 1.8rem;
line-height: 1;
}
.card-tag {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #00d4b4;
border: 1px solid rgba(0, 212, 180, 0.35);
border-radius: 2rem;
padding: 0.25rem 0.85rem;
}
.card-title {
font-size: clamp(1.4rem, 3.5vw, 2rem);
font-weight: 700;
color: #fff;
margin-bottom: 1rem;
line-height: 1.2;
}
.card-body {
font-size: clamp(0.9rem, 1.8vw, 1.1rem);
color: rgba(255, 255, 255, 0.65);
line-height: 1.75;
margin-bottom: 1.6rem;
}
.card-divider {
width: 3rem;
height: 1px;
background: rgba(0, 212, 180, 0.3);
margin: 0 auto 1.2rem;
}
.card-ticker {
font-size: clamp(0.85rem, 1.6vw, 1rem);
color: #00d4b4;
font-weight: 600;
letter-spacing: 0.04em;
opacity: 0.9;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
}
.ticker-arrow {
font-size: 1.1em;
opacity: 0.6;
}
/* ── Timer bar ──────────────────────────────────────────── */
.timer-track {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: rgba(255, 255, 255, 0.06);
z-index: 30;
}
.timer-fill {
height: 100%;
transition:
width 0.15s linear,
background 0.6s ease;
box-shadow: 0 0 10px currentColor;
}
/* ── Keyframes ──────────────────────────────────────────── */
@keyframes logo-breathe {
0%,
100% {
text-shadow: 0 0 40px rgba(0, 212, 180, 0.3);
}
50% {
text-shadow:
0 0 60px rgba(0, 212, 180, 0.55),
0 0 120px rgba(0, 212, 180, 0.2);
}
}
@keyframes glitch-base {
0%,
100% {
transform: translate(0);
filter: none;
}
20% {
transform: translate(-2px, 1px);
filter: hue-rotate(90deg);
}
40% {
transform: translate(3px, -1px);
filter: none;
}
60% {
transform: translate(-1px, 2px);
filter: hue-rotate(-90deg);
}
80% {
transform: translate(2px, -2px);
filter: none;
}
}
@keyframes glitch-r {
0% {
transform: translate(-5px, 0);
clip-path: inset(10% 0 75% 0);
}
25% {
transform: translate(5px, 1px);
clip-path: inset(55% 0 15% 0);
}
50% {
transform: translate(-3px, -1px);
clip-path: inset(30% 0 45% 0);
}
75% {
transform: translate(4px, 2px);
clip-path: inset(70% 0 5% 0);
}
100% {
transform: translate(-5px, 0);
clip-path: inset(10% 0 75% 0);
}
}
@keyframes glitch-b {
0% {
transform: translate(5px, 1px);
clip-path: inset(55% 0 15% 0);
}
25% {
transform: translate(-5px, 0);
clip-path: inset(10% 0 75% 0);
}
50% {
transform: translate(3px, -2px);
clip-path: inset(80% 0 2% 0);
}
75% {
transform: translate(-4px, 1px);
clip-path: inset(20% 0 60% 0);
}
100% {
transform: translate(5px, 1px);
clip-path: inset(55% 0 15% 0);
}
}
@keyframes ring-pulse {
0%,
100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.5;
}
50% {
transform: translate(-50%, -50%) scale(1.07);
opacity: 0.12;
}
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
</style>