P5.js example

Vow! p5.js works with the awesome_panel_extensions.web_component!
I grabbed the source for https://thecodingtrain.com/CodingChallenges/125-fourier-series.html

and tried it: my very first attempt, and it worked. Don’t know who all to applaud, but Kudos!!!

import param
import panel as pn
pn.extension()
from awesome_panel_extensions.web_component import WebComponent
js_urls = {
    "p5": "https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"
}
pn.extension(
    js_files=js_urls
)
p5_html = """
<div id="sketch2" style="float:left;height:10cm;width:10cm;padding-left:5mm;">
    <h1>Sketch2</h1>
</div>
<script type="text/javascript">
target = document.currentScript.parentElement.children[0];
    
const sketch2 = (p55) => {
// Fourier Series
// Daniel Shiffman
// https://thecodingtrain.com/CodingChallenges/125-fourier-series.html
// https://youtu.be/Mm2eYfj0SgA
// https://editor.p5js.org/codingtrain/sketches/SJ02W1OgV

let time = 0;
let wave = [];

let n = 5;

p55.setup = () => {
    p55.createCanvas(600, 400);
    console.log("Created Canvas")
};

p55.draw = () => {
  p55.background(0);
  p55.translate(150, 200);

  console.log("DRAW")
  let x = 0;
  let y = 0;

  for (let i = 0; i < n; i++) {
    let prevx = x;
    let prevy = y;

    let n = i * 2 + 1;
    let radius = 75 * (4 / (n * Math.PI));
    x += radius * Math.cos(n * time);
    y += radius * Math.sin(n * time);

    p55.stroke(255, 100);
    p55.noFill();
    p55.ellipse(prevx, prevy, radius * 2, radius * 2);

    //fill(255);
    p55.stroke(255);
    p55.line(prevx, prevy, x, y);
    //ellipse(x, y, 8);
  }

  wave.unshift(y);

  p55.translate(200, 0);
  p55.line(x - 200, y, 0, wave[0]);
  p55.beginShape();
  p55.noFill();
  for (let i = 0; i < wave.length; i++) {
    p55.vertex(i, wave[i] );
  }
  p55.endShape();

  time += 0.05;

  if (wave.length > 250) {
    wave.pop();
  }
};
};

let myp5_sketch2 = new p5(sketch2, "sketch2");
</script>
"""

class P5Sketch(WebComponent):
    html                = param.String(p5_html)
    #properties_to_watch = param.Dict({"lines": "lines"})
    
    #lines = param.Integer(default=13, bounds=(1,20))
    #height= param.Integer(default=100)

    
p5_sketch = P5Sketch(width=350, height=350)
pn.Column(
    p5_sketch,
    sizing_mode="stretch_height",
)
2 Likes

Hi @ea42gh

Thats a great example. Thanks for sharing.

Please note that as long as the communication is from Python to HTML only (unidirectional) you can use the panel.panel.HTML. The WebComponent (or something similar) is needed if you want to have bidirectional communication. I.e. get a mouse position or some other value from the browser to the server.

