You can go over to https://alejandrogallo.github.io/blog/posts/ob-p5js/ to read a blog post about this project.
I wanted to do some tests with the cool project {{{p5js}}} and I wanted to use it from {{{org-mode}}}.
Regrettably, there is no package I could find that allows
to use {{{p5js}}} in an {{{org-mode}}} document.
What’s worse however is that apparently you can’t have two sketches in a
single html
file. Of course, to be fair, maybe this is my assessment
of the library. As far as I can tell {{{p5js}}} can only deal with a
single
<main></main>
tag. So I decided to let the good-old <iframe></iframe>
save the day.
But of course, I would like to simply write code and let org-mode
handle the output automatically, i.e., I want to write
#+begin_src p5js
function setup() {
// ...
}
function draw() {
// ...
}
#+end_src
sit back and enjoy my interactive html
document.
So you have already seen the eye candy at the beginning of the document,
and you can do everything that {{{p5js}}} can, which is
trivially use WEBGL
function setup() {
createCanvas(500, 200, WEBGL);
}
function draw() {
background(255);
push();
ambientLight(mouseX);
normalMaterial();
rotateZ(frameCount * 0.01);
rotateX(frameCount * 0.01);
rotateY(frameCount * 0.01);
box(70, 70, 70);
pop();
}
where you can just write
#+begin_src p5js :height 200 :center t
function setup() {
createCanvas(500, 200, WEBGL);
}
function draw() {
background(255);
push();
normalMaterial();
rotateZ(frameCount * 0.01);
rotateX(frameCount * 0.01);
rotateY(frameCount * 0.01);
box(70, 70, 70);
pop();
}
#+end_src
export to html
and get that nice rotating cube.
If you prefer storing the result in a self-contained html-file, you can simply do
#+begin_src p5js :height 200 :center t :file ./cube.html :results file raw replace value
function setup() {
createCanvas(500, 200, WEBGL);
}
function draw() {
background(255);
push();
normalMaterial();
rotateZ(frameCount * 0.01);
rotateX(frameCount * 0.01);
rotateY(frameCount * 0.01);
box(70, 70, 70);
pop();
}
#+end_src
#+RESULTS:
[[file:./cube.html]]
Below follows the implementation of the package in a literate programming fashion. If you are interested to see how easy it is to implement these kinds of packages just read on.
For the moment the development of this package happens over at https://github.com/alejandrogallo/ob-p5js.
As of [2022-10-14 Fri] I am going to submit the package to melpa
,
otherwise you can just simply copy the ob-p5js.el
file
where you can require it.
;;; ob-p5js.el --- Support for p5js in org-babel
;; SPDX-License-Identifier: MIT
;; Author: Alejandro Gallo <aamsgallo@gmail.com>
;; Version: 2.0
;; Package-Requires: ((emacs "25.1"))
;; Keywords: javascript, graphics, multimedia, p5js, processing, org-babel
;; URL: https://github.com/alejandrogallo/p5js
;;; Commentary:
;; This package provides a minor mode for p5js
;; and a way to export javascript code to an iframe
;; containing a p5js ready environment to export to html.
All the options for this package are reachable through the group ob-p5js
(defgroup ob-p5js nil
"Options for org-babel p5.js package."
:prefix "ob-p5js-"
:group 'org-babel)
The defaults for every src block are given by
(defvar org-babel-default-header-args:p5js
'((:exports . "results")
(:results . "verbatim html replace value")
(:eval . "t")
(:width . "100%")))
where the most notable one is the :results
,
in that it creates an html
export block.
The custom block arguments are the width
and height
for the iframe
where the {{{p5js}}} is embedded in,
and also a center
boolean field in order to insert
both the iframe
and the main
tag inside an html
center
element.
(defconst org-babel-header-args:p5js
'((width . :any)
(height . :any)
(center . :any)
(file . :any))
"Header arguments specific to p5js.")
We also need to set and define the mode that should be used
for the src-blocks, in this case probably one would like to
use the js
mode, but in the future one might want
to use a dedicated p5js
mode, so we can make it configurable
(defcustom ob-p5js-mode
'js
"The major mode that should be used in the src blocks."
:type '(symbol :tag "Mode name")
:group 'ob-p5js)
(add-to-list 'org-src-lang-modes `("p5js" . ,ob-p5js-mode))
We need to include the script in the iframe
environment,
and you can customize where you want to get your p5js
from. By default it points to the default one from the website
(defcustom ob-p5js-src "https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/p5.js"
"The source of p5js."
:type 'string
:group 'ob-p5js)
and I also give every iframe
the class org-p5js
by default,
so that you can customize it via css
or js
.
(defcustom ob-p5js-iframe-class "org-p5js"
"Default class for iframes containing a p5js sketch."
:type 'string
:group 'ob-p5js)
The body of the input for the iframe
is a minimal
html
document containing the src script for {{{p5js}}}
and yours:
(defun ob-p5js--create-sketch-body (params body)
"Create the main body for the iframe content.
PARAMS contains the parameters of the src block.
BODY contains the sketch."
(format "
<html>
<head>
<script src=%S></script>
<script>
%s
</script>
</head>
<body>
%s
</body>
</html>
" ob-p5js-src body (ob-p5js--maybe-center params "<main></main>")))
(defun ob-p5js--maybe-center (params body)
"Center the content whenever params wants it.
PARAMS contains the parameters of the src block.
BODY contains the sketch."
(if (alist-get :center params)
(format "<center>%s</center>" body)
body))
Now an important aspect arises, how do we embed the
html
document containing the sketch into the iframe
.
From all my testing I found that including the whole script
as a base64 encoding hunk works best, so this is the approach I took
(defun ob-p5js--create-iframe (params body &optional width height)
"Create iframe by encoding base64 the sketch in body.
PARAMS contains the parameters of the src block.
BODY contains the sketch.
WIDTH is a string containing an html-valid width.
HEIGHT is a string containing an html-valid height."
(let ((sketch (base64-encode-string (ob-p5js--create-sketch-body params
body)
t)))
(ob-p5js--maybe-center params
(format "<iframe class=\"%s\"
frameBorder='0'
%s
src=\"data:text/html;base64,%s\">
</iframe>"
ob-p5js-iframe-class
(concat (if width
(format "width=\"%s\" " width)
"")
(if height
(format "height=\"%s\" " height)
""))
sketch))))
Last but not least, comes the part that tells org-babel
how to execute p5js
blocks, which entails simply defining
a function prefixed by orb-babel-execute
with the name of the
src block.
(defun org-babel-execute:p5js (body params)
"Execute a p5js src block.
PARAMS contains the parameters of the src block.
BODY contains the sketch."
(let ((width (alist-get :width params))
(height (alist-get :height params))
(file (alist-get :file params)))
(if file
(let ((html-body (ob-p5js--create-sketch-body params
body)))
(setf (alist-get :result-params params)
(list "file" "raw" "replace" "value"))
(setf (alist-get :results params)
"file raw replace value")
html-body)
(ob-p5js--create-iframe params body width height))))
And just provide the package:
(provide 'ob-p5js)
;;; ob-p5js.el ends here
And this is pretty much everything there is to it. I hope you have some more motivation to use it in your blog posts and provide interesting content to the community and to you.
For the future I would like to add some autocompletion or documentation checking for the mode, that would make the whole experience a little bit more painless.
- The example sketches are adapted from examples | p5.js.