Py.Cafe

kolibril13/

ipymario

Create a "Hi" Widget with anywidget

DocsPricing
  • app.py
  • requirements.txt
  • widget.css
  • widget.js
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import importlib.metadata
import pathlib

import anywidget
import numpy as np
import traitlets

try:
    __version__ = importlib.metadata.version("ipymario")
except importlib.metadata.PackageNotFoundError:
    __version__ = "unknown"

colors = {
    "O": [0, 0, 0, 255],
    "X": [247, 82, 0, 255],
    " ": [247, 186, 119, 255],
}

# fmt: off
box = [
    ['O', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'O'],
    ['X', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'O'],
    ['X', ' ', 'O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'O', ' ', 'O'],
    ['X', ' ', ' ', ' ', ' ', 'X', 'X', 'X', 'X', 'X', ' ', ' ', ' ', ' ', ' ', 'O'],
    ['X', ' ', ' ', ' ', 'X', 'X', 'O', 'O', 'O', 'X', 'X', ' ', ' ', ' ', ' ', 'O'],
    ['X', ' ', ' ', ' ', 'X', 'X', 'O', ' ', ' ', 'X', 'X', 'O', ' ', ' ', ' ', 'O'],
    ['X', ' ', ' ', ' ', 'X', 'X', 'O', ' ', ' ', 'X', 'X', 'O', ' ', ' ', ' ', 'O'],
    ['X', ' ', ' ', ' ', ' ', 'O', 'O', ' ', 'X', 'X', 'X', 'O', ' ', ' ', ' ', 'O'],
    ['X', ' ', ' ', ' ', ' ', ' ', ' ', 'X', 'X', 'O', 'O', 'O', ' ', ' ', ' ', 'O'],
    ['X', ' ', ' ', ' ', ' ', ' ', ' ', 'X', 'X', 'O', ' ', ' ', ' ', ' ', ' ', 'O'],
    ['X', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'O', 'O', ' ', ' ', ' ', ' ', ' ', 'O'],
    ['X', ' ', ' ', ' ', ' ', ' ', ' ', 'X', 'X', ' ', ' ', ' ', ' ', ' ', ' ', 'O'],
    ['X', ' ', ' ', ' ', ' ', ' ', ' ', 'X', 'X', 'O', ' ', ' ', ' ', ' ', ' ', 'O'],
    ['X', ' ', 'O', ' ', ' ', ' ', ' ', ' ', 'O', 'O', ' ', ' ', ' ', 'O', ' ', 'O'],
    ['X', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'O'],
    ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'],
]

# fmt: on
np_box = np.array([[colors[c] for c in row] for row in box], dtype=np.uint8)

class Widget(anywidget.AnyWidget):
    _esm = "widget.js"
    _css = "widget.css"
    _box = traitlets.Bytes(np_box.tobytes()).tag(sync=True)
    gain = traitlets.Float(0.1).tag(sync=True)
    duration = traitlets.Float(1.0).tag(sync=True)
    size = traitlets.Int(300).tag(sync=True)
    animate = traitlets.Bool(True).tag(sync=True)

    def click(self):
        self.send({"type": "click"})

page = Widget()
widget.css
1
2
3
4
5
6
7
8
9
10
11
.ipymario > canvas {
    padding: 12px;
	top: 100px;
    animation-fill-mode: both;
    image-rendering: pixelated; /* Ensures the image stays pixelated */
}

@keyframes ipymario-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-30px); }
}
widget.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
 * Makes a Mario chime sound using web `AudioContext` API.
 *
 * @see {@link https://twitter.com/mbostock/status/1765222176641437859}
 *
 * @param {{ gain: number, duration: number }} options
 */
function chime({ gain, duration }) {
  let c = new AudioContext();
  let g = c.createGain();
  let o = c.createOscillator();
  let of = o.frequency;
  g.connect(c.destination);
  g.gain.value = gain;
  g.gain.linearRampToValueAtTime(0, duration);
  o.connect(g);
  o.type = "square";
  of.setValueAtTime(988, 0);
  of.setValueAtTime(1319, 0.08);
  o.start();
  o.stop(duration);
}

/**
 * @typedef Model
 * @prop {DataView} _box
 * @prop {number} size
 * @prop {number} gain
 * @prop {number} duration
 * @prop {boolean} animate
 */

/** @type {import("npm:@anywidget/types").Render<Model>} */
function render({ model, el }) {
  let size = () => `${model.get("size")}px`;
  let canvas = document.createElement("canvas");
  canvas.width = 16;
  canvas.height = 16;
  canvas.style.width = size();
  canvas.style.height = size();
  let bytes = new Uint8ClampedArray(
    model.get("_box").buffer,
  );
  let imgData = new ImageData(bytes, 16, 16);
  let ctx = canvas.getContext("2d");
  ctx.putImageData(imgData, 0, 0);

  canvas.addEventListener("click", () => {
    chime({
      gain: model.get("gain"),
      duration: model.get("duration"),
    });
    if (model.get("animate")) {
      canvas.style.animation = "none";
      setTimeout(() => {
        canvas.style.animation = "ipymario-bounce 0.2s";
      }, 10);
    }
  });
  model.on("msg:custom", (msg) => {
    if (msg?.type === "click") canvas.click();
  });
  model.on("change:size", () => {
    canvas.style.width = "30px";
    canvas.style.height = "30px";
  });

  el.classList.add("ipymario");
  el.appendChild(canvas);
}
export default { render };