Kino Custom
Mix.install([
{:kino, "~> 0.14"}
])
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")