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. * The core metaphor, as an animated SVG.
* *
* Underground → the mycelium = our tech foundation / custom software * Underground → the mycelium = the shared core stack / custom software.
* stacks. Unseen, always working, connects everything. * Unseen, always working, connects every entity.
* Surface → "flowers" (fruiting bodies) = physical locations * Flowers → the three entities people actually experience:
* (City + Farm) that people actually experience. * • Archipelago (city center) — a permanent bloom
* Ghost flowers→ once the mycelium runs, new locations sprout cheaply, * • Solunar Society (pop-up fest.) — ephemeral cluster
* anywhere in the world. * • 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 * Pure SVG + CSS so it scales crisply on a projector and respects
* prefers-reduced-motion. * prefers-reduced-motion.
*/ */
import Icon from './Icon.vue' import Icon from './Icon.vue'
import { locations } from '../data' import { entityById } from '../data'
const GROUND = 300 const GROUND = 300
// underground mycelium nodes (below the ground line) // underground mycelium nodes (below the ground line)
const nodes: [number, number][] = [ const nodes: [number, number][] = [
[330, 340], // root under city [230, 340], // root · Archipelago
[670, 340], // root under farm [500, 335], // root · Solunar
[150, 335], // root under ghost 1 [770, 340], // root · Chateau
[500, 335], // root under ghost 2 [100, 340], // root · ghost
[850, 335], // root under ghost 3 [900, 340], // root · ghost
[90, 430], [160, 430],
[250, 480], [330, 470],
[410, 420], [430, 420],
[560, 470], [560, 470],
[720, 430], [660, 430],
[900, 470], [840, 470],
[180, 560], [240, 560],
[430, 560], [430, 560],
[620, 560], [620, 560],
[810, 560], [810, 560],
@ -38,33 +40,30 @@ const nodes: [number, number][] = [
// threads weaving the mycelium together (indices into `nodes`) // threads weaving the mycelium together (indices into `nodes`)
const edges: [number, number][] = [ const edges: [number, number][] = [
[0, 7],
[0, 6], [0, 6],
[0, 2], [0, 5],
[0, 7],
[0, 3], [0, 3],
[1, 8], [3, 5],
[1, 9],
[1, 3],
[1, 4],
[2, 5],
[2, 6],
[3, 7],
[3, 8],
[4, 9],
[4, 10],
[5, 11], [5, 11],
[6, 11], [6, 11],
[6, 12], [6, 12],
[7, 12], [7, 12],
[7, 1],
[1, 8],
[8, 12],
[8, 13], [8, 13],
[1, 9],
[9, 13], [9, 13],
[9, 14], [9, 2],
[2, 10],
[10, 14], [10, 14],
[3, 0], [4, 10],
[3, 1], [9, 14],
[7, 8],
[2, 4],
] ]
// organic curved path between two nodes
function thread([a, b]: [number, number], i: number) { function thread([a, b]: [number, number], i: number) {
const [ax, ay] = nodes[a] const [ax, ay] = nodes[a]
const [bx, by] = nodes[b] 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}` 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 = [ const flowers = [
{ x: 330, kind: 'real', color: 'var(--color-pine-400)', name: locations.city.name, tag: 'City node' }, { x: 230, kind: 'bloom', color: 'var(--color-pine-400)', name: arch.name, tag: arch.role },
{ x: 670, kind: 'real', color: 'var(--color-clay-400)', name: locations.farm.name, tag: 'Farm node' }, { x: 500, kind: 'festival', color: 'var(--color-indigo-400)', name: sol.name, tag: sol.role },
{ x: 150, kind: 'ghost' }, { x: 770, kind: 'bloom', color: 'var(--color-clay-400)', name: cha.name, tag: cha.role },
{ x: 500, kind: 'ghost' }, { x: 100, kind: 'ghost' },
{ x: 850, kind: 'ghost' }, { x: 900, kind: 'ghost' },
] as const ] 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> </script>
<template> <template>
@ -165,18 +175,16 @@ const flowers = [
<!-- surface flowers --> <!-- surface flowers -->
<g v-for="(f, i) in flowers" :key="`flower-${i}`"> <g v-for="(f, i) in flowers" :key="`flower-${i}`">
<!-- stem --> <!-- permanent bloom (city / farm) -->
<template v-if="f.kind === 'bloom'">
<path <path
:d="`M ${f.x} ${GROUND} C ${f.x - 6} ${GROUND - 26}, ${f.x + 6} ${GROUND - 44}, ${f.x} ${GROUND - 70}`" :d="`M ${f.x} ${GROUND} C ${f.x - 6} ${GROUND - 26}, ${f.x + 6} ${GROUND - 44}, ${f.x} ${GROUND - 70}`"
fill="none" fill="none"
:stroke="f.kind === 'real' ? f.color : 'var(--color-sand-dim)'" :stroke="f.color"
:stroke-opacity="f.kind === 'real' ? 0.9 : 0.35" stroke-opacity="0.9"
:stroke-dasharray="f.kind === 'ghost' ? '3 5' : '0'"
stroke-width="5" stroke-width="5"
stroke-linecap="round" stroke-linecap="round"
/> />
<!-- cap -->
<template v-if="f.kind === 'real'">
<path <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`" :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" :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 - 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)" /> <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> </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 --> <!-- ghost future flower -->
<template v-else> <template v-else>
<path <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" fill="none"
stroke="var(--color-sand-dim)" stroke="var(--color-sand-dim)"
stroke-opacity="0.35" stroke-opacity="0.35"
@ -209,20 +240,41 @@ const flowers = [
stroke-width="1.6" stroke-width="1.6"
/> />
<g class="myc-plus" :style="{ animationDelay: `${i * 0.7}s` }"> <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 <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="var(--color-amber-400)"
stroke-width="1.6" stroke-width="1.6"
stroke-linecap="round" stroke-linecap="round"
/> />
</g> </g>
</template> </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> </g>
<!-- captions --> <!-- captions -->
<text x="500" y="34" text-anchor="middle" class="myc-zone">ABOVE GROUND · WHAT PEOPLE EXPERIENCE</text> <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> </svg>
<div class="mt-6 grid gap-4 text-sm sm:grid-cols-2"> <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> <span class="mt-0.5 text-pine-300"><Icon name="Layers" class="size-5" /></span>
<p class="text-sand-dim"> <p class="text-sand-dim">
<span class="font-medium text-sand">The mycelium is the product.</span> <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 don’t see it. One core stack runs unseen beneath every entity — members feel it, they don’t see it.
</p> </p>
</div> </div>
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<span class="mt-0.5 text-amber-300"><Icon name="Sparkles" class="size-5" /></span> <span class="mt-0.5 text-amber-300"><Icon name="Sparkles" class="size-5" /></span>
<p class="text-sand-dim"> <p class="text-sand-dim">
<span class="font-medium text-sand">Flowers are cheap to grow.</span> <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> </p>
</div> </div>
</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 { .myc-plus {
transform-box: fill-box; transform-box: fill-box;
transform-origin: center; transform-origin: center;
@ -326,6 +396,7 @@ const flowers = [
.myc-pulse, .myc-pulse,
.myc-node, .myc-node,
.myc-cap, .myc-cap,
.myc-festival,
.myc-plus { .myc-plus {
animation: none; animation: none;
} }