feat(deck): show three entity flowers in the mycelium viz

City and farm render as permanent blooms; Solunar Society is an ephemeral
pop-up cluster (indigo, dashed stem, floats + fades). Underground caption
reframed as the shared core stack every entity runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-07-28 18:34:19 +02:00
commit 9e74000f9f

View file

@ -2,35 +2,37 @@
/**
* The core metaphor, as an animated SVG.
*
* Underground the mycelium = our tech foundation / custom software
* stacks. Unseen, always working, connects everything.
* Surface "flowers" (fruiting bodies) = physical locations
* (City + Farm) that people actually experience.
* Ghost flowers once the mycelium runs, new locations sprout cheaply,
* anywhere in the world.
* Underground the mycelium = the shared core stack / custom software.
* Unseen, always working, connects every entity.
* Flowers the three entities people actually experience:
* Archipelago (city center) a permanent bloom
* Solunar Society (pop-up fest.) ephemeral cluster
* Chateau du Faune (farm center) a permanent bloom
* Ghost flowers once the mycelium runs, new locations & festivals sprout
* cheaply, anywhere in the world.
*
* Pure SVG + CSS so it scales crisply on a projector and respects
* prefers-reduced-motion.
*/
import Icon from './Icon.vue'
import { locations } from '../data'
import { entityById } from '../data'
const GROUND = 300
// underground mycelium nodes (below the ground line)
const nodes: [number, number][] = [
[330, 340], // root under city
[670, 340], // root under farm
[150, 335], // root under ghost 1
[500, 335], // root under ghost 2
[850, 335], // root under ghost 3
[90, 430],
[250, 480],
[410, 420],
[230, 340], // root · Archipelago
[500, 335], // root · Solunar
[770, 340], // root · Chateau
[100, 340], // root · ghost
[900, 340], // root · ghost
[160, 430],
[330, 470],
[430, 420],
[560, 470],
[720, 430],
[900, 470],
[180, 560],
[660, 430],
[840, 470],
[240, 560],
[430, 560],
[620, 560],
[810, 560],
@ -38,33 +40,30 @@ const nodes: [number, number][] = [
// threads weaving the mycelium together (indices into `nodes`)
const edges: [number, number][] = [
[0, 7],
[0, 6],
[0, 2],
[0, 5],
[0, 7],
[0, 3],
[1, 8],
[1, 9],
[1, 3],
[1, 4],
[2, 5],
[2, 6],
[3, 7],
[3, 8],
[4, 9],
[4, 10],
[3, 5],
[5, 11],
[6, 11],
[6, 12],
[7, 12],
[7, 1],
[1, 8],
[8, 12],
[8, 13],
[1, 9],
[9, 13],
[9, 14],
[9, 2],
[2, 10],
[10, 14],
[3, 0],
[3, 1],
[4, 10],
[9, 14],
[7, 8],
[2, 4],
]
// organic curved path between two nodes
function thread([a, b]: [number, number], i: number) {
const [ax, ay] = nodes[a]
const [bx, by] = nodes[b]
@ -73,14 +72,25 @@ function thread([a, b]: [number, number], i: number) {
return `M ${ax} ${ay} Q ${mx} ${my} ${bx} ${by}`
}
// surface flowers: the two real nodes + three faint "future" ones
const arch = entityById('archipelago')
const sol = entityById('solunar')
const cha = entityById('chateau')
// surface flowers: three entities + two faint "future" ones
const flowers = [
{ x: 330, kind: 'real', color: 'var(--color-pine-400)', name: locations.city.name, tag: 'City node' },
{ x: 670, kind: 'real', color: 'var(--color-clay-400)', name: locations.farm.name, tag: 'Farm node' },
{ x: 150, kind: 'ghost' },
{ x: 500, kind: 'ghost' },
{ x: 850, kind: 'ghost' },
{ x: 230, kind: 'bloom', color: 'var(--color-pine-400)', name: arch.name, tag: arch.role },
{ x: 500, kind: 'festival', color: 'var(--color-indigo-400)', name: sol.name, tag: sol.role },
{ x: 770, kind: 'bloom', color: 'var(--color-clay-400)', name: cha.name, tag: cha.role },
{ x: 100, kind: 'ghost' },
{ x: 900, kind: 'ghost' },
] as const
// small pop-up caps for the ephemeral festival flower
const festivalCaps = [
{ dx: -18, dy: -58, r: 15 },
{ dx: 0, dy: -74, r: 20 },
{ dx: 20, dy: -60, r: 14 },
]
</script>
<template>
@ -165,18 +175,16 @@ const flowers = [
<!-- surface flowers -->
<g v-for="(f, i) in flowers" :key="`flower-${i}`">
<!-- stem -->
<!-- permanent bloom (city / farm) -->
<template v-if="f.kind === 'bloom'">
<path
:d="`M ${f.x} ${GROUND} C ${f.x - 6} ${GROUND - 26}, ${f.x + 6} ${GROUND - 44}, ${f.x} ${GROUND - 70}`"
fill="none"
:stroke="f.kind === 'real' ? f.color : 'var(--color-sand-dim)'"
:stroke-opacity="f.kind === 'real' ? 0.9 : 0.35"
:stroke-dasharray="f.kind === 'ghost' ? '3 5' : '0'"
:stroke="f.color"
stroke-opacity="0.9"
stroke-width="5"
stroke-linecap="round"
/>
<!-- cap -->
<template v-if="f.kind === 'real'">
<path
:d="`M ${f.x - 42} ${GROUND - 70} Q ${f.x} ${GROUND - 116} ${f.x + 42} ${GROUND - 70} Q ${f.x} ${GROUND - 82} ${f.x - 42} ${GROUND - 70} Z`"
:fill="f.color"
@ -185,23 +193,46 @@ const flowers = [
/>
<circle :cx="f.x - 16" :cy="GROUND - 88" r="3" fill="hsl(0 0% 100% / 0.55)" />
<circle :cx="f.x + 12" :cy="GROUND - 92" r="2.4" fill="hsl(0 0% 100% / 0.45)" />
<text
:x="f.x"
:y="GROUND - 132"
text-anchor="middle"
class="myc-label-name"
:fill="f.color"
>
{{ f.name }}
</text>
<text :x="f.x" :y="GROUND - 152" text-anchor="middle" class="myc-label-tag">
{{ f.tag?.toUpperCase() }}
</text>
</template>
<!-- ephemeral festival cluster (Solunar) -->
<template v-else-if="f.kind === 'festival'">
<path
:d="`M ${f.x} ${GROUND} C ${f.x - 6} ${GROUND - 20}, ${f.x + 6} ${GROUND - 34}, ${f.x} ${GROUND - 46}`"
fill="none"
:stroke="f.color"
stroke-opacity="0.8"
stroke-width="4"
stroke-linecap="round"
stroke-dasharray="3 5"
/>
<g class="myc-festival" :style="{ animationDelay: `${i * 0.3}s` }">
<template v-for="(c, ci) in festivalCaps" :key="ci">
<path
:d="`M ${f.x + c.dx - c.r} ${GROUND + c.dy} Q ${f.x + c.dx} ${GROUND + c.dy - c.r * 1.1} ${f.x + c.dx + c.r} ${GROUND + c.dy} Q ${f.x + c.dx} ${GROUND + c.dy - c.r * 0.28} ${f.x + c.dx - c.r} ${GROUND + c.dy} Z`"
:fill="f.color"
:fill-opacity="0.75 + ci * 0.08"
/>
</template>
<!-- solunar mark: sun + moon spark -->
<circle :cx="f.x" :cy="GROUND - 96" r="3.4" fill="var(--color-amber-300)" />
<circle :cx="f.x + 9" :cy="GROUND - 100" r="2" fill="var(--color-indigo-300)" />
</g>
</template>
<!-- ghost future flower -->
<template v-else>
<path
:d="`M ${f.x - 30} ${GROUND - 70} Q ${f.x} ${GROUND - 104} ${f.x + 30} ${GROUND - 70} Q ${f.x} ${GROUND - 80} ${f.x - 30} ${GROUND - 70} Z`"
:d="`M ${f.x} ${GROUND} C ${f.x - 5} ${GROUND - 22}, ${f.x + 5} ${GROUND - 38}, ${f.x} ${GROUND - 60}`"
fill="none"
stroke="var(--color-sand-dim)"
stroke-opacity="0.3"
stroke-dasharray="3 5"
stroke-width="4"
stroke-linecap="round"
/>
<path
:d="`M ${f.x - 26} ${GROUND - 60} Q ${f.x} ${GROUND - 90} ${f.x + 26} ${GROUND - 60} Q ${f.x} ${GROUND - 68} ${f.x - 26} ${GROUND - 60} Z`"
fill="none"
stroke="var(--color-sand-dim)"
stroke-opacity="0.35"
@ -209,20 +240,41 @@ const flowers = [
stroke-width="1.6"
/>
<g class="myc-plus" :style="{ animationDelay: `${i * 0.7}s` }">
<circle :cx="f.x" :cy="GROUND - 86" r="9" fill="hsl(160 24% 9%)" stroke="var(--color-amber-400)" stroke-opacity="0.6" />
<circle :cx="f.x" :cy="GROUND - 76" r="9" fill="hsl(160 24% 9%)" stroke="var(--color-amber-400)" stroke-opacity="0.6" />
<path
:d="`M ${f.x - 4} ${GROUND - 86} h 8 M ${f.x} ${GROUND - 90} v 8`"
:d="`M ${f.x - 4} ${GROUND - 76} h 8 M ${f.x} ${GROUND - 80} v 8`"
stroke="var(--color-amber-400)"
stroke-width="1.6"
stroke-linecap="round"
/>
</g>
</template>
<!-- labels for the three named entities -->
<template v-if="f.kind !== 'ghost'">
<text
:x="f.x"
:y="f.kind === 'festival' ? GROUND - 122 : GROUND - 132"
text-anchor="middle"
class="myc-label-name"
:fill="f.color"
>
{{ f.name }}
</text>
<text
:x="f.x"
:y="f.kind === 'festival' ? GROUND - 142 : GROUND - 152"
text-anchor="middle"
class="myc-label-tag"
>
{{ f.tag?.toUpperCase() }}
</text>
</template>
</g>
<!-- captions -->
<text x="500" y="34" text-anchor="middle" class="myc-zone">ABOVE GROUND · WHAT PEOPLE EXPERIENCE</text>
<text x="500" y="588" text-anchor="middle" class="myc-zone">UNDERGROUND · THE TECH FOUNDATION THEY NEVER SEE</text>
<text x="500" y="588" text-anchor="middle" class="myc-zone">UNDERGROUND · THE SHARED CORE STACK THEY NEVER SEE</text>
</svg>
<div class="mt-6 grid gap-4 text-sm sm:grid-cols-2">
@ -230,14 +282,14 @@ const flowers = [
<span class="mt-0.5 text-pine-300"><Icon name="Layers" class="size-5" /></span>
<p class="text-sand-dim">
<span class="font-medium text-sand">The mycelium is the product.</span>
Custom software stacks and open hardware run out of sight members feel it, they dont see it.
One core stack runs unseen beneath every entity members feel it, they dont see it.
</p>
</div>
<div class="flex items-start gap-3">
<span class="mt-0.5 text-amber-300"><Icon name="Sparkles" class="size-5" /></span>
<p class="text-sand-dim">
<span class="font-medium text-sand">Flowers are cheap to grow.</span>
Once the network is alive, each new location is just another fruiting body on proven infrastructure.
A city hub, a travelling festival, a farm and every new bloom is just another fruiting body.
</p>
</div>
</div>
@ -307,6 +359,24 @@ const flowers = [
}
}
/* festival cluster gently floats + fades — it's ephemeral */
.myc-festival {
transform-box: fill-box;
transform-origin: bottom center;
animation: myc-float 4.5s ease-in-out infinite;
}
@keyframes myc-float {
0%,
100% {
transform: translateY(0);
opacity: 0.85;
}
50% {
transform: translateY(-4px);
opacity: 1;
}
}
.myc-plus {
transform-box: fill-box;
transform-origin: center;
@ -326,6 +396,7 @@ const flowers = [
.myc-pulse,
.myc-node,
.myc-cap,
.myc-festival,
.myc-plus {
animation: none;
}