feat: Nostr-vs-HTTP slides with SVG network diagrams

Animated one-server-vs-many-relays illustration (respects
prefers-reduced-motion) and a point-by-point comparison table on its
own slide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
padreug 2026-07-22 02:53:45 +02:00
commit 8de72c9ae3
2 changed files with 172 additions and 0 deletions

View file

@ -0,0 +1,138 @@
<script setup lang="ts">
import SlideShell from './SlideShell.vue'
// HTTP: three users funnel into ONE server.
const users = [40, 95, 150]
// Nostr: you broadcast out to MANY relays (a little mesh).
const you = { x: 52, y: 95 }
const relays = [
{ x: 200, y: 32 },
{ x: 250, y: 62 },
{ x: 262, y: 112 },
{ x: 232, y: 158 },
{ x: 182, y: 128 },
]
// Faint relay-to-relay links to imply a gossiping network.
const mesh = [
[0, 1],
[1, 2],
[2, 3],
[3, 4],
[4, 0],
[1, 4],
]
</script>
<template>
<SlideShell kicker="Mental model" title="Nostr isnt HTTP">
<div class="grid gap-6 sm:grid-cols-2">
<!-- HTTP: many users one server -->
<div class="rounded-xl border border-zinc-700 bg-zinc-900/60 p-6">
<p class="mb-2 text-sm font-semibold uppercase tracking-widest text-zinc-400">
The web (HTTP)
</p>
<svg viewBox="0 0 300 190" class="mx-auto w-full max-w-[300px]">
<!-- connections -->
<g stroke="#52525b" stroke-width="2">
<line v-for="y in users" :key="'l' + y" x1="54" :y1="y" x2="206" y2="95" />
</g>
<!-- users -->
<g fill="#a1a1aa">
<template v-for="y in users" :key="'u' + y">
<circle cx="42" :cy="y - 7" r="6" />
<path :d="`M33 ${y + 9} a9 9 0 0 1 18 0`" />
</template>
</g>
<!-- one server -->
<rect x="206" y="66" width="66" height="58" rx="8" fill="#27272a" stroke="#a1a1aa" stroke-width="2" />
<g stroke="#a1a1aa" stroke-width="2">
<line x1="218" y1="84" x2="260" y2="84" />
<line x1="218" y1="95" x2="260" y2="95" />
<line x1="218" y1="106" x2="260" y2="106" />
</g>
<circle cx="252" cy="118" r="2.5" fill="#ef4444" />
<text x="239" y="150" text-anchor="middle" fill="#71717a" font-size="12">single point</text>
</svg>
<p class="mt-2 text-center text-sm text-zinc-400">
One server holds your account <em>and</em> your data. It bans you? Youre gone.
</p>
</div>
<!-- Nostr: you fan out to many relays -->
<div class="rounded-xl border border-violet-600/50 bg-violet-950/20 p-6">
<p class="mb-2 text-sm font-semibold uppercase tracking-widest text-violet-400">Nostr</p>
<svg viewBox="0 0 300 190" class="mx-auto w-full max-w-[300px]">
<!-- faint relay mesh -->
<g stroke="#5b21b6" stroke-width="1.5" opacity="0.6">
<line
v-for="([a, b], i) in mesh"
:key="'m' + i"
:x1="relays[a].x"
:y1="relays[a].y"
:x2="relays[b].x"
:y2="relays[b].y"
/>
</g>
<!-- broadcast lines (animated) -->
<g stroke="#8b5cf6" stroke-width="2" class="flow">
<line
v-for="(r, i) in relays"
:key="'b' + i"
:x1="you.x + 14"
:y1="you.y"
:x2="r.x"
:y2="r.y"
/>
</g>
<!-- relays -->
<g>
<circle
v-for="(r, i) in relays"
:key="'r' + i"
:cx="r.x"
:cy="r.y"
r="11"
fill="#2e1065"
stroke="#a78bfa"
stroke-width="2"
/>
</g>
<!-- you (signed) -->
<g fill="#8b5cf6">
<circle :cx="you.x" :cy="you.y - 8" r="8" />
<path :d="`M${you.x - 12} ${you.y + 10} a12 12 0 0 1 24 0`" />
</g>
<text :x="you.x" :y="you.y + 34" text-anchor="middle" fill="#a78bfa" font-size="12">you</text>
<text x="235" y="184" text-anchor="middle" fill="#a78bfa" font-size="12">many relays</text>
</svg>
<p class="mt-2 text-center text-sm text-zinc-300">
You sign notes and broadcast to relays you choose. One drops you? The rest still have it.
</p>
</div>
</div>
<p class="mt-6 text-sm text-zinc-500">
Relays actually talk over <code class="text-zinc-300">wss://</code> (WebSockets) same plumbing
as the web. What flips is
<span class="text-zinc-300">who owns your identity and where your data lives.</span>
</p>
</SlideShell>
</template>
<style scoped>
.flow line {
stroke-dasharray: 5 6;
animation: flow 0.9s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -11;
}
}
@media (prefers-reduced-motion: reduce) {
.flow line {
animation: none;
}
}
</style>

View file

@ -0,0 +1,34 @@
<script setup lang="ts">
import SlideShell from './SlideShell.vue'
const rows = [
{ label: 'Identity', http: 'An account the server owns', nostr: 'A keypair you own' },
{ label: 'Data lives', http: 'On one companys server', nostr: 'Copied across many relays' },
{ label: 'Model', http: 'Request → response', nostr: 'Publish signed events (pub/sub)' },
{ label: 'Trust', http: 'Trust the host not to lie', nostr: 'Signatures — math, not trust' },
{ label: 'If it bans you', http: 'Youre gone', nostr: 'Move relays, keep your identity' },
]
</script>
<template>
<SlideShell kicker="Mental model" title="HTTP vs Nostr, point by point">
<div class="overflow-hidden rounded-xl border border-zinc-700">
<table class="w-full text-left text-lg">
<thead class="bg-zinc-800/60 text-sm uppercase tracking-wider text-zinc-400">
<tr>
<th class="px-5 py-3 font-medium"></th>
<th class="px-5 py-3 font-medium">HTTP</th>
<th class="px-5 py-3 font-medium text-violet-300">Nostr</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-800">
<tr v-for="row in rows" :key="row.label">
<td class="px-5 py-4 font-semibold text-white">{{ row.label }}</td>
<td class="px-5 py-4 text-zinc-400">{{ row.http }}</td>
<td class="px-5 py-4 text-zinc-200">{{ row.nostr }}</td>
</tr>
</tbody>
</table>
</div>
</SlideShell>
</template>