integrations/logo-reel.ex
Logo reel
A row of system tiles stepping through a centre spotlight, with the name of the one in it underneath.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.integrations_logo_reel animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
systems |
list | [%{name: "ERP", caption: "Orders, stock and invoices"}, %{name: "CRM", caption: "Customers and open deals"}, ...] |
Three to eight `%{name, caption}`. The name is six characters at most, the caption 40. |
active |
integer | 0 |
Index of the system that sits in the spotlight in the still frame. |
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. |
isometric |
boolean | false |
Tilts the composition into the isometric plane of the platform stack. |
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/integrations/logo-reel.ex
defmodule EHVisuals.Integrations.LogoReel do
@moduledoc """
A row of system tiles with one in a centre spotlight and its name
underneath. The tiles step through the spotlight one by one.
The reel is two cycles wide. One animation jumps it a tile at a time over a
whole cycle, a second one slides it by one tile at the end of every step,
so the two together read as a row that steps and never ends.
"""
use EHVisuals.Composition,
name: "integrations/logo-reel",
title: "Logo reel",
description:
"A row of system tiles stepping through a centre spotlight, with the name of the one in it underneath."
@tile 72
@pitch 92
@center 164
@lead 3
@systems [
%{name: "ERP", caption: "Orders, stock and invoices"},
%{name: "CRM", caption: "Customers and open deals"},
%{name: "Mail", caption: "The shared order inbox"},
%{name: "Sheets", caption: "The price list the team keeps"},
%{name: "Bank", caption: "Statements and payments"},
%{name: "WMS", caption: "Warehouse movements"}
]
# Glyphs are drawn in a 12 by 12 box.
@glyphs %{
"ERP" => "M1.5 1.5h9v9h-9z M6 1.5v9 M1.5 6h9",
"CRM" =>
"M6 5.4a2.2 2.2 0 1 0 0-4.4a2.2 2.2 0 0 0 0 4.4z M1.8 11c0-2.2 1.9-3.6 4.2-3.6s4.2 1.4 4.2 3.6",
"MAIL" => "M1 2.5h10v7h-10z M1 3l5 4l5-4",
"SHEETS" => "M1.5 1.5h9v9h-9z M1.5 4.5h9 M1.5 7.5h9 M4.5 1.5v9",
"BANK" => "M1 4.5l5-3l5 3z M2.5 5v4.5 M6 5v4.5 M9.5 5v4.5 M1 10.5h10",
"WMS" => "M1.5 3.5l4.5-2l4.5 2v5l-4.5 2l-4.5-2z M1.5 3.5l4.5 2l4.5-2 M6 5.5v5",
"HR" => "M2 1.5h8v9h-8z M6 5.6a1.4 1.4 0 1 0 0-2.8a1.4 1.4 0 0 0 0 2.8z M3.8 8.4h4.4",
"BI" => "M2.5 10.5v-4 M6 10.5v-8 M9.5 10.5v-6 M1 10.5h10",
"CHAT" => "M1.5 2h9v6h-5l-2.5 2.5v-2.5h-1.5z",
"DOCS" => "M2.5 1.5h5l2 2v7h-7z M4.5 5.5h3 M4.5 7.8h3"
}
@fallback_glyph "M6 2.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z"
attr(:systems, :list,
default: @systems,
doc: "Three to eight `%{name, caption}`. The name is six characters at most, the caption 40."
)
attr(:active, :integer,
default: 0,
doc: "Index of the system that sits in the spotlight in the still frame."
)
attr(:label, :string,
default: "A row of company system tiles with the ERP in the spotlight and its name underneath"
)
frame_attrs()
@spec integrations_logo_reel(map()) :: Phoenix.LiveView.Rendered.t()
def integrations_logo_reel(assigns) do
systems = Enum.take(assigns.systems, 8)
count = max(length(systems), 1)
active = assigns.active |> max(0) |> min(count - 1)
# A cycle is the systems repeated until it is at least seven tiles long.
cycle = count * ceil(7 / count)
tiles =
for position <- 0..(2 * cycle - 1) do
system = Enum.at(systems, Integer.mod(active - @lead + position, count), %{name: ""})
%{
name: system.name,
glyph: Map.get(@glyphs, String.upcase(system.name), @fallback_glyph),
x: @center + (position - @lead) * @pitch
}
end
labels =
for {system, index} <- Enum.with_index(systems) do
%{
name: system.name,
caption: Map.get(system, :caption, ""),
on: index == active,
step: Integer.mod(index - active, count),
dot_x: Float.round(200 + (index - (count - 1) / 2) * 12, 1)
}
end
assigns =
assigns
|> assign(:tiles, tiles)
|> assign(:labels, labels)
|> assign(:count, count)
|> assign(:cycle, cycle)
|> assign(:reel_x, @center - @lead * @pitch)
|> assign(:reel_w, 2 * cycle * @pitch)
|> assign(:tile, @tile)
~H"""
<.frame name="integrations/logo-reel" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 400 208">
<g class="ehv-h-lift">
<rect
class="ehv-integrations-logo-reel__spot ehv-a-pop"
x="152"
y="12"
width="96"
height="96"
rx="20"
/>
<g class="ehv-a-fade ehv-i-1">
<g class={"ehv-integrations-logo-reel__reel ehv-integrations-logo-reel__reel--m#{@cycle}"}>
<g class="ehv-integrations-logo-reel__slide">
<rect class="ehv-fill-none" x={@reel_x} y="24" width={@reel_w} height={@tile} />
<g :for={tile <- @tiles}>
<rect class="ehv-card" x={tile.x} y="24" width={@tile} height={@tile} rx="16" />
<path
class="ehv-line-ink ehv-round ehv-integrations-logo-reel__glyph"
d={tile.glyph}
transform={"translate(#{tile.x + 26.4} #{38}) scale(1.6)"}
/>
<text
class="ehv-mono ehv-t-2"
x={tile.x + 36}
y="79"
font-size="8"
text-anchor="middle"
>
{String.upcase(tile.name)}
</text>
</g>
</g>
</g>
</g>
<g class="ehv-a-fade ehv-i-3">
<line class="ehv-line-lime" x1="200" x2="200" y1="108" y2="118" />
<circle :for={label <- @labels} class="ehv-fill-4" cx={label.dot_x} cy="184" r="2.5" />
<g class={"ehv-integrations-logo-reel__labels--n#{@count}"}>
<g
:for={label <- @labels}
class={[
"ehv-integrations-logo-reel__label ehv-i-#{label.step}",
label.on && "ehv-integrations-logo-reel__label--on"
]}
>
<text class="ehv-mono ehv-t-lime" x="200" y="138" font-size="10" text-anchor="middle">
{String.upcase(label.name)}
</text>
<text x="200" y="159" font-size="13" text-anchor="middle">{label.caption}</text>
<circle class="ehv-fill-lime" cx={label.dot_x} cy="184" r="3.5" />
</g>
</g>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/integrations/logo-reel.css
/* integrations/logo-reel */
.ehv-integrations-logo-reel { --ehv-max-w: 460px; --ehv-iso-scale: 0.82; --ehv-reel-step: 2.6s; }
/* The row runs past both edges and fades out there. The mask sits on the SVG
itself, so it works without an id in the markup. */
.ehv-integrations-logo-reel .ehv__stage > svg {
-webkit-mask-image: linear-gradient(to right, transparent, #000 24%, #000 76%, transparent);
mask-image: linear-gradient(to right, transparent, #000 24%, #000 76%, transparent);
}
.ehv-integrations-logo-reel__spot { fill: var(--ehv-lime-soft); stroke: var(--ehv-lime); stroke-width: 1.25; }
.ehv-integrations-logo-reel__glyph { stroke-width: 0.8; }
/* Only the name of the tile in the spotlight shows. */
.ehv-integrations-logo-reel__label { opacity: 0; }
.ehv-integrations-logo-reel__label--on { opacity: 1; }
/* The reel is two cycles wide: half its width is one cycle, taken a tile at
a time. The slide inside moves one tile (92) at the end of every step. */
@keyframes ehv-integrations-logo-reel-jump { to { transform: translateX(-50%); } }
@keyframes ehv-integrations-logo-reel-slide {
0%, 68% { transform: translateX(0); }
100% { transform: translateX(-92px); }
}
.ehv-integrations-logo-reel__reel { transform-box: fill-box; }
.ehv--animated .ehv-integrations-logo-reel__reel { animation: ehv-integrations-logo-reel-jump linear infinite; }
.ehv--animated .ehv-integrations-logo-reel__reel--m7 { animation-duration: calc(7 * var(--ehv-reel-step)); animation-timing-function: steps(7); }
.ehv--animated .ehv-integrations-logo-reel__reel--m8 { animation-duration: calc(8 * var(--ehv-reel-step)); animation-timing-function: steps(8); }
.ehv--animated .ehv-integrations-logo-reel__reel--m9 { animation-duration: calc(9 * var(--ehv-reel-step)); animation-timing-function: steps(9); }
.ehv--animated .ehv-integrations-logo-reel__reel--m10 { animation-duration: calc(10 * var(--ehv-reel-step)); animation-timing-function: steps(10); }
.ehv--animated .ehv-integrations-logo-reel__reel--m12 { animation-duration: calc(12 * var(--ehv-reel-step)); animation-timing-function: steps(12); }
.ehv--animated .ehv-integrations-logo-reel__slide { animation: ehv-integrations-logo-reel-slide var(--ehv-reel-step) var(--ehv-ease) infinite; }
/* A name shows for one step out of n and leaves as the row starts to slide.
Its stagger index is the step it belongs to, so a negative delay puts
every name at its place in the loop. */
@keyframes ehv-integrations-logo-reel-show-1 { 0%, 100% { opacity: 1; } }
@keyframes ehv-integrations-logo-reel-show-2 { 0% { opacity: 0; } 4%, 34% { opacity: 1; } 40%, 100% { opacity: 0; } }
@keyframes ehv-integrations-logo-reel-show-3 { 0% { opacity: 0; } 3%, 22.7% { opacity: 1; } 26.7%, 100% { opacity: 0; } }
@keyframes ehv-integrations-logo-reel-show-4 { 0% { opacity: 0; } 2%, 17% { opacity: 1; } 20%, 100% { opacity: 0; } }
@keyframes ehv-integrations-logo-reel-show-5 { 0% { opacity: 0; } 2%, 13.6% { opacity: 1; } 16%, 100% { opacity: 0; } }
@keyframes ehv-integrations-logo-reel-show-6 { 0% { opacity: 0; } 1.5%, 11.3% { opacity: 1; } 13.3%, 100% { opacity: 0; } }
@keyframes ehv-integrations-logo-reel-show-7 { 0% { opacity: 0; } 1.3%, 9.7% { opacity: 1; } 11.4%, 100% { opacity: 0; } }
@keyframes ehv-integrations-logo-reel-show-8 { 0% { opacity: 0; } 1.1%, 8.5% { opacity: 1; } 10%, 100% { opacity: 0; } }
.ehv--animated .ehv-integrations-logo-reel__label {
animation: linear infinite;
animation-duration: calc(var(--ehv-reel-n) * var(--ehv-reel-step));
animation-delay: calc((var(--ehv-i, 0) - var(--ehv-reel-n)) * var(--ehv-reel-step));
}
.ehv-integrations-logo-reel__labels--n1 { --ehv-reel-n: 1; }
.ehv-integrations-logo-reel__labels--n2 { --ehv-reel-n: 2; }
.ehv-integrations-logo-reel__labels--n3 { --ehv-reel-n: 3; }
.ehv-integrations-logo-reel__labels--n4 { --ehv-reel-n: 4; }
.ehv-integrations-logo-reel__labels--n5 { --ehv-reel-n: 5; }
.ehv-integrations-logo-reel__labels--n6 { --ehv-reel-n: 6; }
.ehv-integrations-logo-reel__labels--n7 { --ehv-reel-n: 7; }
.ehv-integrations-logo-reel__labels--n8 { --ehv-reel-n: 8; }
.ehv--animated .ehv-integrations-logo-reel__labels--n1 .ehv-integrations-logo-reel__label { animation-name: ehv-integrations-logo-reel-show-1; }
.ehv--animated .ehv-integrations-logo-reel__labels--n2 .ehv-integrations-logo-reel__label { animation-name: ehv-integrations-logo-reel-show-2; }
.ehv--animated .ehv-integrations-logo-reel__labels--n3 .ehv-integrations-logo-reel__label { animation-name: ehv-integrations-logo-reel-show-3; }
.ehv--animated .ehv-integrations-logo-reel__labels--n4 .ehv-integrations-logo-reel__label { animation-name: ehv-integrations-logo-reel-show-4; }
.ehv--animated .ehv-integrations-logo-reel__labels--n5 .ehv-integrations-logo-reel__label { animation-name: ehv-integrations-logo-reel-show-5; }
.ehv--animated .ehv-integrations-logo-reel__labels--n6 .ehv-integrations-logo-reel__label { animation-name: ehv-integrations-logo-reel-show-6; }
.ehv--animated .ehv-integrations-logo-reel__labels--n7 .ehv-integrations-logo-reel__label { animation-name: ehv-integrations-logo-reel-show-7; }
.ehv--animated .ehv-integrations-logo-reel__labels--n8 .ehv-integrations-logo-reel__label { animation-name: ehv-integrations-logo-reel-show-8; }
.ehv--hover:hover :is(.ehv-integrations-logo-reel__reel, .ehv-integrations-logo-reel__slide, .ehv-integrations-logo-reel__label) { animation-play-state: paused; }