hero/platform-stack.ex
Platform stack
The platform as three isometric plates: Intelligence, EHOS, and the apps that run on top.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.hero_platform_stack animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
intelligence |
string | "Empowered Intelligence" |
|
ehos |
string | "EHOS" |
|
apps |
string | "What runs on the platform" |
|
events |
list | ["Received", "Approved", "Posted"] |
Three event names on the EHOS line. |
app_names |
list | ["Back-office workflows", "Internal business apps", "AI-built business apps"] |
Three names. Each breaks into two lines at its first space. |
knowledge |
list | ["Goal", "Fact", "Fact", "Rule", "Owner"] |
Five labels for the graph on the bottom plate. |
highlight |
"intelligence" | "ehos" | "apps" | "none" | "ehos" |
The plate outlined in lime. |
Frame props, shared by every composition
| Prop | Type | Default | What it does |
|---|---|---|---|
animated |
boolean | false |
Turns motion on. Without it the composition renders in its final, still state. |
trigger |
"mount" | "in-view" | "in-view-repeat" | "in-view" |
When the entrance plays: on page load, once when scrolled into view, or every time it comes back into view. |
hover |
boolean | false |
Enables the hover micro-interactions the composition supports. |
glow |
boolean | true |
The warm incandescent halo behind the composition. |
fade_out |
boolean | false |
Fades the bottom edge into the surface underneath. |
theme |
"dark" | "light" | "dark" |
`dark` for the site canvas, `light` for the cream reversed surface. |
class |
string | nil |
Extra classes for the frame. |
rest |
global | — |
Any other HTML attribute, set on the frame. |
Source
Two files. Copy them into your app if you would rather own the code than depend on the library.
lib/eh_visuals/hero/platform-stack.ex
defmodule EHVisuals.Hero.PlatformStack do
@moduledoc """
The platform as three isometric plates: Empowered Intelligence underneath,
EHOS in the middle, and what runs on the platform on top.
The scene is drawn in isometric projection. Each plate is a 200 by 200
square in its own plane, placed on screen by one matrix, so everything on a
plate (lines, markers, labels) is laid out in flat plan coordinates.
"""
use EHVisuals.Composition,
name: "hero/platform-stack",
title: "Platform stack",
description:
"The platform as three isometric plates: Intelligence, EHOS, and the apps that run on top."
# Screen y of the top vertex of each plate.
@apps_top 24
@ehos_top 150
@intel_top 276
# Plan coordinates on a plate. The corner near {0, 0} of the two lower
# plates is hidden by the plate above, so nothing is placed where both x and
# y are under 86.
@tile_xs [24, 86, 148]
@graph [{150, 70}, {184, 120}, {150, 150}, {40, 110}, {80, 136}]
@edges [{0, 1}, {0, 2}, {1, 2}, {0, 3}, {0, 4}, {3, 4}, {4, 2}]
attr(:intelligence, :string, default: "Empowered Intelligence")
attr(:ehos, :string, default: "EHOS")
attr(:apps, :string, default: "What runs on the platform")
attr(:events, :list,
default: ~w(Received Approved Posted),
doc: "Three event names on the EHOS line."
)
attr(:app_names, :list,
default: ["Back-office workflows", "Internal business apps", "AI-built business apps"],
doc: "Three names. Each breaks into two lines at its first space."
)
attr(:knowledge, :list,
default: ~w(Goal Fact Fact Rule Owner),
doc: "Five labels for the graph on the bottom plate."
)
attr(:highlight, :string,
default: "ehos",
values: ~w(intelligence ehos apps none),
doc: "The plate outlined in lime."
)
attr(:label, :string,
default:
"Three stacked plates: Empowered Intelligence, EHOS above it, and business apps on top"
)
frame_attrs(isometric: false)
@spec hero_platform_stack(map()) :: Phoenix.LiveView.Rendered.t()
def hero_platform_stack(assigns) do
tiles =
for {name, x} <- Enum.zip(assigns.app_names, @tile_xs) do
{line_1, line_2} = split(name)
{sx, sy} = screen(x + 23, 100, @apps_top)
%{x: x, cx: x + 23, line_1: line_1, line_2: line_2, sx: sx, sy: sy}
end
nodes =
for {{x, y}, index} <- Enum.with_index(@graph) do
%{x: x, y: y, index: index, name: Enum.at(assigns.knowledge, index, "")}
end
risers =
for {x, y} <- Enum.slice(@graph, 3, 2) do
{sx, sy} = screen(x, y, @intel_top)
%{sx: sx, sy: sy}
end
assigns =
assigns
|> assign(:tiles, tiles)
|> assign(:nodes, nodes)
|> assign(
:edges,
for({from, to} <- @edges, do: {Enum.at(@graph, from), Enum.at(@graph, to)})
)
|> assign(:risers, risers)
|> assign(:event_items, Enum.zip(assigns.events, Enum.map(@tile_xs, &(&1 + 23))))
|> assign(:tops, %{apps: @apps_top, ehos: @ehos_top, intel: @intel_top})
|> assign(:lift, @intel_top - @ehos_top)
~H"""
<.frame name="hero/platform-stack" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 560 512">
<g class="ehv-a-rise ehv-i-0">
<polygon class="ehv-hero-platform-stack__side" points={left_face(@tops.intel)} />
<polygon class="ehv-hero-platform-stack__side" points={right_face(@tops.intel)} />
<g class="ehv-hero-platform-stack__plane" transform={matrix(@tops.intel)}>
<rect class={plate("intelligence", @highlight)} width="200" height="200" rx="5" />
<line
:for={{{x1, y1}, {x2, y2}} <- @edges}
class="ehv-line-ink ehv-hero-platform-stack__edge"
x1={x1}
y1={y1}
x2={x2}
y2={y2}
/>
<g :for={node <- @nodes}>
<circle class="ehv-node" cx={node.x} cy={node.y} r="4" />
<text class="ehv-mono" x={node.x + 8} y={node.y - 6} font-size="6.5">
{String.upcase(node.name)}
</text>
</g>
<text class={title("intelligence", @highlight)} x="14" y="190" font-size="8">
01  {String.upcase(@intelligence)}
</text>
</g>
</g>
<g class="ehv-a-fade ehv-i-3">
<line
:for={riser <- @risers}
class="ehv-line ehv-dash"
x1={riser.sx}
x2={riser.sx}
y1={riser.sy}
y2={riser.sy - @lift}
/>
</g>
<g class="ehv-a-rise ehv-i-3">
<polygon class="ehv-hero-platform-stack__side" points={left_face(@tops.ehos)} />
<polygon class="ehv-hero-platform-stack__side" points={right_face(@tops.ehos)} />
<g class="ehv-hero-platform-stack__plane" transform={matrix(@tops.ehos)}>
<rect class={plate("ehos", @highlight)} width="200" height="200" rx="5" />
<text class="ehv-mono ehv-t-lime" x="20" y="143" font-size="6">EVENTS</text>
<line class="ehv-line-lime" x1="20" x2="182" y1="150" y2="150" />
<path class="ehv-fill-lime" d="M182 146 l8 4 l-8 4 z" />
<g :for={{event, x} <- @event_items}>
<line class="ehv-line-ink" x1={x} x2={x} y1="100" y2="146" />
<circle class="ehv-node" cx={x} cy="100" r="4" />
<rect class="ehv-fill-lime" x={x - 5} y="145" width="10" height="10" rx="2" />
<text class="ehv-mono ehv-t-2" x={x} y="169" font-size="6.5" text-anchor="middle">
{String.upcase(event)}
</text>
</g>
<g :if={@animated} class="ehv-smil">
<rect class="ehv-fill-ink" y="147.5" width="5" height="5" rx="1">
<animate attributeName="x" from="20" to="178" dur="3.2s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0;1;1;0"
keyTimes="0;0.1;0.85;1"
dur="3.2s"
repeatCount="indefinite"
/>
</rect>
</g>
<text class={title("ehos", @highlight)} x="14" y="190" font-size="8">
02  {String.upcase(@ehos)}
</text>
</g>
</g>
<g class="ehv-a-fade ehv-i-6">
<line
:for={tile <- @tiles}
class="ehv-line ehv-dash"
x1={tile.sx}
x2={tile.sx}
y1={tile.sy + @lift}
y2={tile.sy}
/>
</g>
<g class="ehv-a-rise ehv-i-6">
<polygon class="ehv-hero-platform-stack__side" points={left_face(@tops.apps)} />
<polygon class="ehv-hero-platform-stack__side" points={right_face(@tops.apps)} />
<g class="ehv-hero-platform-stack__plane" transform={matrix(@tops.apps)}>
<rect
class={[plate("apps", @highlight), "ehv-hero-platform-stack__plate--open"]}
width="200"
height="200"
rx="5"
/>
<g :for={tile <- @tiles}>
<rect
class="ehv-hero-platform-stack__tile"
x={tile.x}
y="77"
width="46"
height="46"
rx="3"
/>
<text class="ehv-mono" x={tile.x} y="137" font-size="6">
{String.upcase(tile.line_1)}
</text>
<text class="ehv-mono" x={tile.x} y="146" font-size="6">
{String.upcase(tile.line_2)}
</text>
</g>
<text class={title("apps", @highlight)} x="14" y="190" font-size="8">
03  {String.upcase(@apps)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
# Plan {x, y} on a plate to screen, for the plate whose top vertex is at `top`.
defp screen(x, y, top),
do: {Float.round(280 + 1.1 * (x - y), 1), Float.round(top + 0.55 * (x + y), 1)}
defp matrix(top), do: "matrix(1.1 0.55 -1.1 0.55 280 #{top})"
# The two visible edges of a plate, ten units thick.
defp left_face(top), do: "60,#{top + 110} 280,#{top + 220} 280,#{top + 230} 60,#{top + 120}"
defp right_face(top), do: "280,#{top + 220} 500,#{top + 110} 500,#{top + 120} 280,#{top + 230}"
defp plate(name, highlight) do
["ehv-hero-platform-stack__plate", name == highlight && "ehv-hero-platform-stack__plate--on"]
end
defp title(name, highlight) do
["ehv-mono", if(name == highlight, do: "ehv-t-lime", else: "ehv-t-2")]
end
defp split(name) do
case String.split(name, " ", parts: 2) do
[first, rest] -> {first, rest}
[only] -> {only, ""}
end
end
end
assets/css/hero/platform-stack.css
/* hero/platform-stack */
.ehv-hero-platform-stack { --ehv-max-w: 620px; --ehv-step: 150ms; --ehv-dur: 900ms; }
.ehv-hero-platform-stack .ehv__glow { background: radial-gradient(46% 40% at 50% 52%, var(--ehv-glow), transparent 72%); }
/* Hairlines stay one pixel wide in the skewed plane of a plate. */
.ehv-hero-platform-stack__plane * { vector-effect: non-scaling-stroke; }
.ehv-hero-platform-stack__plate {
fill: var(--ehv-surface-1);
fill-opacity: 0.92;
stroke: var(--ehv-border-strong);
stroke-width: 1;
}
.ehv-hero-platform-stack__plate--open { stroke-dasharray: 3 4; }
.ehv-hero-platform-stack__plate--on { stroke: var(--ehv-lime); stroke-dasharray: none; }
.ehv-hero-platform-stack__side {
fill: var(--ehv-surface-2);
stroke: var(--ehv-border-soft);
stroke-width: 1;
stroke-linejoin: round;
}
.ehv-hero-platform-stack__tile {
fill: var(--ehv-surface-3);
stroke: var(--ehv-ink-3);
stroke-width: 1;
}
.ehv-hero-platform-stack__edge { stroke-width: 1; }
.ehv--hover .ehv-hero-platform-stack__tile { transition: fill 200ms var(--ehv-ease), stroke 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-hero-platform-stack__tile { fill: var(--ehv-lime-soft); stroke: var(--ehv-lime); }