hero/security-gateway.ex
Security gateway
Isometric request lanes running into a gate wall: allowed requests pass in lime, a blocked one stops in coral.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.hero_security_gateway animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
caption |
string | "Access gateway · live requests" |
|
requests |
list | [%{name: "Marta · Finance", allowed: true}, %{name: "Orders agent", allowed: true}, ...] |
Two to five `%{name, allowed}`, one lane each. Keep names under 18 characters. A lane with `allowed: false` stops at the wall. |
checks |
list | ["Role", "Policy", "Audit"] |
Up to four short labels on the wall for what it checks. |
denied_label |
string | "Denied · no role" |
The tag over a shut gate, up to 18 characters. |
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/security-gateway.ex
defmodule EHVisuals.Hero.SecurityGateway do
@moduledoc """
Request lanes running into a gate wall: allowed requests pass through an
open gate, a blocked one stops in front of a shut one.
The scene is drawn in isometric projection. The lanes run along the plan
y axis towards the wall, which stands on the line y = 0 and runs along the
x axis. The ground and the top of the wall are placed by `plane/1`, the
front of the wall by `face/0`, and the lanes are screen lines computed here.
"""
use EHVisuals.Composition,
name: "hero/security-gateway",
title: "Security gateway",
description:
"Isometric request lanes running into a gate wall: allowed requests pass in lime, a blocked one stops in coral."
# Screen position of the plan origin, the left end of the wall's front.
@cx 185
@cy 90
# The wall: length along x, thickness along y, height, and its gates.
@len 340
@depth 12
@height 46
@gate_w 40
@gate_h 28
# Where a lane starts in front of the wall and where it ends behind it.
@near 190
@far -170
# Where the packets rest in the still frame, by lane.
@passed [-104, -134, -92, -122, -98]
@ahead [128, 92, 150, 110, 136]
@requests [
%{name: "Marta · Finance", allowed: true},
%{name: "Orders agent", allowed: true},
%{name: "Unknown key", allowed: false},
%{name: "Jan · Sales", allowed: true}
]
attr(:caption, :string, default: "Access gateway · live requests")
attr(:requests, :list,
default: @requests,
doc:
"Two to five `%{name, allowed}`, one lane each. Keep names under 18 characters. A lane with `allowed: false` stops at the wall."
)
attr(:checks, :list,
default: ~w(Role Policy Audit),
doc: "Up to four short labels on the wall for what it checks."
)
attr(:denied_label, :string,
default: "Denied · no role",
doc: "The tag over a shut gate, up to 18 characters."
)
attr(:label, :string,
default:
"Four request lanes running into a gate wall: three pass through open gates and one is stopped"
)
frame_attrs(isometric: false)
@spec hero_security_gateway(map()) :: Phoenix.LiveView.Rendered.t()
def hero_security_gateway(assigns) do
requests = Enum.take(assigns.requests, 5)
count = max(length(requests), 1)
lanes =
for {request, index} <- Enum.with_index(requests) do
x = r(@len * (index + 0.5) / count)
allowed = Map.get(request, :allowed, true)
{tag_x, tag_y} = screen(x, -@depth / 2, @height)
%{
name: request.name,
index: index,
allowed: allowed,
x: x,
gate: x - @gate_w / 2,
front: segment({x, @near}, {x, 0}),
behind: segment({x, -@depth}, {x, @far}),
passed: packet_box(x, Enum.at(@passed, index)),
ahead: packet_box(x, if(allowed, do: Enum.at(@ahead, index), else: 20)),
begin: "#{r(index * 0.7)}s",
tag_x: tag_x,
tag_y: tag_y
}
end
check_count = max(length(Enum.take(assigns.checks, 4)), 1)
checks =
for {name, index} <- assigns.checks |> Enum.take(4) |> Enum.with_index() do
%{name: name, x: r(@len * (index + 0.5) / check_count)}
end
allowed = Enum.count(lanes, & &1.allowed)
assigns =
assigns
|> assign(:lanes, lanes)
|> assign(:check_items, checks)
|> assign(:counter, "#{pad(allowed)} allowed · #{pad(length(lanes) - allowed)} blocked")
|> assign(:tag_w, r(18 + String.length(assigns.denied_label) * 5.5))
|> assign(:ground, plane(0))
|> assign(:roof, plane(@height))
|> assign(:face, face())
|> assign(
:wall_end,
points([
screen(@len, 0, @height),
screen(@len, -@depth, @height),
screen(@len, -@depth, 0),
screen(@len, 0, 0)
])
)
|> assign(:wall, %{len: @len, depth: @depth, height: @height})
|> assign(:gate, %{w: @gate_w, h: @gate_h, inset: @depth})
|> assign(:run, %{near: @near - 14, far: @far + 6})
~H"""
<.frame name="hero/security-gateway" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 680 352">
<text class="ehv-mono ehv-a-fade" x="0" y="14" font-size="9">
{String.upcase(@caption)}
</text>
<text
class="ehv-mono ehv-t-2 ehv-a-fade ehv-i-2"
x="680"
y="14"
font-size="9"
text-anchor="end"
>
{String.upcase(@counter)}
</text>
<g class="ehv-h-lift">
<g :for={lane <- @lanes}>
<line
class={"ehv-line ehv-a-draw ehv-i-#{lane.index + 1}"}
x1={lane.front.x1}
y1={lane.front.y1}
x2={lane.front.x2}
y2={lane.front.y2}
pathLength="1"
/>
<line
:if={lane.allowed}
class={"ehv-line-lime ehv-a-draw ehv-i-#{lane.index + 6}"}
x1={lane.behind.x1}
y1={lane.behind.y1}
x2={lane.behind.x2}
y2={lane.behind.y2}
pathLength="1"
/>
</g>
<g class="ehv-a-fade ehv-i-3">
<g class="ehv-hero-security-gateway__plane" transform={@ground}>
<g :for={lane <- @lanes}>
<text
class={["ehv-mono", !lane.allowed && "ehv-t-coral"]}
x={lane.x + 17}
y={@run.near + 12}
font-size="7.5"
transform={"rotate(-90 #{lane.x + 17} #{@run.near + 12})"}
>
{String.upcase(lane.name)}
</text>
<path
:if={lane.allowed}
class="ehv-fill-lime"
d={"M#{lane.x - 4} #{@run.far - 2} L#{lane.x} #{@run.far - 11} L#{lane.x + 4} #{@run.far - 2} z"}
/>
</g>
</g>
</g>
<g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-12">
<g class="ehv-hero-security-gateway__plane" transform={@ground}>
<circle
:for={lane <- @lanes}
class={if lane.allowed, do: "ehv-fill-lime", else: "ehv-fill-coral"}
cx={lane.x}
r="3"
opacity="0"
>
<animate
:if={lane.allowed}
attributeName="cy"
from={@run.near}
to={@run.far}
dur="3.6s"
begin={lane.begin}
repeatCount="indefinite"
/>
<animate
:if={!lane.allowed}
attributeName="cy"
values={"#{@run.near};30;30"}
keyTimes="0;0.55;1"
dur="3.6s"
begin={lane.begin}
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="0;1;1;0"
keyTimes="0;0.1;0.85;1"
dur="3.6s"
begin={lane.begin}
repeatCount="indefinite"
/>
</circle>
</g>
</g>
<g :for={lane <- @lanes} :if={lane.allowed} class={"ehv-a-pop ehv-i-#{lane.index + 9}"}>
<.packet box={lane.passed} tone="lime" />
</g>
<g class="ehv-a-rise ehv-i-4">
<g class="ehv-hero-security-gateway__plane" transform={@face}>
<rect
class="ehv-hero-security-gateway__wall"
y={-@wall.height}
width={@wall.len}
height={@wall.height}
/>
<g :for={lane <- @lanes}>
<g :if={lane.allowed}>
<rect
class="ehv-hero-security-gateway__gate"
x={lane.gate}
y={-@gate.h}
width={@gate.w}
height={@gate.h}
/>
<polygon
class="ehv-hero-security-gateway__floor"
points={"#{lane.gate},0 #{lane.gate + @gate.w},0 #{lane.gate + @gate.w},#{-@gate.inset} #{lane.gate + @gate.inset},#{-@gate.inset}"}
/>
<polygon
class="ehv-hero-security-gateway__jamb"
points={"#{lane.gate},0 #{lane.gate + @gate.inset},#{-@gate.inset} #{lane.gate + @gate.inset},#{-@gate.h} #{lane.gate},#{-@gate.h}"}
/>
<line class="ehv-line-lime" x1={lane.x} y1="0" x2={lane.x + 20} y2="-20" />
</g>
<g :if={!lane.allowed}>
<rect
class="ehv-hero-security-gateway__gate ehv-hero-security-gateway__gate--shut"
x={lane.gate}
y={-@gate.h}
width={@gate.w}
height={@gate.h}
/>
<line
:for={bar <- [-7, -14, -21]}
class="ehv-line-coral"
x1={lane.gate + 6}
x2={lane.gate + @gate.w - 6}
y1={bar}
y2={bar}
/>
</g>
</g>
<text
:for={check <- @check_items}
class="ehv-mono ehv-t-2"
x={check.x}
y={-@gate.h - 6.5}
font-size="7"
text-anchor="middle"
>
{String.upcase(check.name)}
</text>
</g>
<polygon class="ehv-hero-security-gateway__side" points={@wall_end} />
<g class="ehv-hero-security-gateway__plane" transform={@roof}>
<rect
class="ehv-hero-security-gateway__top"
y={-@wall.depth}
width={@wall.len}
height={@wall.depth}
/>
</g>
</g>
<g :for={lane <- @lanes} class={"ehv-a-pop ehv-i-#{lane.index + 7}"}>
<.packet box={lane.ahead} tone={if lane.allowed, do: "ink", else: "coral"} />
</g>
<g :for={lane <- @lanes} :if={!lane.allowed} class="ehv-a-fade ehv-i-12">
<line
class="ehv-line-coral ehv-dash"
x1={lane.tag_x}
x2={lane.tag_x}
y1={lane.tag_y}
y2={lane.tag_y - 22}
/>
<rect
class="ehv-hero-security-gateway__tag"
x={lane.tag_x - @tag_w / 2}
y={lane.tag_y - 40}
width={@tag_w}
height="18"
rx="5"
/>
<text
class="ehv-mono ehv-t-coral"
x={lane.tag_x}
y={lane.tag_y - 28}
font-size="8"
text-anchor="middle"
>
{String.upcase(@denied_label)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
# A request on its lane: a small block, lime once it is through, coral
# when it is stopped.
defp packet(assigns) do
~H"""
<polygon class="ehv-hero-security-gateway__side" points={@box.left} />
<polygon class="ehv-hero-security-gateway__side" points={@box.right} />
<g class="ehv-hero-security-gateway__plane" transform={@box.plane}>
<rect
class={"ehv-hero-security-gateway__packet ehv-hero-security-gateway__packet--#{@tone}"}
x={@box.x}
y={@box.y}
width={@box.w}
height={@box.h}
rx="2"
/>
</g>
"""
end
defp packet_box(x, y), do: box(x - 8, y - 8, 16, 16, 0, 5)
defp segment({x1, y1}, {x2, y2}) do
{sx1, sy1} = screen(x1, y1)
{sx2, sy2} = screen(x2, y2)
%{x1: sx1, y1: sy1, x2: sx2, y2: sy2}
end
defp pad(number), do: number |> Integer.to_string() |> String.pad_leading(2, "0")
# Plan {x, y} at height z to screen.
defp screen(x, y, z \\ 0), do: {r(@cx + x - y), r(@cy + (x + y) / 2 - z)}
# The matrix of a flat face at height z.
defp plane(z), do: "matrix(1 0.5 -1 0.5 #{@cx} #{r(@cy - z)})"
# The matrix of the wall's front: x along the wall, y down from the ground line.
defp face, do: "matrix(1 0.5 0 1 #{@cx} #{@cy})"
# A block on the plan rectangle {x, y, w, h} between the heights z0 and z1:
# its top face in plan coordinates and its two visible sides as screen points.
defp box(x, y, w, h, z0, z1) do
%{
x: r(x),
y: r(y),
w: w,
h: h,
plane: plane(z1),
left:
points([
screen(x, y + h, z1),
screen(x + w, y + h, z1),
screen(x + w, y + h, z0),
screen(x, y + h, z0)
]),
right:
points([
screen(x + w, y + h, z1),
screen(x + w, y, z1),
screen(x + w, y, z0),
screen(x + w, y + h, z0)
])
}
end
defp points(list), do: Enum.map_join(list, " ", fn {x, y} -> "#{x},#{y}" end)
defp r(number), do: Float.round(number * 1.0, 1)
end
assets/css/hero/security-gateway.css
/* hero/security-gateway */
.ehv-hero-security-gateway { --ehv-max-w: 760px; --ehv-step: 110ms; --ehv-dur: 800ms; }
/* Hairlines stay one pixel wide on the ground and on the faces of the wall. */
.ehv-hero-security-gateway__plane * { vector-effect: non-scaling-stroke; }
.ehv-hero-security-gateway__wall {
fill: var(--ehv-surface-1);
stroke: var(--ehv-border-strong);
stroke-width: 1;
stroke-linejoin: round;
}
.ehv-hero-security-gateway__top {
fill: var(--ehv-surface-3);
stroke: var(--ehv-border-strong);
stroke-width: 1;
stroke-linejoin: round;
}
.ehv-hero-security-gateway__side {
fill: var(--ehv-surface-2);
stroke: var(--ehv-border-soft);
stroke-width: 1;
stroke-linejoin: round;
}
/* A gate: the opening, the floor inside it and the inner side of the wall. */
.ehv-hero-security-gateway__gate {
fill: var(--ehv-canvas);
stroke: var(--ehv-border-strong);
stroke-width: 1;
}
.ehv-hero-security-gateway__gate--shut { fill: var(--ehv-surface-3); stroke: var(--ehv-coral); }
.ehv-hero-security-gateway__floor { fill: var(--ehv-surface-2); }
.ehv-hero-security-gateway__jamb { fill: var(--ehv-surface-3); }
.ehv-hero-security-gateway__packet { stroke-width: 1; }
.ehv-hero-security-gateway__packet--ink { fill: var(--ehv-surface-3); stroke: var(--ehv-ink-3); }
.ehv-hero-security-gateway__packet--lime { fill: var(--ehv-lime-fill); stroke: var(--ehv-lime); }
.ehv-hero-security-gateway__packet--coral { fill: var(--ehv-coral); stroke: var(--ehv-coral); }
.ehv-hero-security-gateway__tag { fill: var(--ehv-canvas); stroke: var(--ehv-coral); stroke-width: 1; }
.ehv--hover .ehv-hero-security-gateway__gate { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-hero-security-gateway__gate:not(.ehv-hero-security-gateway__gate--shut) { stroke: var(--ehv-lime); }