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