import param
import panel as pn
pn.extension()
from awesome_panel_extensions.web_component import WebComponent
js_urls = {
    "p5": "https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"
}
pn.extension(
    js_files=js_urls
)
p5_html = """
<div id="sketch2" style="float:left;height:10cm;width:10cm;padding-left:5mm;">
    <h1>Sketch2</h1>
</div>
<script type="text/javascript">
target = document.currentScript.parentElement.children[0];

const sketch2 = (p55) => {
// Fourier Series
// Daniel Shiffman
// https://thecodingtrain.com/CodingChallenges/125-fourier-series.html
// https://youtu.be/Mm2eYfj0SgA
// https://editor.p5js.org/codingtrain/sketches/SJ02W1OgV

let time = 0;
let wave = [];

let n = 5;

p55.setup = () => {
    p55.createCanvas(600, 400);
    console.log("Created Canvas")
};

p55.draw = () => {
  p55.background(0);
  p55.translate(150, 200);

  console.log("DRAW")
  let x = 0;
  let y = 0;

  for (let i = 0; i < n; i++) {
    let prevx = x;
    let prevy = y;

    let n = i * 2 + 1;
    let radius = 75 * (4 / (n * Math.PI));
    x += radius * Math.cos(n * time);
    y += radius * Math.sin(n * time);

    p55.stroke(255, 100);
    p55.noFill();
    p55.ellipse(prevx, prevy, radius * 2, radius * 2);

    //fill(255);
    p55.stroke(255);
    p55.line(prevx, prevy, x, y);
    //ellipse(x, y, 8);
  }

  wave.unshift(y);

  p55.translate(200, 0);
  p55.line(x - 200, y, 0, wave[0]);
  p55.beginShape();
  p55.noFill();
  for (let i = 0; i < wave.length; i++) {
    p55.vertex(i, wave[i] );
  }
  p55.endShape();

  time += 0.05;

  if (wave.length > 250) {
    wave.pop();
  }
};
};

let myp5_sketch2 = new p5(sketch2, "sketch2");
</script>
"""

p5_sketch = pn.pane.HTML(p5_html, width=350, height=350)
pn.Column(
    p5_sketch,
    sizing_mode="stretch_height",
).servable()
2 Likes

Hi @ea42gh and others

I have also been experimenting a bit in this area and have some unfinished code that would improve the possibility of easily integrating with p5.js and other libraries above.

Use of p5.js and other .js libraries directly from Python and Panel

Inspired by pyp5js I have been working on being able to write p5 examples in Python and have them transpiled by Transcrypt and inserted directly in Panel.

This should solve the pain of having to use javascript and actually also bring p5.js and other .js libraries to Jupyter Notebooks.

A better WebComponent

Panel needs something like the WebComponent that can make it more easy to get bidirectional communication from server to browser on an ad hoc basis in order to enable easier use of the large world of .js libraries.

The WebComponent is the first attempt. But I’m working on an improved api and something called the DataModel. This is close to being ready.

I also know there are some thoughts and hopes of bringing something like that to directly into Bokeh and Panel.

Please share your examples.

If some of you play around with p5.js I would really like to see the examples and help improve them. So please share. Thanks.

1 Like

Hi @Marc,
I am eager to try whatever you come up with!

The example above was my first attempt at using awesome panel extensions:
I spent some time searching through the .org site, until it finally dawned on me
to look at the code instead…

Where I wanted to go is to figure out the bidirectional communications:

  • start with a slider to control the number of terms in the Fourier expansion
  • a start/stop button
  • modify the output by passing in coefficients of a series expansion

I am currently struggling with the mwc components. It’s all too new:
I’ll end up with a notebook explaining the steps to get this put together.

A question: do you know how to get Jupyterlab to honor the output size?
For markdown, I usually generate a div with a height setting, but here this does not seem to work: only part of the plot is shown.

1 Like

Hi @ea42gh

I played a bit around with inspired by How to get a JavaScript event to trigger a Python event.

You can now

  • Change the number of terms with the slider
  • Get the mouse position back and information on mouse clicks.
  • Stop the loop when mouse down on the canvas.

I could not get the loop stopped via the panel checkbox. I’m sure there is a way. @xavArtley. Do you know how to catch an event from the checkbox on the js side? Thanks in advance.

from panel.util import value_as_date
import param
import panel as pn
pn.extension()
from awesome_panel_extensions.web_component import WebComponent
js_urls = {
    "p5": "https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"
}
pn.extension(
    js_files=js_urls
)

number_of_terms_slider = pn.widgets.IntSlider(value=5, start=1, end=20,step=1)
start_stop_checkbox = pn.widgets.Checkbox(name="Enabled", value=True)
mouse_x_input=pn.widgets.FloatInput(name="Mouse X")
mouse_y_input=pn.widgets.FloatInput(name="Mouse Y")
mouse_clicks_input = pn.widgets.IntInput(name="Clicks")
args = {
    "number_of_terms": number_of_terms_slider,
    "start_stop": start_stop_checkbox,
    "mouse_x": mouse_x_input,
    "mouse_y": mouse_y_input,
    "mouse_clicks": mouse_clicks_input,
}

