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()
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); } }
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 };