empowered.house ↗

sections/backgrounds.ex

Section backgrounds

A section preview with one of the brand's backgrounds behind a heading: grid, dots, warm glow or diagonal hairlines.

Use it
# in the module that renders the template
use EHVisuals

<div class="h-96 rounded-xl border border-border">
  <.sections_backgrounds animated />
</div>
grid
dot field
warm glow · custom copy
diagonal hairlines · hover
light surface · isometric
grid · fade out · still

Props

Prop Type Default What it does
pattern "grid" | "dots" | "glow" | "lines" "grid" The background behind the heading.
eyebrow string "Order to cash"
heading string "One place for every order" 34 characters at most.
text string "Orders, stock and invoices in the same system." 62 characters at most. Empty hides it.
action string "See how it works"
secondary string "Read the docs" Empty hides the second button.
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/sections/backgrounds.ex
backgrounds.ex
defmodule EHVisuals.Sections.Backgrounds do
  @moduledoc """
  A section preview that shows one of the brand's background styles behind a
  heading: a faint grid, a dot field, the warm glow, or diagonal hairlines.

  Every style is drawn from plain shapes. The glow is a stack of flat
  ellipses, so the picture needs no gradient and no id.
  """

  use EHVisuals.Composition,
    name: "sections/backgrounds",
    title: "Section backgrounds",
    description:
      "A section preview with one of the brand's backgrounds behind a heading: grid, dots, warm glow or diagonal hairlines."

  @labels %{
    "grid" => "A section heading over a faint grid",
    "dots" => "A section heading over a field of dots",
    "glow" => "A section heading over the warm glow",
    "lines" => "A section heading over diagonal hairlines"
  }

  attr(:pattern, :string,
    default: "grid",
    values: ~w(grid dots glow lines),
    doc: "The background behind the heading."
  )

  attr(:eyebrow, :string, default: "Order to cash")
  attr(:heading, :string, default: "One place for every order", doc: "34 characters at most.")

  attr(:text, :string,
    default: "Orders, stock and invoices in the same system.",
    doc: "62 characters at most. Empty hides it."
  )

  attr(:action, :string, default: "See how it works")
  attr(:secondary, :string, default: "Read the docs", doc: "Empty hides the second button.")

  attr(:label, :any,
    default: nil,
    doc: "What the picture shows. Left empty, it is written from the pattern."
  )

  frame_attrs()

  @spec sections_backgrounds(map()) :: Phoenix.LiveView.Rendered.t()
  def sections_backgrounds(assigns) do
    action_w = Float.round(26 + String.length(assigns.action) * 6.0, 1)

    secondary_w =
      if assigns.secondary == "",
        do: 0,
        else: Float.round(26 + String.length(assigns.secondary) * 6.0, 1)

    total = action_w + if(secondary_w > 0, do: 10 + secondary_w, else: 0)
    buttons_y = if assigns.text == "", do: 142, else: 164

    assigns =
      assigns
      |> assign(:label, assigns.label || Map.fetch!(@labels, assigns.pattern))
      |> assign(:columns, for(index <- 0..10, do: {20 + index * 40, index}))
      |> assign(:lanes, for(index <- 0..6, do: {12 + index * 40, index}))
      |> assign(:dot_columns, for(index <- 0..18, do: {22 + index * 22, index}))
      |> assign(:dot_rows, for(index <- 0..10, do: 22 + index * 22))
      |> assign(:rings, rings())
      |> assign(:clearing, for(index <- 0..5, do: {190 - index * 22, 84 - index * 9}))
      |> assign(:hairlines, hairlines())
      |> assign(:action_w, action_w)
      |> assign(:secondary_w, secondary_w)
      |> assign(:action_x, Float.round(220 - total / 2, 1))
      |> assign(:secondary_x, Float.round(220 - total / 2 + action_w + 10, 1))
      |> assign(:buttons_y, buttons_y)

    ~H"""
    <.frame name="sections/backgrounds" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 440 264">
        <g class="ehv-h-lift">
          <rect class="ehv-well ehv-a-fade" x="0.5" y="0.5" width="439" height="263" rx="12" />

          <g :if={@pattern == "grid"}>
            <line
              :for={{x, index} <- @columns}
              class={"ehv-line-soft ehv-a-draw ehv-i-#{index + 1}"}
              x1={x + 0.5}
              x2={x + 0.5}
              y1="1"
              y2="263"
              pathLength="1"
            />
            <line
              :for={{y, index} <- @lanes}
              class={"ehv-line-soft ehv-a-draw ehv-i-#{index + 4}"}
              x1="1"
              x2="439"
              y1={y + 0.5}
              y2={y + 0.5}
              pathLength="1"
            />
          </g>

          <g :if={@pattern == "dots"}>
            <g :for={{x, index} <- @dot_columns} class={"ehv-a-fade ehv-i-#{index + 1}"}>
              <circle :for={y <- @dot_rows} class="ehv-fill-strong" cx={x} cy={y} r="1.2" />
            </g>
          </g>

          <g :if={@pattern == "glow"}>
            <ellipse
              :for={{{rx, ry}, index} <- @rings}
              class={"ehv-sections-backgrounds__glow ehv-a-fade ehv-i-#{div(index, 2) + 1}"}
              cx="220"
              cy="132"
              rx={rx}
              ry={ry}
            />
          </g>

          <g :if={@pattern == "lines"}>
            <line
              :for={{{x1, y1, x2, y2}, index} <- @hairlines}
              class={"ehv-line-soft ehv-a-draw ehv-i-#{div(index, 2) + 1}"}
              x1={x1}
              y1={y1}
              x2={x2}
              y2={y2}
              pathLength="1"
            />
          </g>

          <g :if={@pattern != "glow"} class="ehv-a-fade ehv-i-5">
            <ellipse
              :for={{rx, ry} <- @clearing}
              class="ehv-sections-backgrounds__clearing"
              cx="220"
              cy="128"
              rx={rx}
              ry={ry}
            />
          </g>

          <g class="ehv-a-rise ehv-i-6">
            <text class="ehv-mono ehv-t-lime" x="220" y="86" font-size="8.5" text-anchor="middle">
              {String.upcase(@eyebrow)}
            </text>
            <text class="ehv-t-display" x="220" y="118" font-size="24" text-anchor="middle">
              {@heading}
            </text>
            <text
              :if={@text != ""}
              class="ehv-t-2"
              x="220"
              y="142"
              font-size="12"
              text-anchor="middle"
            >
              {@text}
            </text>
          </g>
          <g class="ehv-a-rise ehv-i-8">
            <rect
              class="ehv-fill-ink ehv-sections-backgrounds__action"
              x={@action_x}
              y={@buttons_y}
              width={@action_w}
              height="28"
              rx="8"
            />
            <text
              class="ehv-t-strong ehv-sections-backgrounds__action-text"
              x={@action_x + @action_w / 2}
              y={@buttons_y + 18}
              font-size="11"
              text-anchor="middle"
            >
              {@action}
            </text>
            <g :if={@secondary != ""}>
              <rect
                class="ehv-card"
                x={@secondary_x + 0.5}
                y={@buttons_y + 0.5}
                width={@secondary_w - 1}
                height="27"
                rx="8"
              />
              <text
                class="ehv-t-strong"
                x={@secondary_x + @secondary_w / 2}
                y={@buttons_y + 18}
                font-size="11"
                text-anchor="middle"
              >
                {@secondary}
              </text>
            </g>
          </g>
          <text class="ehv-mono ehv-a-fade ehv-i-10" x="16" y="248" font-size="7.5">
            BACKGROUND / {String.upcase(@pattern)}
          </text>
        </g>
      </svg>
    </.frame>
    """
  end

  # The glow: flat ellipses that get smaller, each a little more light.
  defp rings do
    for index <- 0..13 do
      {{206 - index * 13, 118 - index * 7.5}, index}
    end
  end

  # Hairlines running up to the right, each cut where it meets the edge of
  # the section. The box is inset so no line pokes out of a rounded corner.
  defp hairlines do
    {left, top, right, bottom} = {3, 3, 437, 261}

    for {sum, index} <- Enum.with_index(Enum.to_list(28..676//24)) do
      {x1, y1} = if sum - top <= right, do: {sum - top, top}, else: {right, sum - right}
      {x2, y2} = if sum - bottom >= left, do: {sum - bottom, bottom}, else: {left, sum - left}
      {{x1, y1, x2, y2}, index}
    end
  end
end
assets/css/sections/backgrounds.css
backgrounds.css
/* sections/backgrounds */
.ehv-sections-backgrounds { --ehv-max-w: 500px; --ehv-step: 60ms; --ehv-iso-scale: 0.82; }

/* One step of the warm glow. Fourteen of them stack up towards the middle. */
.ehv-sections-backgrounds__glow { fill: var(--ehv-glow); opacity: 0.065; }

/* The pattern thins out behind the heading: flat ellipses in the colour of
   the section, stacked the same way. */
.ehv-sections-backgrounds__clearing { fill: var(--ehv-canvas); opacity: 0.22; }

/* The first button is ink on the section, with the text reversed. */
.ehv svg .ehv-sections-backgrounds__action-text { fill: var(--ehv-canvas); }

.ehv--hover .ehv-sections-backgrounds__action { transition: fill 200ms var(--ehv-ease); }
.ehv--hover .ehv-sections-backgrounds__action:hover { fill: var(--ehv-lime-fill); }