empowered.house ↗

ai/neural-net.ex

Neural net

A layered network of nodes with one path lit in lime as a pulse travels from input to output.

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

<div class="h-96 rounded-xl border border-border">
  <.ai_neural_net animated />
</div>
default
isometric
three layers · one output
five layers · hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
caption string "Order risk model"
result string "Approve · 0.94" What the lit output node says.
layers list [3, 5, 4, 2] Two to five columns, each the number of nodes in it (one to six).
path list [1, 2, 1, 0] The lit node of each column, counted from the top. Missing entries are 0.
labels list ["Input", "Hidden", "Hidden", "Output"] A short name under each column.
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/ai/neural-net.ex
neural-net.ex
defmodule EHVisuals.Ai.NeuralNet do
  @moduledoc """
  A layered network: nodes in columns, every node joined to the next column,
  and one path lit as a signal travels from input to output.
  """

  use EHVisuals.Composition,
    name: "ai/neural-net",
    title: "Neural net",
    description:
      "A layered network of nodes with one path lit in lime as a pulse travels from input to output."

  attr(:caption, :string, default: "Order risk model")
  attr(:result, :string, default: "Approve · 0.94", doc: "What the lit output node says.")

  attr(:layers, :list,
    default: [3, 5, 4, 2],
    doc: "Two to five columns, each the number of nodes in it (one to six)."
  )

  attr(:path, :list,
    default: [1, 2, 1, 0],
    doc: "The lit node of each column, counted from the top. Missing entries are 0."
  )

  attr(:labels, :list,
    default: ~w(Input Hidden Hidden Output),
    doc: "A short name under each column."
  )

  attr(:label, :string,
    default: "A four layer network with one path from input to output lit in lime"
  )

  frame_attrs()

  @spec ai_neural_net(map()) :: Phoenix.LiveView.Rendered.t()
  def ai_neural_net(assigns) do
    columns = columns(assigns.layers, assigns.path)
    lit = Enum.map(columns, fn column -> Enum.find(column.nodes, & &1.lit) end)
    last = List.last(lit)

    assigns =
      assigns
      |> assign(:columns, columns)
      |> assign(:edges, edges(columns))
      |> assign(:hops, lit |> Enum.zip(Enum.drop(lit, 1)) |> Enum.with_index())
      |> assign(:last, last)
      |> assign(:midway, midway(lit))
      |> assign(:xs, Enum.map_join(lit, ";", & &1.x))
      |> assign(:ys, Enum.map_join(lit, ";", & &1.y))
      |> assign(:dur, "#{Float.round(max(length(lit) - 1, 1) * 0.9, 1)}s")
      |> assign(:names, Enum.zip(columns, assigns.labels))

    ~H"""
    <.frame name="ai/neural-net" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 420 280">
        <g class="ehv-h-lift">
          <text class="ehv-mono ehv-a-fade" x="8" y="14" font-size="9">
            {String.upcase(@caption)}
          </text>
          <text
            class="ehv-mono ehv-t-lime ehv-a-fade ehv-i-12"
            x="412"
            y="14"
            font-size="9"
            text-anchor="end"
          >
            {String.upcase(@result)}
          </text>
          <line
            :for={edge <- @edges}
            class={"ehv-line-soft ehv-a-fade ehv-i-#{edge.layer + 2}"}
            x1={edge.x1}
            y1={edge.y1}
            x2={edge.x2}
            y2={edge.y2}
          />
          <line
            :for={{{from, to}, index} <- @hops}
            class={"ehv-line-lime ehv-a-draw ehv-i-#{index * 3 + 5}"}
            x1={from.x}
            y1={from.y}
            x2={to.x}
            y2={to.y}
            pathLength="1"
          />
          <g :for={column <- @columns} class={"ehv-a-pop ehv-i-#{column.index + 1}"}>
            <g :for={node <- column.nodes}>
              <circle
                :if={node.lit}
                class="ehv-fill-lime-soft"
                cx={node.x}
                cy={node.y}
                r="13"
              />
              <circle
                class={if node.lit, do: "ehv-node-lime", else: "ehv-ai-neural-net__node"}
                cx={node.x}
                cy={node.y}
                r="7.5"
              />
              <circle
                :if={node.lit}
                class="ehv-ai-neural-net__core"
                cx={node.x}
                cy={node.y}
                r="3.5"
              />
            </g>
          </g>
          <circle class="ehv-ai-neural-net__core ehv-a-ping ehv-i-14" cx={@last.x} cy={@last.y} r="7" />
          <circle
            :if={!@animated}
            class="ehv-ai-neural-net__core"
            cx={elem(@midway, 0)}
            cy={elem(@midway, 1)}
            r="3"
          />
          <g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-14">
            <circle class="ehv-ai-neural-net__core" r="3.5">
              <animate attributeName="cx" values={@xs} dur={@dur} repeatCount="indefinite" />
              <animate attributeName="cy" values={@ys} dur={@dur} repeatCount="indefinite" />
              <animate
                attributeName="opacity"
                values="0;1;1;0"
                keyTimes="0;0.1;0.85;1"
                dur={@dur}
                repeatCount="indefinite"
              />
            </circle>
          </g>
          <text
            :for={{column, name} <- @names}
            class={[
              "ehv-mono ehv-a-fade ehv-i-#{column.index + 3}",
              column.index == length(@columns) - 1 && "ehv-t-lime"
            ]}
            x={column.x}
            y="270"
            font-size="8.5"
            text-anchor="middle"
          >
            {String.upcase(name)}
          </text>
        </g>
      </svg>
    </.frame>
    """
  end

  # Columns sit evenly between x = 48 and x = 372, nodes 36 apart around y = 138.
  defp columns(layers, path) do
    sizes = layers |> Enum.take(5) |> Enum.map(&(&1 |> max(1) |> min(6)))
    sizes = if sizes == [], do: [1], else: sizes
    count = length(sizes)
    gap = if count > 1, do: 324 / (count - 1), else: 0

    for {size, index} <- Enum.with_index(sizes) do
      x = Float.round(if(count > 1, do: 48 + index * gap, else: 210.0), 1)
      chosen = path |> Enum.at(index, 0) |> max(0) |> min(size - 1)

      nodes =
        for row <- 0..(size - 1) do
          %{x: x, y: Float.round(138 + (row - (size - 1) / 2) * 36, 1), lit: row == chosen}
        end

      %{index: index, x: x, nodes: nodes}
    end
  end

  # Every node is joined to every node of the next column.
  defp edges(columns) do
    for {left, right} <- Enum.zip(columns, Enum.drop(columns, 1)),
        from <- left.nodes,
        to <- right.nodes do
      %{layer: left.index, x1: from.x, y1: from.y, x2: to.x, y2: to.y}
    end
  end

  # Where the pulse rests in the still frame: halfway along the middle hop.
  defp midway([only]), do: {only.x, only.y}

  defp midway(lit) do
    hop = div(length(lit) - 1, 2)
    from = Enum.at(lit, hop)
    to = Enum.at(lit, hop + 1)
    {Float.round((from.x + to.x) / 2, 1), Float.round((from.y + to.y) / 2, 1)}
  end
end
assets/css/ai/neural-net.css
neural-net.css
/* ai/neural-net */
.ehv-ai-neural-net { --ehv-max-w: 470px; --ehv-step: 110ms; }

.ehv-ai-neural-net__node { fill: var(--ehv-surface-2); stroke: var(--ehv-border-strong); stroke-width: 1; }
.ehv-ai-neural-net__core { fill: var(--ehv-lime); }

.ehv--hover .ehv-ai-neural-net__node { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover .ehv-ai-neural-net__node:hover { stroke: var(--ehv-lime); }