p5_html = """
<div id="sketch2" style="float:left;height:10cm;width:10cm;padding-left:5mm;">
    <h1>Sketch2</h1>
</div>"""
# comm to run the js script
comm_py_to_js = pn.widgets.StaticText(style={'visibility': 'hidden', 'width': 0, 'height': 0, 'overflow': 'hidden'}, margin=0)
# comm to return informations from javascript to python
comm_js_to_py = pn.widgets.StaticText(style={'visibility': 'hidden', 'width': 0, 'height': 0, 'overflow': 'hidden'}, margin=0)

code = """
console.log("args");
console.log(start_stop);
console.log(start_stop.value);
console.log(number_of_terms);
console.log(mouse_x);
console.log(mouse_y);
console.log(mouse_clicks);

console.log("target")
const target = document.getElementById("sketch2");
console.log(target);


const sketch2 = (p55) => {
// Fourier Series
// Daniel Shiffman
// https://thecodingtrain.com/CodingChallenges/125-fourier-series.html
// https://youtu.be/Mm2eYfj0SgA
// https://editor.p5js.org/codingtrain/sketches/SJ02W1OgV

let time = 0;
let wave = [];

let n = number_of_terms.value;

p55.setup = () => {
    p55.createCanvas(600, 400);
    console.log("Created Canvas")
};

p55.mousePressed = () => {
  console.log("pressed");
  p55.noLoop();
};

p55.mouseReleased = () => {
  p55.loop();
};

p55.mouseClicked = () => {
    mouse_clicks.value += 1;
}

p55.draw = () => {
  p55.background(0);
  p55.translate(150, 200);
  mouse_x.value = p55.mouseX;
  mouse_y.value = p55.mouseY;

  console.log("DRAW")
  let x = 0;
  let y = 0;
  n = number_of_terms.value;

  for (let i = 0; i < n; i++) {
    let prevx = x;
    let prevy = y;

    let n = i * 2 + 1;
    let radius = 75 * (4 / (n * Math.PI));
    x += radius * Math.cos(n * time);
    y += radius * Math.sin(n * time);

    p55.stroke(255, 100);
    p55.noFill();
    p55.ellipse(prevx, prevy, radius * 2, radius * 2);

    //fill(255);
    p55.stroke(255);
    p55.line(prevx, prevy, x, y);
    //ellipse(x, y, 8);
  }

  wave.unshift(y);

  p55.translate(200, 0);
  p55.line(x - 200, y, 0, wave[0]);
  p55.beginShape();
  p55.noFill();
  for (let i = 0; i < wave.length; i++) {
    p55.vertex(i, wave[i] );
  }
  p55.endShape();

  time += 0.05;

  if (wave.length > 250) {
    wave.pop();
  }
};
};
let myp5_sketch2 = new p5(sketch2, "sketch2");
"""

p5_sketch = pn.pane.HTML(p5_html, width=350, height=350)
comm_py_to_js.jscallback(args=args, value=code)
def _execute_js_code(*_):
    comm_py_to_js.param.trigger("value")
pn.state.onload(_execute_js_code)
pn.Column(
    start_stop_checkbox,
    number_of_terms_slider,
    mouse_x_input,
    mouse_y_input,
    mouse_clicks_input,
    p5_sketch,
    comm_py_to_js,
    sizing_mode="stretch_height",
).servable()

Hi @Marc,
this looked really promising!
Can’t seem to get it working in Jupyterlab, I tried all morning!

Decided to create a script file and load it. Could not get the script loaded,
so I read in the file and passed the contents as part of the html.
The globals also give me a headache: I tried passing them in as parameters,
only to get errors…

Is there an easy way to share a notebook, other than copy and paste here?

This is what I have now:

