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 dont see it. One core stack runs unseen beneath every entity members feel it, they dont 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;
} }