Lissajous Curves

Code

Lissajous module · lib/phx_demo/examples/lissajous.ex
defmodule PhxDemo.Examples.Lissajous do
  @width 700
  @height 500

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

  def render(t) do
    cx = @width / 2
    cy = @height / 2

    Easel.new(@width, @height)
    |> Easel.set_fill_style("rgba(5, 10, 24, 0.12)")
    |> Easel.fill_rect(0, 0, @width, @height)
    |> draw_curve(cx, cy, t)
    |> Easel.render()
  end

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

  defp draw_curve(canvas, cx, cy, t) do
    a = 3
    b = 2
    delta = t / 40
    points = 480

    yaw = t / 180
    pitch = :math.sin(t / 140) * 0.8

    Enum.reduce(0..points, canvas, fn i, acc ->
      u = i / points
      angle = u * 2 * :math.pi()

      # base lissajous in a faux 3D space
      x0 = 180 * :math.sin(a * angle + delta)
      y0 = 140 * :math.sin(b * angle)
      z0 = 110 * :math.sin(4 * angle + delta * 1.3)

      # rotate around Y (yaw)
      x1 = x0 * :math.cos(yaw) + z0 * :math.sin(yaw)
      z1 = -x0 * :math.sin(yaw) + z0 * :math.cos(yaw)

      # rotate around X (pitch)
      y2 = y0 * :math.cos(pitch) - z1 * :math.sin(pitch)
      z2 = y0 * :math.sin(pitch) + z1 * :math.cos(pitch)

      perspective = 1.0 + z2 / 420
      x = cx + x1 * perspective
      y = cy + y2 * perspective

      hue = rem(round(u * 360 + t), 360)
      lw = 1.0 + perspective * 1.6

      if i == 0 do
        acc
        |> Easel.begin_path()
        |> Easel.move_to(x, y)
      else
        acc
        |> Easel.set_stroke_style("hsl(#{hue}, 90%, 65%)")
        |> Easel.set_line_width(lw)
        |> Easel.line_to(x, y)
        |> Easel.stroke()
      end
    end)
  end
end
Lissajous LiveView · lib/phx_demo_web/live/lissajous_live.ex
defmodule PhxDemoWeb.LissajousLive do
  use PhxDemoWeb, :live_view

  def mount(_params, _session, socket) do
    t = 0

    socket =
      socket
      |> assign(:t, t)
      |> assign(:background, PhxDemo.Examples.Lissajous.background())
      |> assign(:canvas, PhxDemo.Examples.Lissajous.render(t))
      |> Easel.LiveView.animate(
        "fg",
        :t,
        fn t ->
          t2 = t + 2
          {PhxDemo.Examples.Lissajous.render(t2), t2}
        end,
        interval: 33,
        canvas_assign: :canvas
      )

    {:ok, socket}
  end

  def handle_info({:easel_tick, id}, socket) do
    {:noreply, Easel.LiveView.tick(socket, id)}
  end

  def render(assigns) do
    ~H"""
    <.demo title="Lissajous Curves" code_id="lissajous">
      <Easel.LiveView.canvas_stack
        id="lissajous"
        width={@background.width}
        height={@background.height}
      >
        <:layer id="bg" ops={@background.ops} />
        <:layer id="fg" ops={@canvas.ops} />
      </Easel.LiveView.canvas_stack>
    </.demo>
    """
  end
end