diff --git a/lib/livebook/notebook/learn.ex b/lib/livebook/notebook/learn.ex
index 27b0a4640..dcc39a7bb 100644
--- a/lib/livebook/notebook/learn.ex
+++ b/lib/livebook/notebook/learn.ex
@@ -86,17 +86,13 @@ defmodule Livebook.Notebook.Learn do
}
},
%{
- ref: :kino_reference,
- path: Path.join(__DIR__, "learn/kino/reference.livemd")
+ ref: :kino_intro,
+ path: Path.join(__DIR__, "learn/kino/intro_to_kino.livemd")
},
%{
ref: :kino_vm_introspection,
path: Path.join(__DIR__, "learn/kino/vm_introspection.livemd")
},
- %{
- ref: :kino_pong,
- path: Path.join(__DIR__, "learn/kino/pong.livemd")
- },
%{
ref: :kino_custom_kinos,
path: Path.join(__DIR__, "learn/kino/custom_kinos.livemd")
@@ -200,15 +196,14 @@ defmodule Livebook.Notebook.Learn do
@group_configs [
%{
- title: "Interactive notebooks with Kino",
+ title: "Deep dive into Kino",
description:
"Learn more about the Kino package, including the creation of custom UI components.",
cover_url: "/images/kino.png",
notebook_refs: [
- :kino_reference,
+ :kino_intro,
:kino_vm_introspection,
:kino_custom_kinos,
- :kino_pong,
:kino_smart_cells
]
}
diff --git a/lib/livebook/notebook/learn/deploy_apps.livemd b/lib/livebook/notebook/learn/deploy_apps.livemd
index a878b59f3..6dbefe9fb 100644
--- a/lib/livebook/notebook/learn/deploy_apps.livemd
+++ b/lib/livebook/notebook/learn/deploy_apps.livemd
@@ -14,14 +14,14 @@ To do so, we will use Livebook's companion library called
In a nutshell, Kino is a library that you install as part
of your notebooks to make your notebooks interactive.
-Kino comes from the Greek prefix of the same name and it
-stands for "motion". And, as you learn the library, it
-will become clear that this is precisely what it brings to
-our notebooks.
+Kino comes from the Greek prefix "kino-" and it stands for
+"motion". As you learn the library, it will become clear that
+this is precisely what it brings to our notebooks.
-There is many functionality in the Kino library: it can render
-Markdown, animate frames, display tables, manage inputs, and
-more. For building notebook applications, we rely on two main
+Kino can render Markdown, animate frames, display tables,
+manage inputs, and more. It also provides the building blocks
+for extending Livebook with charts, smart cells, and much more.
+For building notebook applications, we rely on two main
building blocks: `Kino.Control` and `Kino.Frame`.
You can see `kino` listed as a dependency above, so let's run
diff --git a/lib/livebook/notebook/learn/kino/reference.livemd b/lib/livebook/notebook/learn/kino/intro_to_kino.livemd
similarity index 73%
rename from lib/livebook/notebook/learn/kino/reference.livemd
rename to lib/livebook/notebook/learn/kino/intro_to_kino.livemd
index 0911594aa..693445e38 100644
--- a/lib/livebook/notebook/learn/kino/reference.livemd
+++ b/lib/livebook/notebook/learn/kino/intro_to_kino.livemd
@@ -1,4 +1,4 @@
-# Introduction to Kino
+# A look into built-in Kinos
```elixir
Mix.install([
@@ -8,12 +8,15 @@ Mix.install([
## Introduction
-In this notebook we will explore the possibilities that
-[`kino`](https://github.com/elixir-nx/kino) brings
-into your notebooks. Kino can be thought of as Livebook's
-friend that instructs it how to render certain widgets
-and interact with them. You can see `kino` listed as a
-dependency above, let's run the setup cell and get started!
+Throughout the Learning section, we have used Kino several times.
+Sometimes we use built-in Kinos, such as using `Kino.Control` and
+`Kino.Frame` to deploy applications](/learn/notebooks/deploy-apps),
+other times we used custom Kinos tailired for
+[data exploration](/learn/notebooks/intro-to-explorer) or
+[plotting](/learn/notebooks/intro-to-vega-lite).
+
+In this notebook, we will explore several of the built-in Kinos.
+`kino` is already listed as a dependency, so let's get started.
@@ -37,6 +40,25 @@ IO.puts("Hello, #{Kino.Input.read(name)}!")
There are multiple types of inputs, such as text areas,
color dialogs, selects, and more. Feel free to explore them.
+One important feature of inputs is that they are shared.
+Once someone changes an input, it will reflect on all users
+currently seeing the notebook.
+
+
+
+## Kino.Control
+
+The [`Kino.Control`](https://hexdocs.pm/kino/Kino.Control.html)
+module represents forms, buttons, and other interactive controls.
+Opposite to `Kino.Input`, each user has their own control and
+the main way to interact with controls is by listening to their
+events.
+
+```elixir
+Kino.Control.button("Click me!")
+|> Kino.listen(fn event -> IO.inspect(event) end)
+```
+
## Kino.Markdown
@@ -170,7 +192,7 @@ Kino.Tree.new(data)
-## Kino.render/1
+## Kino.render/1 and Kino.Frame
As we saw, Livebook automatically recognises widgets returned
from each cell and renders them accordingly. However, sometimes
@@ -190,66 +212,22 @@ Kino.render(Kino.Markdown.new("**Hello world**"))
"Cell result 🚀"
```
-
-
-## Kino.Frame and animations
-
-`Kino.Frame` allows us to render an empty frame and update it
-as we progress. Let's render an empty frame:
+The `Kino.Frame` construct we used when deploying our chat app is a
+generalization of `Kino.render`, which gives us more control over when
+to update, append, or clear the output:
```elixir
frame = Kino.Frame.new()
```
-Now, let's render a random number between 1 and 100 directly
-in the frame:
+By default, a frame will update in place. Try running the cell below
+several times:
```elixir
Kino.Frame.render(frame, "Got: #{Enum.random(1..100)}")
```
-Notice how every time you reevaluate the cell above it updates
-the frame. You can also use `Kino.Frame.append/2` to append to
-the frame:
-
-```elixir
-Kino.Frame.append(frame, "Got: #{Enum.random(1..100)}")
-```
-
-Appending multiple times will always add new contents. The content
-can be reset by calling `Kino.Frame.render/2` or `Kino.Frame.clear/1`.
-
-By using loops, you can use `Kino.Frame` to dynamically add contents
-or animate your livebooks. In fact, there is a convenience function
-called `Kino.animate/2` to be used exactly for this purpose:
-
-```elixir
-Kino.animate(100, fn i ->
- Kino.Markdown.new("**Iteration: `#{i}`**")
-end)
-```
-
-The above example renders new Markdown output every 100ms.
-You can use the same approach to render regular output
-or images too!
-
-There's also `Kino.animate/3`, in case you need to accumulate state or halt the animation at certain point!
-
-```elixir
-button = Kino.Control.button("Click") |> Kino.render()
-
-button
-|> Kino.Control.stream()
-|> Kino.animate(0, fn _event, counter ->
- new_counter = counter + 1
- md = Kino.Markdown.new("**Clicks: `#{new_counter}`**")
- {:cont, md, new_counter}
-end)
-```
-
-Note that this time, instead of refreshing the animation every 100ms, we use an event stream. This way we refresh the animation whenever the button is clicked.
-
-Finally, there's `Kino.listen/{2,3}`, that allows you to consume a stream the same way, but doesn't render anything on its own.
+but you can use `append` and `clear` to get different results.
@@ -322,15 +300,9 @@ to inspect, toggle, and swap each operation along the way:
|> dbg()
```
-## Next steps with custom Kinos
+## Next steps
-With this, we finished our introduction to Kino. Most the guides
-ahead of us will use Kino in one way or the other. You can jump
-into [the VegaLite guide](/learn/notebooks/intro-to-vega-lite)
-for plotting charts or [the MapLibre guide](/learn/notebooks/intro-to-maplibre)
-for rendering maps to learn how other packages extend Livebook
-through Kino.
-
-We also have a collection of deep dive guides into Kino in the
-[Learn](/learn) page if you want to learn more, including how
-to create your custom widgets.
+We have learned many new Kinos in this section. In the next guide,
+we will put some of our new found knowledge into practice [by rendering
+inputs, plotting graphs, and drawing diagrams with information retrieved
+from the notebook runtime](/learn/notebooks/vm-introspection).
diff --git a/lib/livebook/notebook/learn/kino/pong.livemd b/lib/livebook/notebook/learn/kino/pong.livemd
deleted file mode 100644
index b62da2cc0..000000000
--- a/lib/livebook/notebook/learn/kino/pong.livemd
+++ /dev/null
@@ -1,937 +0,0 @@
-# Multiplayer pong game from scratch
-
-```elixir
-Mix.install([
- {:kino, "~> 0.9.0"}
-])
-```
-
-## Introduction
-
-In this notebook, we are going to learn more about
-[`Kino.Control`](https://hexdocs.pm/kino/Kino.Control.html)
-and [`Kino.Image`](https://hexdocs.pm/kino/Kino.Image.html).
-Specifically, we will be building the the [Pong](https://en.wikipedia.org/wiki/Pong)
-game directly in Livebook. Not only that, we will actually
-make it multiplayer!
-
-## Painting the scene
-
-The first step in our game is to define our scene. The
-scene has a `width`, `height`, and several game objects
-that we want to render into the scene. For rendering
-the objects, we will use a custom protocol function,
-called `Scene.Renderer.to_svg/1`. This will allow us
-to define as many game objects as we want and teach the
-scene how to render them, without changing the scene code:
-
-```elixir
-defmodule Scene do
- @moduledoc """
- The scene for the game objects.
- """
-
- defstruct [:width, :height, :objects]
-
- def new(width, height) do
- %__MODULE__{width: width, height: height, objects: []}
- end
-
- def add(scene, object) do
- update_in(scene.objects, fn objects -> [object | objects] end)
- end
-
- def to_svg(scene) do
- svgs = Enum.map(scene.objects, &Scene.Renderer.to_svg(&1))
-
- """
-
- """
- end
-end
-```
-
-To render the scene, we will convert it to `svg` and
-use `Kino.Image.new/1` to render it in Livebook:
-
-```elixir
-Scene.new(80, 10)
-|> Scene.to_svg()
-|> Kino.Image.new(:svg)
-```
-
-Since our scene has no objects, we only see an empty white canvas.
-To address this, let's define two shapes and alongside the
-`Scene.Renderer` protocol for them.
-
-Let's first define the protocol:
-
-```elixir
-defprotocol Scene.Renderer do
- def to_svg(object)
-end
-```
-
-Our first shape is a rectangle:
-
-```elixir
-defmodule Scene.Rect do
- defstruct [:x, :y, :width, :height, fill: "black"]
-
- def new(x, y, width, height, opts \\ []) do
- struct!(%Scene.Rect{x: x, y: y, width: width, height: height}, opts)
- end
-
- defimpl Scene.Renderer do
- def to_svg(%Scene.Rect{x: x, y: y, width: width, height: height, fill: fill}) do
- """
-
- """
- end
- end
-end
-```
-
-Our second shape is text:
-
-```elixir
-defmodule Scene.Text do
- defstruct [:x, :y, :text, font_size: 1, fill: "black"]
-
- def new(x, y, text, opts \\ []) do
- struct!(%Scene.Text{x: x, y: y, text: text}, opts)
- end
-
- defimpl Scene.Renderer do
- def to_svg(%Scene.Text{x: x, y: y, text: text, fill: fill, font_size: font_size}) do
- """
- #{text}
- """
- end
- end
-end
-```
-
-Now let's paint a new scene with the rectangle and some text:
-
-```elixir
-Scene.new(80, 10)
-|> Scene.add(Scene.Rect.new(0, 0, 80, 10, fill: "#d4f1f477"))
-|> Scene.add(Scene.Text.new(40, 4, "Hello world!", font_size: 3))
-|> Scene.to_svg()
-|> Kino.Image.new(:svg)
-```
-
-Perfect.
-
-## Animating scene objects
-
-So far we are able to paint static objects in the scene.
-However, all games have dynamic objects that move on the scene
-as time passes. In our case, the dynamic objects are the paddles
-and the ball.
-
-For such, we will define an `Animation` protocol with a single
-function called `step`. It receives the object, the scene, and
-is responsible for animating the object one step at a time.
-
-```elixir
-defprotocol Animation do
- def step(object, scene)
-end
-```
-
-Now let's define our first game object, the `Game.Ball` struct.
-The ball will have a radius `r`, coordinates `x` and `y`, velocities
-`dx` and `dy`, and implement both `Scene.Renderer` and `Animation`
-protocols.
-
-```elixir
-defmodule Game.Ball do
- defstruct [:r, :x, :y, :dx, :dy]
-
- def new(r, x, y, opts \\ []) do
- struct!(%Game.Ball{r: r, x: x, y: y}, opts)
- end
-
- defimpl Scene.Renderer do
- def to_svg(%Game.Ball{x: x, y: y, r: r}) do
- """
-
- """
- end
- end
-
- defimpl Animation do
- # On each step, increase x by dx, and y by dy.
- def step(ball, _scene) do
- %{ball | x: ball.x + ball.dx, y: ball.y + ball.dy}
- end
- end
-end
-```
-
-We can now paint the ball in the scene:
-
-```elixir
-Scene.new(80, 10)
-|> Scene.add(Game.Ball.new(2, 40, 4))
-|> Scene.to_svg()
-|> Kino.Image.new(:svg)
-```
-
-However, we can also animate the ball over the scene using `Kino.Frame`.
-Let's define the frame, the scene, and the ball:
-
-```elixir
-# The frame we will render scene images on top of
-frame = Kino.Frame.new() |> Kino.render()
-
-# The scene with a static background
-scene =
- Scene.new(80, 20)
- |> Scene.add(Scene.Rect.new(0, 0, 80, 20, fill: "#d4f1f477"))
-
-# The ball which we will animate
-ball = Game.Ball.new(2, 2, 4, dx: 1, dy: 1)
-```
-
-Now we will instantiate a `Kino.Control.interval/1` and use it to
-generate a stream of events, one every 33ms (which gives 30 frames per second).
-We will take 20 of those events. On each events, we will paint the
-scene, update the frame, and then animate the ball for the next event:
-
-```elixir
-Kino.Control.interval(33)
-|> Kino.Control.stream()
-|> Stream.take(20)
-|> Enum.reduce(ball, fn _counter, ball ->
- # Build the image
- image = scene |> Scene.add(ball) |> Scene.to_svg() |> Kino.Image.new(:svg)
-
- # Update the frame
- Kino.Frame.render(frame, image)
-
- # Animate the ball
- Animation.step(ball, scene)
-end)
-```
-
-Feel free to tweak the parameters above and see how it affects the ball
-movements.
-
-You may also have noticed that, at the moment, the ball simply moves out
-of the scene when it reaches the edges, which is not what we would expect
-in practice. We will tackle this later once we add collision detection.
-For now, let's learn how to capture the user keyboard and use it to control
-the paddles.
-
-
-
-## Capturing the user keyboard
-
-Each player in a Pong game must use the arrow up and arrow down
-keys to control the paddle. We can achieve this by using
-`Kino.Control.keyboard/1`. Let's instantiate it:
-
-```elixir
-keyboard = Kino.Control.keyboard([:status, :keyup, :keydown])
-```
-
-The above renders a keyboard icon. Once you click it, Livebook
-will start capturing keyboard events. We are interested in three
-different events:
-
-* `:status` - it is emitted whenever the keyboard is activated/deactivated
-* `:keydown` - whenever a key is pressed down
-* `:keyup` - whenever a key is released up
-
-Let's use `Kino.Control.stream/1` to capture and print those events.
-In particular, we want to stream while the keyboard is enabled. Once
-the keyboard is disabled (i.e. we get a `:status` event with the
-`:enabled` key set to `false`), the stream stops:
-
-```elixir
-keyboard
-|> Kino.Control.stream()
-|> Stream.take_while(fn
- %{type: :status, enabled: false} -> false
- _event -> true
-end)
-|> Kino.listen()
-```
-
-Once you execute the cell above, it will start consuming
-the event stream. Click on the keyboard icon from the previous
-cell and watch it print keyboard events as you press different keys.
-When you are done, **disable the keyboard control**. Now we know
-how to capture keyboard events and how they look like, we are
-ready to implement the paddles.
-
-## Controlling the paddle with keypresses
-
-The paddle will be similar to the ball: it is its own struct
-that can be rendered and animated. However, the paddle should
-also be able to handle keyboard events. In particular, a paddle
-only moves up or down, and only while a key is pressed. Since
-there is additional logic here, let's first define `Game.Paddle`
-and the implement the protocols later:
-
-```elixir
-defmodule Game.Paddle do
- defstruct [:x, :y, :width, :height, dy: 0]
- @dy 4
-
- def new(x, y, width, height) do
- %Game.Paddle{x: x, y: y, width: width, height: height}
- end
-
- def on_key(paddle, %{key: "ArrowUp", type: :keydown}), do: %{paddle | dy: -@dy}
- def on_key(paddle, %{key: "ArrowUp", type: :keyup}), do: %{paddle | dy: 0}
- def on_key(paddle, %{key: "ArrowDown", type: :keydown}), do: %{paddle | dy: @dy}
- def on_key(paddle, %{key: "ArrowDown", type: :keyup}), do: %{paddle | dy: 0}
- def on_key(paddle, _), do: paddle
-end
-```
-
-The paddle moves up while the "ArrowUp" key is pressed,
-and down while the "ArrowDown" key is pressed. We will
-render the paddle as a rectangle:
-
-```elixir
-defimpl Scene.Renderer, for: Game.Paddle do
- def to_svg(%Game.Paddle{x: x, y: y, width: width, height: height}) do
- """
-
- """
- end
-end
-```
-
-And animate it over the `y` dimension, with the addition
-that the paddle must never leave the scene:
-
-```elixir
-defimpl Animation, for: Game.Paddle do
- def step(paddle, scene) do
- %{paddle | y: clip(paddle.y + paddle.dy, 0, scene.height - paddle.height)}
- end
-
- defp clip(value, min, max), do: value |> max(min) |> min(max)
-end
-```
-
-We are ready to give it a try!
-
-
-
-## Mixing control streams
-
-To animate the paddle, we will need two control streams:
-the keyboard control and an interval to refresh
-the paddle as we keep holding the arrow keys up
-and down. Let's put the relevant pieces in place:
-
-```elixir
-# The frame we will render scene images on top of
-frame = Kino.Frame.new() |> Kino.render()
-
-# The scene with a static background
-scene =
- Scene.new(200, 50)
- |> Scene.add(Scene.Rect.new(0, 0, 200, 50, fill: "#d4f1f477"))
-
-# The keyboard control
-keyboard = Kino.Control.keyboard([:status, :keyup, :keydown]) |> Kino.render()
-
-# The refresh interval
-interval = Kino.Control.interval(33)
-
-# The paddle which we will animate
-paddle = Game.Paddle.new(0, 21, 2, 8)
-```
-
-Now we want to capture all keyboard events plus animate
-the paddle every 33ms. We can do this by passing a list
-of controls to `Kino.Control.stream/1`. As before, we
-still want to stop the stream as soon as the keyboard is
-disabled:
-
-```elixir
-[keyboard, interval]
-|> Kino.Control.stream()
-|> Stream.take_while(fn
- %{type: :status, enabled: false} -> false
- _event -> true
-end)
-|> Kino.listen(paddle, fn event, paddle ->
- # Build the image
- image = scene |> Scene.add(paddle) |> Scene.to_svg() |> Kino.Image.new(:svg)
-
- # Update the frame
- Kino.Frame.render(frame, image)
-
- # Input the event into the paddle and animate it
- {:cont,
- paddle
- |> Game.Paddle.on_key(event)
- |> Animation.step(scene)}
-end)
-```
-
-Press the keyboard icon and you should be able to control
-the paddle. In particular, as long as you hold the arrow
-up or arrow down keys, the paddle should move, without exiting
-the scene. Once you are done, disable the keyboard.
-
-With all of the game objects in place, let's implement
-collision detection between the scene, the ball, and the
-paddles.
-
-## Collision detection
-
-As the ball moves around, it may collide with different elements.
-Let's describe those scenarios below:
-
-* If the ball hits either the top or the bottom of the scene,
- it should reverse its `y` direction
-
-* If the ball hits any of the paddles, it should reverse its
- `x` direction
-
-* If the ball hits either the left or right side of the scene,
- it means the opposite paddle won
-
-For simplicity, we will assume that:
-
-* The paddles are positioned to the absolute left and absolute
- right of the scene
-
-* For collision detection with the paddle, we will assume the
- ball is a square. In practice this is enough unless the ball
- is much bigger than the paddle
-
-With this in mind, let's see the collision code. Our function
-will either return `{:cont, ball}` or `{:won, :left | :right}`:
-
-```elixir
-defmodule Game.Collision do
- def bounce(ball, left_paddle, right_paddle, scene) do
- ball
- |> bounce_y(scene)
- |> bounce_x(left_paddle, right_paddle, scene)
- end
-
- defp bounce_y(ball, _scene) when ball.y - ball.r <= 0 do
- %{ball | y: ball.r, dy: -ball.dy}
- end
-
- defp bounce_y(ball, scene) when ball.y + ball.r >= scene.height do
- %{ball | y: scene.height - ball.r, dy: -ball.dy}
- end
-
- defp bounce_y(ball, _scene), do: ball
-
- defp bounce_x(ball, left, _right, _scene) when ball.x - ball.r <= left.width do
- if collides_vertically?(ball, left) do
- {:cont, %{ball | x: ball.r + left.width, dx: -ball.dx}}
- else
- {:won, :right}
- end
- end
-
- defp bounce_x(ball, _left, right, scene) when ball.x + ball.r >= scene.width do
- if collides_vertically?(ball, right) do
- {:cont, %{ball | x: scene.width - ball.r - right.width, dx: -ball.dx}}
- else
- {:won, :left}
- end
- end
-
- defp bounce_x(ball, _left, _right, _scene), do: {:cont, ball}
-
- defp collides_vertically?(ball, paddle) do
- ball.y - ball.r < paddle.y + paddle.height and ball.y + ball.r > paddle.y
- end
-end
-```
-
-To ensure we got the different scenarios right, let's add some tests:
-
-```elixir
-ExUnit.start(autorun: false)
-
-defmodule Game.CollisionTest do
- use ExUnit.Case, async: true
-
- @scene Scene.new(200, 100)
- # Position the left paddle on the middle
- @left Game.Paddle.new(0, 40, 2, 20)
- # Position the right paddle on the middle
- @right Game.Paddle.new(198, 40, 2, 20)
-
- def bounce(ball), do: Game.Collision.bounce(ball, @left, @right, @scene)
-
- test "no bouncing" do
- ball = Game.Ball.new(10, _x = 100, _y = 50, dx: 10, dy: 10)
- assert bounce(ball) == {:cont, ball}
- end
-
- describe "bounces on top" do
- test "with perfect collision" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 100, _y = 10, dy: -10))
- assert ball.dy == 10
- assert ball.y == 10
- end
-
- test "with ball slightly outside of scene" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 100, _y = 5, dy: -10))
- assert ball.dy == 10
- assert ball.y == 10
- end
- end
-
- describe "bounces on bottom" do
- test "with perfect collision" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 100, _y = 90, dy: 10))
- assert ball.dy == -10
- assert ball.y == 90
- end
-
- test "with ball slightly outside of scene" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 100, _y = 95, dy: 10))
- assert ball.dy == -10
- assert ball.y == 90
- end
- end
-
- describe "bounces on left" do
- test "with perfect collision on the paddle center" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 2 + 10, _y = 50, dx: -10))
- assert ball.dx == 10
- assert ball.x == 12
- end
-
- test "with ball slightly inside the paddle" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 5, _y = 50, dx: -10))
- assert ball.dx == 10
- assert ball.x == 12
- end
-
- test "with the ball bottom touching the paddle top" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 5, _y = 31, dx: -10))
- assert ball.dx == 10
- assert ball.x == 12
- end
-
- test "with the ball top touching the paddle bottom" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 5, _y = 69, dx: -10))
- assert ball.dx == 10
- assert ball.x == 12
- end
-
- test "with the ball top perfectly missing the paddle bottom" do
- assert bounce(Game.Ball.new(10, _x = 5, _y = 70, dx: -10)) == {:won, :right}
- end
-
- test "with the ball bottom perfectly missing the paddle top" do
- assert bounce(Game.Ball.new(10, _x = 5, _y = 30, dx: -10)) == {:won, :right}
- end
- end
-
- describe "bounces on right" do
- test "with perfect collision on the paddle center" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 200 - 2 - 10, _y = 50, dx: 10))
- assert ball.dx == 10
- assert ball.x == 188
- end
-
- test "with ball slightly inside the paddle" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 195, _y = 50, dx: 10))
- assert ball.dx == -10
- assert ball.x == 188
- end
-
- test "with the ball bottom touching the paddle top" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 195, _y = 31, dx: 10))
- assert ball.dx == -10
- assert ball.x == 188
- end
-
- test "with the ball top touching the paddle bottom" do
- {:cont, ball} = bounce(Game.Ball.new(10, _x = 195, _y = 69, dx: 10))
- assert ball.dx == -10
- assert ball.x == 188
- end
-
- test "with the ball top perfectly missing the paddle bottom" do
- assert bounce(Game.Ball.new(10, _x = 195, _y = 70, dx: 10)) == {:won, :left}
- end
-
- test "with the ball bottom perfectly missing the paddle top" do
- assert bounce(Game.Ball.new(10, _x = 195, _y = 30, dx: 10)) == {:won, :left}
- end
- end
-end
-
-ExUnit.run()
-```
-
-All tests should pass! We are now ready to put all pieces
-together. We will do so in two steps. First we will create
-the `Game.State`, which will keep all objects and know how
-to dispatch events to them. Then we will create the
-`Game.Server` that will hold the state, control the frame,
-keyboards, and refresh rate.
-
-## Encapsulating the game state
-
-The `Game.State` is a module with functions to start a new
-game, receive paddle events, and animate its objects.
-The `Game.State` itself should hold the scene, the ball,
-and both paddles. It should also hold the score and a
-status which control if the game is running or idle.
-The game is idle before it starts and also after each
-player scores.
-
-```elixir
-defmodule Game.State do
- @moduledoc """
- Represents the paddles game state and rules.
- """
-
- @w 400
- @h 200
- @ball_r 8
- @paddle_w 2
- @paddle_h 40
- @paddle_y div(@h - @paddle_h, 2)
-
- defstruct [:ball, :left_paddle, :left_score, :right_paddle, :right_score, :status, :scene]
-
- @doc """
- Returns initial game state.
- """
- def new() do
- scene =
- Scene.new(@w, @h)
- |> Scene.add(Scene.Rect.new(0, 0, @w, @h, fill: "#d4f1f477"))
-
- reset(%__MODULE__{scene: scene, left_score: 0, right_score: 0})
- end
-
- defp reset(state) do
- %{
- state
- | status: :idle,
- ball: new_ball(),
- left_paddle: Game.Paddle.new(0, @paddle_y, @paddle_w, @paddle_h),
- right_paddle: Game.Paddle.new(@w - @paddle_w, @paddle_y, @paddle_w, @paddle_h)
- }
- end
-
- defp new_ball() do
- Game.Ball.new(
- @ball_r,
- div(@w, 2),
- div(@h, 2),
- # Each new ball goes in a random direction
- dx: Enum.random([3, -3]),
- dy: Enum.random([2, -2])
- )
- end
-
- @doc """
- Marks the game as running.
- """
- def start(state) do
- %{state | status: :running}
- end
-
- @doc """
- Applies the event to the given paddle.
- """
- def on_key(state, :left, event) do
- update_in(state.left_paddle, &Game.Paddle.on_key(&1, event))
- end
-
- def on_key(state, :right, event) do
- update_in(state.right_paddle, &Game.Paddle.on_key(&1, event))
- end
-
- @doc """
- Performs a single step within the game by updating object positions.
- """
- def step(%{status: :running} = state) do
- %Game.State{
- ball: ball,
- left_paddle: left_paddle,
- right_paddle: right_paddle,
- scene: scene
- } = state
-
- ball = Animation.step(ball, scene)
- left_paddle = Animation.step(left_paddle, scene)
- right_paddle = Animation.step(right_paddle, scene)
-
- case Game.Collision.bounce(ball, left_paddle, right_paddle, scene) do
- {:cont, ball} ->
- %{state | ball: ball, left_paddle: left_paddle, right_paddle: right_paddle}
-
- {:won, :left} ->
- reset(update_in(state.left_score, &(&1 + 1)))
-
- {:won, :right} ->
- reset(update_in(state.right_score, &(&1 + 1)))
- end
- end
-
- def step(state), do: state
-
- @doc """
- Returns an SVG representation of the game board.
- """
- def to_svg(state) do
- %Game.State{
- ball: ball,
- left_paddle: left_paddle,
- left_score: left_score,
- right_paddle: right_paddle,
- right_score: right_score,
- scene: scene
- } = state
-
- text = Scene.Text.new(div(@w, 2), 4, "#{left_score} : #{right_score}", font_size: 10)
-
- scene
- |> Scene.add(ball)
- |> Scene.add(left_paddle)
- |> Scene.add(right_paddle)
- |> Scene.add(text)
- |> Scene.to_svg()
- end
-end
-```
-
-The most complex function above is `Game.State.step/1`. It
-animates the ball and the paddles. Then it checks for collisions.
-Depending on the result of the collision, it resets the game
-state and bumps the relevant score.
-
-Let's render the game statically and verify everything is in place:
-
-```elixir
-Game.State.new()
-|> Game.State.to_svg()
-|> Kino.Image.new(:svg)
-```
-
-Looks good! Now let's animate it over a frame until one of the paddles score:
-
-```elixir
-# The frame we will render scene images on top of
-frame = Kino.Frame.new() |> Kino.render()
-
-# The scene with a static background
-game = Game.State.new()
-
-# Every 33ms, paint a new frame until it completes a round
-Kino.Control.interval(33)
-|> Kino.Control.stream()
-|> Kino.listen(Game.State.start(game), fn _, game ->
- if game.status == :running do
- game = Game.State.step(game)
- Kino.Frame.render(frame, game |> Game.State.to_svg() |> Kino.Image.new(:svg))
- {:cont, game}
- else
- :halt
- end
-end)
-```
-
-Perfect. Now we are ready to wrap it all up with the `Game.Server`.
-
-## Running the game server
-
-The job of the `Game.Server` is to wire everything together.
-It will render the keyboard and the frame. However, there is
-one important difference. So far, we have been using streams
-to drive our objects and animations. Streams are fine for quick
-examples but, now that we have both `state` and `events`, our
-code will be cleaner if we organize it inside an Elixir process.
-
-In Elixir, we often implement those processes using an abstraction
-called [`GenServer`](https://hexdocs.pm/elixir/GenServer.html).
-So that's what we will use below. We won't explain all parts that
-make a `GenServer`, but here are the relevant bits:
-
- * A `GenServer` is typically started via its `start_link/2` function.
- This function will spawn a new process and then invoke the `init/1`
- function as a callback
-
- * On `init`, we should receive both `frame` and `keyboard` control as
- options. Then, instead of using `Kino.Control.stream/1`, we will use
- `Kino.Control.subscribe/2` to receive the control events as messages
-
- * Each message the process receives will be handled by `handle_info/2`
-
- * To implement the tick, we will configure the process to send itself a
- message every 33 milliseconds
-
-Let's see those concepts in place:
-
-```elixir
-defmodule Game.Server do
- @moduledoc """
- The game server, handles rendering, timing, and interactions.
- """
-
- use GenServer
- @tick_time_ms 33
-
- @doc """
- Starts the server and renders the initial UI.
- """
- def start_link(opts) do
- GenServer.start_link(__MODULE__, opts)
- end
-
- @impl true
- def init(opts) do
- # Subscribe to keyboard events. All events will be
- # wrapped in a tuple with :keyboard as key
- Kino.Control.subscribe(opts[:keyboard], :keyboard)
-
- state = %{
- frame: opts[:frame],
- game: nil,
- players: []
- }
-
- {:ok, render(state)}
- end
-
- @impl true
- def handle_info({:keyboard, event}, state) do
- {:noreply, handle_keyboard(state, event)}
- end
-
- @impl true
- def handle_info(:tick, state) do
- state = update_in(state.game, &Game.State.step/1)
-
- state =
- case state.game.status do
- :idle -> state
- :running -> schedule_tick(state)
- end
-
- {:noreply, render(state)}
- end
-
- # A player enabled the keyboard
- defp handle_keyboard(state, %{type: :status, enabled: true, origin: origin}) do
- case state.players do
- # First player joined
- [] ->
- %{state | players: [%{side: :left, origin: origin}]}
- |> render()
-
- # Second player joined
- [player] ->
- %{state | players: [player, %{side: :right, origin: origin}], game: Game.State.new()}
- |> render()
-
- # Someone else tried to join, ignore them!
- _ ->
- state
- end
- end
-
- # There is a keyboard event but no game yet, ignore it
- defp handle_keyboard(state, _event) when state.game == nil do
- state
- end
-
- # There is a game and it is idle, a keypress from a player will start the game
- defp handle_keyboard(state, %{type: :keydown, origin: origin})
- when state.game.status == :idle do
- if Enum.any?(state.players, &(&1.origin == origin)) do
- update_in(state.game, &Game.State.start/1)
- |> schedule_tick()
- else
- state
- end
- end
-
- # All other keypress go to one of the paddles based on the player
- defp handle_keyboard(state, %{origin: origin} = event) do
- if player = Enum.find(state.players, &(&1.origin == origin)) do
- update_in(state.game, &Game.State.on_key(&1, player.side, event))
- else
- state
- end
- end
-
- # Ignore any event that does not match the clauses above
- defp handle_keyboard(state, _event) do
- state
- end
-
- defp schedule_tick(state) do
- Process.send_after(self(), :tick, @tick_time_ms)
- state
- end
-
- defp render(state) do
- output = paint(state)
- Kino.Frame.render(state.frame, output)
- state
- end
-
- defp paint(%{players: []}) do
- Kino.Markdown.new("Waiting for players. Enable your keyboard to join **left**!")
- end
-
- defp paint(%{players: [_]}) do
- Kino.Markdown.new("Waiting for another player. Enable your keyboard to join **right**!")
- end
-
- defp paint(state) do
- state.game |> Game.State.to_svg() |> Kino.Image.new(:svg)
- end
-end
-```
-
-The server is in place! Let's show the keyboard and the frame,
-then start a `Game.Server` passing them as arguments:
-
-```elixir
-keyboard = Kino.Control.keyboard([:status, :keydown, :keyup]) |> Kino.render()
-frame = Kino.Frame.new() |> Kino.render()
-
-Kino.start_child({Game.Server, keyboard: keyboard, frame: frame})
-Kino.nothing() # Do not print anything after
-```
-
-We are ready to play! To verify it for yourself, copy and paste
-this URL in a separate browser tab and toggle the keyboard mode in
-both tabs. It may be challenging to act as two players at the same
-time, but let's say it's a part of the game! Once you are done,
-remember to toggle off the keyboard mode.
-
-If you want one additional challenge, you will notice that, once you
-disable the keyboard, the game continues running. Could you change the
-code above to stop the game once any player toggles the keyboard off?
-
-In any case, this was a fun and long ride! The next guide should be
-much shorter and we will learn [how to create our own kinos](/learn/notebooks/custom-kinos)
-using Elixir and JavaScript.
diff --git a/lib/livebook/notebook/learn/kino/vm_introspection.livemd b/lib/livebook/notebook/learn/kino/vm_introspection.livemd
index e084604fa..097b23735 100644
--- a/lib/livebook/notebook/learn/kino/vm_introspection.livemd
+++ b/lib/livebook/notebook/learn/kino/vm_introspection.livemd
@@ -139,7 +139,7 @@ tree:
supervisor
```
-We can go even further!
+And we can go even further!
Sometimes a supervisor may be supervised by another supervisor,
which may have be supervised by another supervisor, and so on.
@@ -363,5 +363,10 @@ for i <- 1..1_000_000 do
end
```
-In the next notebook, we will learn [how to use `Kino.Control`
-to build a chat app](/learn/notebooks/chat-app)!
+In this notebook, we learned how powerful Kino and Livebook
+are together. With them, we can augment existing Elixir
+constructs, such as supervisors, with rich information, but
+also to create new visualizations such as VegaLite charts.
+
+In the next notebook, we will learn [how to create our custom
+Kinos with Elixir and JavaScript](/learn/notebooks/custom-kinos)!
diff --git a/lib/livebook_web/live/learn_live.ex b/lib/livebook_web/live/learn_live.ex
index 8b0975a54..bf9d88c30 100644
--- a/lib/livebook_web/live/learn_live.ex
+++ b/lib/livebook_web/live/learn_live.ex
@@ -86,7 +86,7 @@ defmodule LivebookWeb.LearnLive do