Flow Field Particles — click to burst

Code

Flow Field module · lib/phx_demo/examples/flow_field.ex
defmodule PhxDemo.Examples.FlowField do
  @width 900
  @height 560
  @count 1300

  def width, do: @width
  def height, do: @height

  def init do
    particles = for _ <- 1..@count, do: new_particle()
    %{particles: particles, t: 0}
  end

  def tick(%{particles: particles, t: t}) do
    t2 = t + 1

    particles =
      Enum.map(particles, fn p ->
        a = angle(p.x, p.y, t2)
        vx = p.vx * 0.85 + :math.cos(a) * 1.4
        vy = p.vy * 0.85 + :math.sin(a) * 1.4
        x = wrap(p.x + vx, @width)
        y = wrap(p.y + vy, @height)
        life = p.life - 1

        if life <= 0 do
          new_particle()
        else
          %{p | x: x, y: y, vx: vx, vy: vy, life: life}
        end
      end)

    %{particles: particles, t: t2}
  end

  def add_burst(%{particles: particles} = state, x, y) do
    burst =
      for _ <- 1..260 do
        a = :rand.uniform() * 2 * :math.pi()
        s = 1.0 + :rand.uniform() * 4.0
        d = :rand.uniform() * 18.0

        %{
          x: x + :math.cos(a) * d,
          y: y + :math.sin(a) * d,
          vx: :math.cos(a) * s,
          vy: :math.sin(a) * s,
          life: 100 + :rand.uniform(160)
        }
      end

    %{state | particles: Enum.take(burst ++ particles, @count)}
  end

  def background do
    Easel.new(@width, @height)
    |> Easel.set_fill_style("#050914")
    |> Easel.fill_rect(0, 0, @width, @height)
    |> Easel.render()
  end

  def render(%{particles: particles, t: t}) do
    boid_instances =
      Enum.map(particles, fn p ->
        speed = :math.sqrt(p.vx * p.vx + p.vy * p.vy)
        hue = rem(round(p.x / @width * 220 + t), 360)

        %{
          x: p.x,
          y: p.y,
          rotate: :math.atan2(p.vy, p.vx),
          scale_x: 0.7 + speed * 0.25,
          scale_y: 0.7 + speed * 0.25,
          fill: "hsl(#{hue}, 95%, 68%)",
          alpha: 0.9
        }
      end)

    vector_instances =
      for gy <- 0..12, gx <- 0..20 do
        x = gx * 45 + 22
        y = gy * 45 + 22
        a = angle(x, y, t)

        %{
          x: x,
          y: y,
          rotate: a,
          stroke: "rgba(125, 211, 252, 0.25)",
          fill: "rgba(125, 211, 252, 0.25)"
        }
      end

    Easel.new(@width, @height)
    |> Easel.template(
      :boid,
      fn c ->
        c
        |> Easel.begin_path()
        |> Easel.move_to(3.2, 0)
        |> Easel.line_to(-2.0, -1.25)
        |> Easel.line_to(-1.2, 0)
        |> Easel.line_to(-2.0, 1.25)
        |> Easel.close_path()
        |> Easel.fill()
      end,
      x: 1,
      y: 1,
      rotate: 3,
      scale_x: 2,
      scale_y: 2,
      alpha: 2
    )
    |> Easel.template(
      :vec,
      fn c ->
        c
        |> Easel.begin_path()
        |> Easel.move_to(-6, 0)
        |> Easel.line_to(4, 0)
        |> Easel.set_line_width(1)
        |> Easel.stroke()
        |> Easel.begin_path()
        |> Easel.move_to(4, 0)
        |> Easel.line_to(0, -2)
        |> Easel.line_to(0, 2)
        |> Easel.close_path()
        |> Easel.fill()
      end,
      x: 0,
      y: 0,
      rotate: 3
    )
    |> Easel.instances(:vec, vector_instances)
    |> Easel.instances(:boid, boid_instances)
    |> Easel.render()
  end

  defp angle(x, y, t) do
    :math.sin(x * 0.012 + t * 0.02) * 2.1 + :math.cos(y * 0.01 - t * 0.015) * 1.7
  end

  defp wrap(v, max) when v < 0, do: v + max
  defp wrap(v, max) when v > max, do: v - max
  defp wrap(v, _max), do: v

  defp new_particle do
    %{
      x: :rand.uniform() * @width,
      y: :rand.uniform() * @height,
      vx: 0.0,
      vy: 0.0,
      life: 80 + :rand.uniform(200)
    }
  end
end
Flow Field LiveView · lib/phx_demo_web/live/flow_field_live.ex
defmodule PhxDemoWeb.FlowFieldLive do
  use PhxDemoWeb, :live_view

  def mount(_params, _session, socket) do
    state = PhxDemo.Examples.FlowField.init()
    first = PhxDemo.Examples.FlowField.render(state)

    socket =
      socket
      |> assign(:state, state)
      |> assign(:background, PhxDemo.Examples.FlowField.background())
      |> assign(:canvas, %{ops: first.ops})
      |> assign(:templates, first.templates)
      |> Easel.LiveView.animate(
        "fg",
        :state,
        fn state ->
          {Easel.new(), PhxDemo.Examples.FlowField.tick(state)}
        end,
        interval: 16
      )

    {:ok, socket}
  end

  def handle_info({:easel_tick, id}, socket) do
    socket = Easel.LiveView.tick(socket, id)
    frame = PhxDemo.Examples.FlowField.render(socket.assigns.state)
    {:noreply, Easel.LiveView.draw(socket, "fg", frame, clear: true)}
  end

  def handle_event("fg:mousedown", %{"x" => x, "y" => y}, socket) do
    state = PhxDemo.Examples.FlowField.add_burst(socket.assigns.state, x * 1.0, y * 1.0)
    {:noreply, assign(socket, :state, state)}
  end

  def handle_event("fg:click", params, socket), do: handle_event("fg:mousedown", params, socket)

  def render(assigns) do
    ~H"""
    <.demo title="Flow Field Particles — click to burst" code_id="flow">
      <Easel.LiveView.canvas_stack id="flow" width={@background.width} height={@background.height}>
        <:layer id="bg" ops={@background.ops} />
        <:layer id="fg" ops={@canvas.ops} templates={@templates} on_click on_mouse_down />
      </Easel.LiveView.canvas_stack>
    </.demo>
    """
  end
end