Skip to content

alejandrogallo/ob-p5js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OB-P5JS

https://melpa.org/packages/ob-p5js-badge.svg

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]]

Installing

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.

Implementation

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

Conclusion

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.

References