media/video-player.ex
Video player
A video player frame with a play button, a title, the time and a progress bar, for demos and walkthroughs.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.media_video_player animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "Month-end close, walkthrough" |
Up to 36 characters. |
meta |
string | "Recorded by Marta Nowak · Finance" |
|
elapsed |
string | "02:14" |
|
duration |
string | "06:20" |
|
progress |
float | 0.35 |
The watched share of the video, 0.0 to 1.0. |
scene |
"hills" | "peak" | "sun" | "grid" | "blocks" | "hills" |
The placeholder picture in the 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/media/video-player.ex
defmodule EHVisuals.Media.VideoPlayer do
@moduledoc """
A video player: the frame with a placeholder picture and a play button, the
title, the time and a progress bar.
"""
use EHVisuals.Composition,
name: "media/video-player",
title: "Video player",
description:
"A video player frame with a play button, a title, the time and a progress bar, for demos and walkthroughs."
attr(:title, :string, default: "Month-end close, walkthrough", doc: "Up to 36 characters.")
attr(:meta, :string, default: "Recorded by Marta Nowak · Finance")
attr(:elapsed, :string, default: "02:14")
attr(:duration, :string, default: "06:20")
attr(:progress, :float,
default: 0.35,
doc: "The watched share of the video, 0.0 to 1.0."
)
attr(:scene, :string,
default: "hills",
values: ~w(hills peak sun grid blocks),
doc: "The placeholder picture in the frame."
)
attr(:label, :string,
default: "A video player with a play button and a progress bar a third of the way"
)
frame_attrs()
@spec media_video_player(map()) :: Phoenix.LiveView.Rendered.t()
def media_video_player(assigns) do
progress = min(max(assigns.progress, 0.0), 1.0)
assigns =
assigns
|> assign(:picture, scene(assigns.scene, 10, 10, 380, 208))
|> assign(:played_w, Float.round(progress * 360, 1))
|> assign(:knob_x, Float.round(20 + progress * 360, 1))
~H"""
<.frame name="media/video-player" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 400 292">
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="399" height="291" rx="12" />
<g class="ehv-a-fade ehv-i-1">
<rect class="ehv-well" x="10.5" y="10.5" width="379" height="207" rx="8" />
<circle
:if={@picture.sun}
class="ehv-fill-4 ehv-a-pop ehv-i-5"
cx={elem(@picture.sun, 0)}
cy={elem(@picture.sun, 1)}
r={elem(@picture.sun, 2)}
/>
<path
class="ehv-media-video-player__scene ehv-round ehv-a-draw ehv-i-2"
d={@picture.lines}
pathLength="1"
/>
</g>
<g class="ehv-a-pop ehv-i-4">
<g class="ehv-media-video-player__play">
<circle class="ehv-fill-canvas" cx="200" cy="114" r="36" />
<circle class="ehv-fill-lime" cx="200" cy="114" r="27" />
<path class="ehv-media-video-player__glyph" d="M193 102.5 l18 11.5 l-18 11.5 z" />
</g>
</g>
<g class="ehv-a-fade ehv-i-5">
<text class="ehv-t-strong" x="20" y="244" font-size="13.5">{@title}</text>
<text class="ehv-mono" x="20" y="259" font-size="8.5">{String.upcase(@meta)}</text>
<text class="ehv-mono" x="380" y="244" font-size="9" text-anchor="end">
<tspan class="ehv-mono ehv-t-ink">{@elapsed}</tspan>
/ {@duration}
</text>
</g>
<g class="ehv-a-fade ehv-i-6">
<rect class="ehv-fill-soft" x="20" y="272" width="360" height="4" rx="2" />
<rect
class="ehv-fill-2 ehv-a-grow-x ehv-i-7"
x="20"
y="272"
width={@played_w}
height="4"
rx="2"
/>
<g class="ehv-a-pop ehv-i-10">
<circle class="ehv-fill-ink ehv-media-video-player__knob" cx={@knob_x} cy="274" r="5.5" />
</g>
</g>
</g>
</svg>
</.frame>
"""
end
# A placeholder picture in the box at {x, y} of size {w, h}: hairlines for a
# horizon and a landscape, and an optional sun as {cx, cy, r}.
defp scene(kind, x, y, w, h) do
px = fn f -> Float.round(x + f * w, 1) end
py = fn f -> Float.round(y + f * h, 1) end
r = Float.round(min(w, h) * 0.08, 1)
case kind do
"peak" ->
%{
lines:
"M#{px.(0.06)} #{py.(0.8)} H#{px.(0.94)} " <>
"M#{px.(0.1)} #{py.(0.8)} L#{px.(0.25)} #{py.(0.34)} L#{px.(0.32)} #{py.(0.54)} " <>
"L#{px.(0.36)} #{py.(0.47)} L#{px.(0.45)} #{py.(0.8)} " <>
"M#{px.(0.57)} #{py.(0.8)} L#{px.(0.73)} #{py.(0.4)} L#{px.(0.9)} #{py.(0.8)}",
sun: {px.(0.84), py.(0.2), r}
}
"sun" ->
%{
lines:
"M#{px.(0.06)} #{py.(0.74)} H#{px.(0.94)} " <>
"M#{px.(0.66)} #{py.(0.74)} A#{Float.round(0.1 * w, 1)} #{Float.round(0.1 * w, 1)} 0 0 1 #{px.(0.86)} #{py.(0.74)} " <>
"M#{px.(0.69)} #{py.(0.82)} H#{px.(0.83)} M#{px.(0.73)} #{py.(0.89)} H#{px.(0.79)} " <>
"M#{px.(0.1)} #{py.(0.74)} C#{px.(0.17)} #{py.(0.48)} #{px.(0.29)} #{py.(0.48)} #{px.(0.38)} #{py.(0.74)}",
sun: nil
}
"grid" ->
rays =
Enum.map_join([0.06, 0.2, 0.35, 0.5, 0.65, 0.8, 0.94], " ", fn f ->
"M#{px.(0.5)} #{py.(0.73)} L#{px.(f)} #{py.(0.92)}"
end)
rungs =
Enum.map_join([0.34, 0.64, 1.0], " ", fn t ->
"M#{px.(0.5 - 0.44 * t)} #{py.(0.73 + 0.19 * t)} H#{px.(0.5 + 0.44 * t)}"
end)
%{
lines: "M#{px.(0.06)} #{py.(0.73)} H#{px.(0.94)} #{rays} #{rungs}",
sun: {px.(0.84), py.(0.2), r}
}
"blocks" ->
blocks =
Enum.map_join(
[
{0.1, 0.19, 0.56},
{0.22, 0.34, 0.34},
{0.4, 0.48, 0.72},
{0.52, 0.6, 0.75},
{0.66, 0.78, 0.3},
{0.81, 0.9, 0.58}
],
" ",
fn {left, right, top} ->
"M#{px.(left)} #{py.(0.8)} V#{py.(top)} H#{px.(right)} V#{py.(0.8)}"
end
)
%{lines: "M#{px.(0.06)} #{py.(0.8)} H#{px.(0.94)} #{blocks}", sun: nil}
_hills ->
%{
lines:
"M#{px.(0.06)} #{py.(0.8)} H#{px.(0.94)} " <>
"M#{px.(0.08)} #{py.(0.8)} C#{px.(0.16)} #{py.(0.4)} #{px.(0.3)} #{py.(0.4)} #{px.(0.4)} #{py.(0.8)} " <>
"M#{px.(0.6)} #{py.(0.8)} C#{px.(0.68)} #{py.(0.34)} #{px.(0.84)} #{py.(0.38)} #{px.(0.93)} #{py.(0.8)} " <>
"M#{px.(0.34)} #{py.(0.8)} C#{px.(0.42)} #{py.(0.7)} #{px.(0.58)} #{py.(0.7)} #{px.(0.66)} #{py.(0.8)}",
sun: {px.(0.84), py.(0.2), r}
}
end
end
end
assets/css/media/video-player.css
/* media/video-player */
.ehv-media-video-player { --ehv-max-w: 440px; }
.ehv-media-video-player__scene { fill: none; stroke: var(--ehv-ink-3); stroke-width: 1.25; }
.ehv-media-video-player__glyph { fill: var(--ehv-on-lime); }
.ehv-media-video-player__play { transform-box: fill-box; transform-origin: center; }
.ehv--hover .ehv-media-video-player__play { transition: transform 220ms var(--ehv-ease); }
.ehv--hover:hover .ehv-media-video-player__play { transform: scale(1.08); }
.ehv--hover .ehv-media-video-player__knob { transition: r 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-media-video-player__knob { r: 7; }