Creating a MarioButton with AnyWidgetComponent#

In this tutorial we will build a Mario style button with sounds and animations using the AnyWidgetComponent feature in Panel. It aims to help you learn how to push the boundaries of what can be achieved with HoloViz Panel by creating advanced components using modern JavaScript and CSS technologies.

Mario chime button

This tutorial draws heavily on the great ipymario video and tutorial by Trevor Manz.

Overview#

We’ll build a MarioButton that displays a pixelated Mario icon and plays a chime sound when clicked. The button will also have customizable parameters for gain, duration, size, and animation, showcasing the powerful capabilities of AnyWidgetComponent.

Prerequisites#

Ensure you have HoloViz Panel installed:

pip install panel watchfiles

Step 1: Define the MarioButton Component#

We’ll start by defining the Python class for the MarioButton component, including its parameters and rendering logic.

Create a file named mario_button.py:

import numpy as np
import param
from panel.custom import AnyWidgetComponent
import panel as pn

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)
np_box_as_list = [[[int(z) for z in y] for y in x] for x in np_box.tolist()]

class MarioButton(AnyWidgetComponent):

    _esm = "mario_button.js"
    _stylesheets = ["mario_button.css"]

    _box = param.List(np_box_as_list)
    gain = param.Number(0.1, bounds=(0.1, 1.0), step=0.1)
    duration = param.Number(1.0, bounds=(0.5, 2), step=0.5,)
    size = param.Integer(100, bounds=(10, 1000), step=10)
    animate = param.Boolean(True)

    margin = param.Integer(10)

if pn.state.served:
    button = MarioButton()
    parameters = pn.Param(
        button, parameters=["gain", "duration", "size", "animate"]
    )
    settings=pn.Column(parameters, "Credits: Trevor Manz")
    pn.FlexBox(settings, button).servable()

Explanation - Python#

  • _esm: Specifies the path to the JavaScript file for the component.

  • _stylesheets: Specifies the path to the CSS file for styling the component.

  • _box: A parameter representing the pixel data for the Mario icon.

  • gain, duration, size, animate: Parameters for customizing the button’s behavior.

  • pn.Param: Creates a Panel widget to control the parameters.

Step 2: Define the JavaScript for the MarioButton#

Create a file named mario_button.js:

/**
 * Plays a Mario chime sound with the specified gain and duration.
 * @see {@link https://twitter.com/mbostock/status/1765222176641437859}
 */
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);
}

function createCanvas(model) {
  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();
  return canvas;
}

function drawImageData(canvas, pixelData) {
  const flattenedData = pixelData.flat(2);
  const imageDataArray = new Uint8ClampedArray(flattenedData);
  const imgData = new ImageData(imageDataArray, 16, 16);

  let ctx = canvas.getContext("2d");
  ctx.imageSmoothingEnabled = false;
  ctx.putImageData(imgData, 0, 0);
}

function addClickListener(canvas, model) {
  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);
    }
  });
}

function addResizeWatcher(canvas, model) {
  model.on('change:size', () => {
    let size = () => `${model.get('size')}px`;
    canvas.style.width = size();
    canvas.style.height = size();
  });
}

function render({ model, el }) {
  let canvas = createCanvas(model);
  drawImageData(canvas, model.get('_box'));
  addClickListener(canvas, model);
  addResizeWatcher(canvas, model);

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

Explanation - JavaScript#

  • chime: A function that generates the Mario chime sound using the Web Audio API.

  • render: The main function that renders the button, sets up the canvas, handles click events, and manages parameter changes.

Step 3: Define the CSS for the MarioButton#

Create a file named mario_button.css:

.ipymario > canvas {
    animation-fill-mode: both;
    image-rendering: pixelated; /* Ensures the image stays pixelated */
    image-rendering: crisp-edges; /* For additional support in some browsers */
}

@keyframes ipymario-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-12px); }
}

Explanation - CSS#

  • .ipymario > canvas: Styles the canvas to ensure the Mario icon remains pixelated.

  • @keyframes ipymario-bounce: Defines the bounce animation for the button when clicked.

Step 4: Serve the Application#

To serve the application, run the following command in your terminal:

panel serve mario_button.py --autoreload

This command will start a Panel server and automatically reload changes as you edit the files.

The result should look like this:

You’ll have to turn on the sound to hear the chime.

Step 4: Develop the Application with Autoreload#

When you save your .py, .js or .css file, the Panel server will automatically reload the changes. This feature is called auto reload or hot reload.

Try changing "ipymario-bounce 0.2s" in the mario_button.js file to "ipymario-bounce 2s" and save the file. The Panel server will automatically reload the changes.

Try clicking the button to see the button bounce more slowly.

Conclusion#

You’ve now created a custom MarioButton component using AnyWidgetComponent in HoloViz Panel. This button features a pixelated Mario icon, plays a chime sound when clicked, and has customizable parameters for gain, duration, size, and animation.

References#

Tutorials#

How-To Guides#

Reference Guides#