import param
import panel as pn
pn.extension()

from awesome_panel_extensions.web_component import WebComponent
# ==============================================================================================================
js_urls = {
    "p5":           "https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js",
    #"jquery":       "https://code.jquery.com/jquery-3.4.1.min.js"
}
pn.extension(
    js_files=js_urls
)

js_module_urls_str = "".join([f"<script type='module' src='{value}'></script>" for value in js_urls.values()])
extension_pane = pn.pane.HTML(js_module_urls_str,\
                              #+css_urls_str,
                              width=0, height=0, sizing_mode="fixed", margin=0)
extension_pane
number_of_terms_slider = pn.widgets.IntSlider(value=5, start=1, end=20,step=1)
start_stop_checkbox    = pn.widgets.Checkbox(name="Enabled", value=True)
mouse_x_input          = pn.widgets.FloatInput(name="Mouse X")
mouse_y_input          = pn.widgets.FloatInput(name="Mouse Y")
mouse_clicks_input     = pn.widgets.IntInput(name="Clicks")
args = {
    "number_of_terms": number_of_terms_slider,
    "start_stop":      start_stop_checkbox,
    "mouse_x":         mouse_x_input,
    "mouse_y":         mouse_y_input,
    "mouse_clicks":    mouse_clicks_input,
}

div_name = "p5_ts_sketch"

p5_html = f"""
<!--            ****************************************************   this does not really work.... 
<script>
const inlineScript = document.createElement('script')
script.type='text/javascript';
script.src = 'p5_ts_sketch.js'
//script.innerHTML = 'alert("Inline script loaded!")'
document.head.append(script)
</script>
************************************************************************   -->
<div id="{div_name}" style="float:left;padding-left:5mm;border:2px solid black; height:10cm;width:100%;">
    <h1>Step Approximation</h1>
</div>
"""
# comm to run the js script
comm_py_to_js = pn.widgets.StaticText(style={'visibility': 'hidden', 'width': 0, 'height': 0, 'overflow': 'hidden'}, margin=0)
# comm to return informations from javascript to python
comm_js_to_py = pn.widgets.StaticText(style={'visibility': 'hidden', 'width': 0, 'height': 0, 'overflow': 'hidden'}, margin=0)

with open('p5_ts_sketch.js') as f:
    p5_ts_sketch_script = f.read()

code = f"""
{p5_ts_sketch_script}
console.log("target")
const target = document.getElementById("{div_name}");
console.log(target);

let myp5_sketch = new p5( p5_ts_sketch, "{div_name}", number_of_terms_slider );
console.log( "created "+ myp5_sketch)
console.log("args");
console.log(start_stop);
console.log(start_stop.value);
console.log(number_of_terms);
console.log(mouse_x);
console.log(mouse_y);
console.log(mouse_clicks);
"""

p5_sketch = pn.pane.HTML(p5_html, width=650, height=550)

comm_py_to_js.jscallback(args=args, value=code)
def _execute_js_code(*_):
    comm_py_to_js.param.trigger("value")
pn.state.onload(_execute_js_code)
pn.Column(
    start_stop_checkbox,
    number_of_terms_slider,
    mouse_x_input,
    mouse_y_input,
    mouse_clicks_input,
    p5_sketch,
    comm_py_to_js,
    sizing_mode="stretch_height",
).servable()

with the java script file p5_ts_sketch.js

