Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Kino Custom

livebooks/kino/kino_custom.livemd

Kino Custom

Mix.install([
  {:kino, "~> 0.12"}
])

Kino.JS

defmodule KinoDocs.HTML do
  use Kino.JS

  def new(html) do
    Kino.JS.new(__MODULE__, html)
  end

  asset "main.js" do
    """
    export function init(ctx, html) {
      ctx.importCSS("https://fonts.googleapis.com/css?family=Sofia")
      ctx.importCSS("main.css")

      ctx.root.innerHTML = html;
    }
    """
  end

  asset "main.css" do
    """
    body {
      font-family: "Sofia", sans-serif;
    }
    """
  end
end
KinoDocs.HTML.new("""

  Hello
  
  • World
  • Elixir
  • Livebook
  • Kino
"""
)
defmodule KinoCustom.List do
  use Kino.JS

  def new(element) do
    element
    |> generate_html()
    |> then(&Kino.JS.new(__MODULE__, "#{&1}"))
  end

  defp generate_html(element) when is_list(element) do
    if Keyword.keyword?(element) do
      element
      |> Enum.into(%{})
      |> generate_html()
    else
      element
      |> Enum.map(&"#{generate_html(&1)}")
      |> then(&"
    #{&1}
"
) end end defp generate_html(element) when is_tuple(element) do element |> Tuple.to_list() |> generate_html() end defp generate_html(element) when is_map(element) do element |> Enum.map(fn {key, value} -> cond do is_binary(value) or is_atom(value) or is_number(value) -> "#{value}" true -> generate_html(value) end |> then(&"#{key}: #{&1}") |> generate_html() end) end defp generate_html(element) when is_binary(element) or is_atom(element) or is_number(element) do "
  • #{element}
  • "
    end asset "main.js" do """ export function init(ctx, html) { ctx.importCSS("main.css") ctx.root.innerHTML = html; } """ end asset "main.css" do """ li { color: rgb(79, 53, 96); width: fit-content; } li::marker { content: "- "; } """ end end
    KinoCustom.List.new(["a", "b", "c"])
    KinoCustom.List.new([
      "a",
      %{"b" => ["b1", "b2"]},
      %{c: {%{c1: "A"}, "c2", [1, 2]}},
      [d1: 10, d2: [21, 22]]
    ])
    defmodule KinoDocs.Mermaid do
      use Kino.JS
    
      def new(graph) do
        Kino.JS.new(__MODULE__, graph)
      end
    
      asset "main.js" do
        """
        import "https://cdn.jsdelivr.net/npm/mermaid@9.1.3/dist/mermaid.min.js";
    
        mermaid.initialize({ startOnLoad: false });
    
        export function init(ctx, graph) {
          mermaid.render("graph1", graph, (svgSource, bindListeners) => {
            ctx.root.innerHTML = svgSource;
            bindListeners && bindListeners(ctx.root);
          });
        }
        """
      end
    end
    KinoDocs.Mermaid.new("""
    graph TD;
      A-->B;
      A-->C;
      B-->D;
      C-->D;
    """)
    defmodule KinoCustom.Three do
      use Kino.JS
    
      def new(color) do
        Kino.JS.new(__MODULE__, color)
      end
    
      asset "main.js" do
        """
        import "https://unpkg.com/three@0.142.0/build/three.min.js";
    
        export function init(ctx, color) {
          const canvas = document.createElement("canvas");
          ctx.root.appendChild(canvas);
    
          const renderer = new THREE.WebGLRenderer({canvas: canvas});
          const width = 320;
          const height = 320;
          renderer.setSize(width, height);
    
          const scene = new THREE.Scene();
    
          const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000);
    
          camera.position.set(0, 0, 500);
    
          const size = 80;
          const geometry = new THREE.BoxGeometry(size, size, size);
          const material = new THREE.MeshStandardMaterial({color: color});
    
          const box = new THREE.Mesh(geometry, material);
          scene.add(box);
    
          const light = new THREE.DirectionalLight(0xffffff);
          light.intensity = 2;
          light.position.set(1, 1, 1);
          scene.add(light);
    
          light.position.set(1, 1, 1);
    
          renderer.render(scene, camera);
    
          tick();
    
          function tick() {
            requestAnimationFrame(tick);
    
            box.rotation.x += 0.05;
            box.rotation.y -= 0.05;
    
            renderer.render(scene, camera);
          }
        }
        """
      end
    end
    KinoCustom.Three.new("green")

    Kino.JS.Live

    defmodule KinoDocs.LiveHTML do
      use Kino.JS
      use Kino.JS.Live
    
      def new(html) do
        Kino.JS.Live.new(__MODULE__, html)
      end
    
      def replace(kino, html) do
        Kino.JS.Live.cast(kino, {:replace, html})
      end
    
      @impl true
      def init(html, ctx) do
        {:ok, assign(ctx, html: html)}
      end
    
      @impl true
      def handle_connect(ctx) do
        {:ok, ctx.assigns.html, ctx}
      end
    
      @impl true
      def handle_cast({:replace, html}, ctx) do
        broadcast_event(ctx, "replace", html)
        {:noreply, assign(ctx, html: html)}
      end
    
      asset "main.js" do
        """
        export function init(ctx, html) {
          ctx.root.innerHTML = html;
    
          ctx.handleEvent("replace", (html) => {
            ctx.root.innerHTML = html;
          });
        }
        """
      end
    end
    list =
      KinoDocs.LiveHTML.new("""
      

    Hello

    """
    )
    KinoDocs.LiveHTML.replace(list, """
    

    World

    """
    )
    defmodule KinoCustom.Bar do
      use Kino.JS
      use Kino.JS.Live
    
      def new(width) do
        Kino.JS.Live.new(__MODULE__, width)
      end
    
      def update(kino, width) do
        Kino.JS.Live.cast(kino, {:update, width})
      end
    
      @impl true
      def init(html, ctx) do
        {:ok, assign(ctx, html: html)}
      end
    
      @impl true
      def handle_connect(ctx) do
        {:ok, ctx.assigns.html, ctx}
      end
    
      @impl true
      def handle_cast({:update, width}, ctx) do
        broadcast_event(ctx, "update", width)
        {:noreply, assign(ctx, width: width)}
      end
    
      asset "main.js" do
        """
        export function init(ctx, width) {
          const bar = document.createElement("div");
          bar.className = "bar";
          bar.style.width = width;
          bar.style.height = "40px";
          bar.style.backgroundColor = "red";
    
          ctx.root.appendChild(bar);
    
          ctx.handleEvent("update", (width) => {
            bar.style.width = width
          });
        }
        """
      end
    end
    bar = KinoCustom.Bar.new("50%")
    Stream.interval(50)
    |> Stream.take(100)
    |> Kino.animate(fn width ->
      KinoCustom.Bar.update(bar, "#{width}%")
    end)

    Kino.SmartCell

    defmodule Kino.SmartCell.Plain do
      use Kino.JS
      use Kino.JS.Live
      use Kino.SmartCell, name: "Plain code editor"
    
      @impl true
      def init(attrs, ctx) do
        source = attrs["source"] || ""
        {:ok, assign(ctx, source: source)}
      end
    
      @impl true
      def handle_connect(ctx) do
        {:ok, %{source: ctx.assigns.source}, ctx}
      end
    
      @impl true
      def handle_event("update", %{"source" => source}, ctx) do
        broadcast_event(ctx, "update", %{"source" => source})
        {:noreply, assign(ctx, source: source)}
      end
    
      @impl true
      def to_attrs(ctx) do
        %{"source" => ctx.assigns.source}
      end
    
      @impl true
      def to_source(attrs) do
        attrs["source"]
      end
    
      asset "main.js" do
        """
        export function init(ctx, payload) {
          ctx.importCSS("main.css");
    
          ctx.root.innerHTML = `
            
          `;
    
          const textarea = ctx.root.querySelector("#source");
          textarea.value = payload.source;
    
          textarea.addEventListener("change", (event) => {
            ctx.pushEvent("update", { source: event.target.value });
          });
    
          ctx.handleEvent("update", ({ source }) => {
            textarea.value = source;
          });
    
          ctx.handleSync(() => {
            // Synchronously invokes change listeners
            document.activeElement &&
              document.activeElement.dispatchEvent(new Event("change"));
          });
        }
        """
      end
    
      asset "main.css" do
        """
        #source {
          box-sizing: border-box;
          width: 100%;
          min-height: 100px;
        }
        """
      end
    end
    Kino.SmartCell.register(Kino.SmartCell.Plain)
    target = "World"
    
    "Hello, #{target}"
    defmodule KinoCustom.Color do
      use Kino.JS
      use Kino.JS.Live
    
      def new(color) do
        Kino.JS.Live.new(__MODULE__, color)
      end
    
      @impl true
      def init(html, ctx) do
        {:ok, assign(ctx, html: html)}
      end
    
      @impl true
      def handle_connect(ctx) do
        {:ok, ctx.assigns.html, ctx}
      end
    
      @impl true
      def handle_cast({:update, color}, ctx) do
        broadcast_event(ctx, "update", color)
        {:noreply, assign(ctx, color: color)}
      end
    
      asset "main.js" do
        """
        export function init(ctx, color) {
          const bar = document.createElement("div");
          bar.style.width = "100%";
          bar.style.height = "40px";
          bar.style.backgroundColor = color;
    
          ctx.root.appendChild(bar);
        }
        """
      end
    end
    KinoCustom.Color.new("red")
    defmodule KinoCustom.Palette do
      use Kino.JS
      use Kino.JS.Live
      use Kino.SmartCell, name: "Palette"
    
      @impl true
      def init(attrs, ctx) do
        color = attrs["color"] || "white"
        {:ok, assign(ctx, color: color)}
      end
    
      @impl true
      def handle_connect(ctx) do
        {:ok, %{color: ctx.assigns.color}, ctx}
      end
    
      @impl true
      def handle_event("update", %{"color" => color}, ctx) do
        broadcast_event(ctx, "update", %{"color" => color})
        {:noreply, assign(ctx, color: color)}
      end
    
      @impl true
      def to_attrs(ctx) do
        %{"color" => ctx.assigns.color}
      end
    
      @impl true
      def to_source(attrs) do
        quote do
          KinoCustom.Color.new(unquote(attrs["color"]))
        end
        |> Kino.SmartCell.quoted_to_string()
      end
    
      asset "main.js" do
        """
        export function init(ctx, payload) {
          ctx.importCSS("main.css");
    
          const input = document.createElement("input");
          input.type = "text"
          input.value = payload.color;
    
          const output = document.createElement("output");
          output.style.color = payload.color;
    
          const rgbContainer = document.createElement("div");
    
          const rLabel = document.createElement("span");
          rLabel.innerText = "R: ";
          rgbContainer.appendChild(rLabel);
          const rValue = document.createElement("span");
          rValue.className = "color-value";
          rValue.innerText = "255";
          rgbContainer.appendChild(rValue);
    
          const gLabel = document.createElement("span");
          gLabel.innerText = "G: ";
          rgbContainer.appendChild(gLabel);
          const gValue = document.createElement("span");
          gValue.className = "color-value";
          gValue.innerText = "255";
          rgbContainer.appendChild(gValue);
    
          const bLabel = document.createElement("span");
          bLabel.innerText = "B: ";
          rgbContainer.appendChild(bLabel);
          const bValue = document.createElement("span");
          bValue.className = "color-value";
          bValue.innerText = "255";
          rgbContainer.appendChild(bValue);
    
          ctx.root.appendChild(input);
          ctx.root.appendChild(output);
          ctx.root.appendChild(rgbContainer);
    
          input.addEventListener("change", (event) => {
            ctx.pushEvent("update", { color: event.target.value });
          });
    
          ctx.handleEvent("update", ({ color }) => {
            input.value = color;
            output.style.color = color;
    
            const rgb =
              window
                .getComputedStyle(output)
                .color
                .replace("rgb(", "")
                .replace(")", "")
                .split(",")
                .map(ch => ch.trim());
            
            rValue.innerText = rgb[0];
            gValue.innerText = rgb[1];
            bValue.innerText = rgb[2];
    
            console.log(rgb);
          });
    
          ctx.handleSync(() => {
            document.activeElement &&
              document.activeElement.dispatchEvent(new Event("change"));
          });
        }
        """
      end
    
      asset "main.css" do
        """
        .color-value {
          margin-right: 16px;
        }
        """
      end
    end
    Kino.SmartCell.register(KinoCustom.Palette)
    KinoCustom.Color.new("white")