const p5_ts_sketch = (p55, number_of_terms, mouse_x, mouse_y, mouse_clicks ) => {
// Fourier Series
// Daniel Shiffman
// https://thecodingtrain.com/CodingChallenges/125-fourier-series.html
// https://youtu.be/Mm2eYfj0SgA
// https://editor.p5js.org/codingtrain/sketches/SJ02W1OgV
// minor mods to work with pyviz: https://discourse.holoviz.org/t/p5-js-example/1551/5

let n_terms       = number_of_terms;  // can't get these to work in any variation....
let mouse_x_      = mouse_x;
let mouse_y_      = mouse_y;
let mouse_clicks_ = mouse_clicks;

let time    = 0;
let wave    = [];
let x_vals  = [];
let looping = true;

p55.setup = () => {
    let canvas = p55.createCanvas(600, 400);
    looping = true;
    console.log("Created Canvas" + canvas)
};
p55.draw = () => {
  p55.background(0);
  p55.translate(150, 200);
  //mouse_x_.value = p55.mouseX;
  //mouse_y_.value = p55.mouseY;

  let x = 0;
  let y = 0;
  let n = 5; //number_of_terms_.value;

  // draw the vectors and circles
  for (let i = 0; i < n; i++) {
    let prevx = x;
    let prevy = y;

    let n = i * 2 + 1;
    let radius = 75 * (4 / (n * Math.PI));
    x += radius * Math.cos(n * time);
    y += radius * Math.sin(n * time);

    p55.stroke(255, 100);
    p55.noFill();
    p55.ellipse(prevx, prevy, radius * 2, radius * 2);

    p55.stroke(255);
    p55.line(prevx, prevy, x, y);
  }
  x_vals.unshift(x);
  wave.unshift(y);

  // draw the path being traced out
  p55.beginShape();
  p55.noFill();
  for (let i = 0; i < wave.length; i++) {
    p55.vertex(x_vals[i], wave[i] );
  }
  p55.endShape();

  // draw the generated function
  p55.translate(200, 0);
  p55.line(x - 200, y, 0, wave[0]);

  p55.beginShape();
  p55.noFill();
  for (let i = 0; i < wave.length; i++) {
    p55.vertex(i, wave[i] );
  }
  p55.endShape();

  if (wave.length > 250) {
    wave.pop();
    x_vals.pop()
  }

  time += 0.05;

};
// --------------------------------------------------------------------------------------------
p55.checkLoop = () => {
  if (this.checked()) {
    p55.loop();
  } else {
    p55.noLoop();
  }
};
// --------------------------------------------------------------------------------------------
p55.mousePressed = () => {
  p55.noLoop();
};
// --------------------------------------------------------------------------------------------
p55.mouseReleased = () => {
  p55.loop();
};
// --------------------------------------------------------------------------------------------
p55.mouseClicked = () => {
  //mouse_clicks_.value += 1;
  if (looping ) {
    p55.noLoop();
    looping = false;
  } else {
    p55.loop();
    looping = true;
  }
};
// --------------------------------------------------------------------------------------------
p55.keyPressed = () => {
    console.log( mouse_x_.value );
    mouse_x_.value += 1;
};
// --------------------------------------------------------------------------------------------
};

Just to be sure, I chose to test it with a stand alone html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>P5 Time Series</title>
  </head>
  <body>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>
    <script language="javascript" type="text/javascript" src="./p5_ts_sketch.js"></script>
    <div id="p5_ts_sketch_1"></div>
    <script>
        let number_of_terms_slider = {}; number_of_terms_slider.value = 5;
        let mouse_x                = {}; mouse_x               .value = 5;
        let mouse_y                = {}; mouse_y               .value = 5;
        let mouse_clicks           = {}; mouse_clicks          .value = 5;

        let myp5_sketch = new p5( p5_ts_sketch, "p5_ts_sketch_1", number_of_terms_slider, mouse_x, mouse_y, mouse_clicks );
    </script>
  </body>
</html>

Figured out some of it…
I created a repository on gitlab
https://gitlab.com/ea42gh/awesomepanelplayground.git
should anybody be interested…

1 Like

MarcsAwesomeP5.ipynb in the gitlab repository does work when served, e.g., using
panel serve --port 8000 MarcsAwesomeP5.ipynb

@Marc : interestingly, the checkbox does start/stop the code for me!
take it back:its an artifact to my having tied mouse clicks to start/stop, sorry.

1 Like

Hi @Marc

If I understand you right, you have a way of passing data to the javascript code?
If I compute the DFT coefficients in python, how would I pass them in?