The <graphics-element> documentation

What if you could just put graphics on your web page, or in your web app, by just writing graphics JavaScript code and then using a <graphics-element src="..."> the same way you'd put regular JavaScript on a page using a <script> tag? As it turns out, that's not a "what if", it's something that the modern web supports, so if that's something you want, or need: maybe the <graphics-element> is for you!

Also, if this made a difference in your dev life, consider (temporarily, even?) becoming a patron of my work over on my Patreon page, or send a one-time donatation to help keep this project, and others like it, funded. Any amount is appreciated!

Table of contents

Using the <graphics-element> tag top

Using the <graphics-element> is similar to writing any other HTML:

<graphics-element attr1="val1" attr2="val2" ...></graphics-element>

With the same universal attributes like id or class as well as the following tag-specific attributes:

Additionally, a <graphics-element> is just a normal HTML tag, and allows child content. This means that if you wish to add guide text, that's as easy as just adding a paragraph:

<graphics-element title="an example" src="..." width="400" height="200">
  <p>This guide text will render inside the graphics-element "box".</p>
</graphics-element>

Using data-* attributes top

As a <graphics-element> is just an HTML element, we can also use data-* attributes to inject value presets:

<graphics-element title="an example" src="..." width="400" height="200" data-radius="1.5">
</graphics-element>

Note that this will set the corresponding variable in your graphics code to the value you specified, with some caveats:

  1. Your graphics code must have a (mutable) variable available to assign.
  2. Preset assignment happens as "the first thing" that happens when your graphics program runs.
  3. The same naming HTML vs. JS naming rules apply: a variable using the standard JS naming convention, e.g. let myPerfectVar;, must use the standard HTML data attribute naming convention on the HTML side, e.g. data-my-perfect-var="...".

And because HTML attributes only support strings, data-* attribute values get automatically converted to "the type they look like they should be":

Load and error events top

Also note that as a <graphics-element> is just an HTML element, we can listener for load and error events using either the legacy onload and onerror event handling properties (and emphatically not as HTML attributes, it's not 1998 anymore: JS event listening is something you add on the JS side, not on the HTML side), or by adding an event listener for the "load" or "error" events.

// wait for the graphics-element tag to be available:
customElements.whenDefined(`graphics-element`).then(() => {
  // Then get our graphics element
  const e = document.querySelector(`graphics-element`);

  // Set up a load handler:
  const loaded = (evt) => console.log(`graphics element loaded`, evt.target);

  // And then either bind that the legacy way:
  e.onload = loaded;

  // Or the (really not even) modern way:
  e.addEventListener(`load`, loaded);

  // Also set up an error handler:
  const failed = (evt) => console.log(`graphics element failed!`, evt.detail);

  // And again, either bind that legacy style:
  e.onerror = failed;

  // Or using normal JS:
  e.addEventListener(`error`, failed);
});

Using fallback images (for when JS is disabled) top

Since the <graphics-element> tag is a custom element, the browser will only load this tag in if JS is enabled. However, on the modern web the baseline assumption should be that your users run both ad-blockers and script blockers, so you'll want to have fallback images in place for when JS is disabled.

This can be done using an <img> tag with the `fallback` class, ideally with the same height and width as your <graphics-element> will take up:

<graphics-element title="an example" src="..." width="400" height="200">
  <img
    class="fallback"
    src="..."
    width="...px"
    height="...px"
    title="Please enable JS to interact with this graphic"
  >
</graphics-element>

In order to make adding placeholders to your pages easier, the <graphics-element> code repository provides a tool that can be used with Node.js to automatically generate fallback images and update the code in an HTML page, automatically making sure the correct width and height values are set, with a placeholder text prefilled.

Writing graphics source code top

Graphics source code uses normal JavaScript, with all graphics API functions available as global functions. While the smallest source code is just an empty file, the recommended minimal code is:

function setup() {
  setSize(123, 456);
}

function draw() {
  clear(`white`);
}

And because modern JS may require awaiting asynchronous returns, both of those can be marked async, too:

async function setup() {
  setSize(123, 456);
  await fetch(`http://example.com/ping`);
}

async function draw() {
  clear(`white`);
  const img = await downloadImage(...);
  image(img);
}

Both of these functions are technically optional, but omitting them doesn't make a lot of sense: by default the <graphics-element> will first run setup, and will then run draw, once. The setSize(width, height) call is optional as long as you've specified the width and height attributes on the <graphics-element> tag itself, but must be present if you decide to omit them (and note that setSize() can be called at any time to resize your graphic).

If you cannot link to a source code URL, you may also inline your code using a <graphics-source> element:

<graphics-element title="an example" width="400" height="200">
  <graphics-source>
    function setup() {
      // ...
    }

    function draw() {
      // ...
    }
  </graphics-source>
</graphics-element>

Or you may inject it using a source code function if you're doing all your work in JS:

const myGraphicsElement = document.createElement(`graphics-element`);

function sourceCode() {
  function setup() {
    setSize(300, 100);
  }

  function draw() {
    clear(`yellow`);
  }
}

// load the code once the custom element loader is done:
customElements.whenDefined(`graphics-element`).then(() => {
  myGraphicsElement.loadFromFunction(sourceCode);
});

Using the graphics-element.d.ts type declarations file top

While not available from CDN, the graphics-element project comes with a graphics-element.d.ts file that you can use to enable type hinting, autocomplete, and docs lookups in your code editor, provided it has a way to add typing support.

As an example, if you use Visual Studio Code, you can quickly and easily add the graphics-element.d.ts to your project, which allows VS Code to show type hints and documentation, as well as perform call autocompletion, by adding a jsconfig.json to your project.

Say you've put all your graphics-element source files in their own directory (which is generally a good idea to keep it from cluttering up other kinds of client-side JS), then you can create a new file called jsconfig.json in the same directory, with the following content:

{
  "files": ["graphics-element.d.ts"],
  "include": ["./*.js"]
}

The "files" property tells VS Code where it can find the .d.ts file, and allows you to put in relative directories (so you don't need to have the .d.ts file in the same directory), and the "include" property tells VS Code which files that .d.ts will apply to (in this case "." means "everything in this directory").

Global variables top

Several global variables exist to make your graphics life easier:

Note that except for pointer and keyboard, these values can be reassigned by your code, with unpredictable results, so be careful what you name your own variables!

Slider-based variables top

You can automatically declare slider-controlled variables, which adds a slider to the on-page graphics element box, and automatically creates a global variable for your code to use. For example, to declare a variable radius that can range from 0 to 1, with slider steps of 0.001 and a starting value of 0.5, you would use:

function setup() {
  addSlider(`radius`, { min: 0, max: 1, step: 0.001, value: 0.5 });
}

After this, the radius variable will be globally available, so that this code will "just work":

function draw() {
  circle(width / 2, height / 2, radius);
}

For convenience, any variable that ends in a number will have that number shown using subscript styling, so a slider for t1 will show "t1" rather than "t1" as its slider label, and any variable with _ in it will subscript the part after the underscore, so that k_p becomes "kp" rather than "kp". Because your code doesn't care, but your users do, and subscripted variables are much nicer to look at.

Adding buttons

Sometimes you want buttons to do things that you could do with click handlers, but would be much nicer with a normal button. In those cases, you can use the addButton function:

let p;

function setup() {
  addButton(`play`, (btn) => {
    btn.textContent = togglePlay() ? `pause` : `play`;
  });
  p = new Point(width / 2, height / 2);
}

function draw() {
  clear();
  setColor(`black`);
  point(p.x, p.y);
  p.x += random(-2, 2);
  p.y += random(-2, 2);
}

Movable entities top

Rather than having to write your own click-drag logic, you can mark things as "movable" by calling setMovable(...). This can either be Point instances or an array of Point instances. After marking them as movable, the API does the rest. When the pointer is over movable points it will update the global currentMovable value and change to the typically pointing finger icon, with click-dragging (or touch-dragging) automatically updating your point's coordinates.

const p;

function setup() {
  setSize(400, 400);
  p = new Point(200, 200);
  setMovable(p);
}

function draw() {
  clear(`#eee`);
  setColor(`#444`);
  point(p.x, p.y);
}

That's all the code we need: users can now click/tap/touch-drag our point around.

Pointer handling top

Graphics interaction is based on "the pointer", which is a unified handler for mouse, stylus, and touch handling, so you don't have to care whether your code runs on a desktop computer, a laptop, a tablet, a phone, or anything else. Event handling uses five functions:

function pointerActive(trueOrFalse) {
  // the pointer either entered or left the canvas region.
}

function pointerDown(x, y) {
  // a mouse down, stylus down, or touch start at
  // graphics coordinate x/y (not screen coordinate).
}

function pointerUp(x, y) {
  // a mouse up, stylus up, or touch end at
  // graphics coordinate x/y.
}

function pointerClick(x, y) {
  // a shorthand function for a pointerdown followed
  // by a pointerup on the same x/y coordinate
}

function pointerMove(x, y) {
  // a mouse move/drag, stylus move/drag, or touch
  // move/drag at graphics coordinate x/y.
}

function pointerDrag(x, y) {
  // a shorthand function for a pointer move at some
  // time after a pointer down, but before pointer up.
}

In addition to this, there is the global pointer object that can be consulted in any of your code, with the following properties:

Keyboard handling top

When focus is on the <graphics-element>, keyboard input will be sent into your graphics code, using the following functions:

function keyDown(key, shift, alt, ctrl, meta) {
  // the "key" value is the key name being pressed,
  // shift, alt, ctrl, and meta are boolean flags
}

function keyUp(key, shift, alt, ctrl, meta) {
  // as above
}

Note that there is no "key typed" handler, you get to decide whether down or up counts as "typing". There is a global keyboard object that tracks which keys are down: if a key is down, it will have a corresponding keyboard[keyName] entry, with its value being the Date.now() timestamp when the key got pressed. Once the key is released, its entry gets removed from keyboard (not just set to a falsey value).

Linking guide text and graphic top

The graphics element supports automatic highlighting of parts of your graphic by using color tags. For example, the following graphics code:

function draw() {
  setColor(`red`);
  line(0, 0, width, height);
  text(`the center of the universe`, width / 2, height / 2);
}

can be combined with guide text that includes a highlighting tag for the corresponding color:

<graphics-element title="..." src="...">
  <p>Let's highlight <red>the universe</red>!</p>
</graphics-element>

Now, whenever a user places their pointer (mouse, stylus, or touch) on that marked-up text, the corresponding color will get highlighted in the graphic. All named CSS colors are supported for this purpose.

Spreading code over multiple source files top

The <graphics-element> element allows you to specify multiple source files, with one "main "file indicated using the src="..." attribute, and additional sources through the use of the <source> element:

...

<p>Let's look at the base case:</p>

<graphics-element
  title="multiple sources"
  src="./base-code.js"
></graphics-element>

<p>We can extend this by using the standard approach:</p>

<graphics-element title="established convention" src="./base-code.js">
  <source src="variation-01.js" />
</graphics-element>

<p>But there's a more interesting way to tackle this:</p>

<graphics-element title="a creative variation" src="./base-code.js">
  <source src="variation-02.js" />
</graphics-element>

...

This will create a single source file, but allows you to split up your code in a way that lets you reuse the same code across multiple graphics elements with only the differences stored in each extra source file.

Overloading "parent" functions

Your additional sources may contain additional setup() and draw() functions, which will be run at the end of the main setup() and draw() functions, in the same order of your <source> elements.

You cannot use this to "redeclare" any other function, though: if the main source file contains function test() { ... } and your additional source file also specifies function test() { ... }, then you'll see an error in your Developer Tools "console" tab along the lines of:

Uncaught (in promise) SyntaxError: redeclaration of function test
note: Previously declared at line 123, column 45

Why use <source> to load code when JS imports exist?

The way additional sources are included fundamentally differs from the standard module import mechanism: JS imports are loaded in their own, isolated, scope and will not have access to any of the graphics functions and constants, whereas code loaded through a <source> element effectively gets packed up as a single bundle with the "main" graphics source code and so will have access to all the graphics API functions.

As such, if you need pure JS code imported, use an import statement in your source code, but if you need something like a class that knows how to draw itself by calling graphics API functions, you can load that same code as a <source> and things will work just fine.

As example, pure utility code that doesn't do any sort of drawing should always be imported:

class PID {
  constructor(p, i, d, ...) {
    this.kp = p;
    this.ki = i;
    this.kd = d;
    ...
  }

  getRecommendation(current, target) {
    const error = target - current;
    const P = (kp * error) / dt;
    const I = ...
    // ...
    return P + I + D;
  }
}

Nothing in the above code has anything to do with drawing things, and is pure JS utility code. As such, we can simply import it directly:

import { PID } from "./pid.js";

let pid;

function setup() {
  pid = new PID(1, 0, 0);
}

// ...

However, the following kind of code should be a <source> inclusion:

class Airplane extends Circle {
  heading = 0;

  constructor(x, y, heading) {
    super(x, y, 100);
    this.heading = heading;
  }

  draw() {
    save();
    translate(this.x, this.y);
    rotate(this.heading);
    line(0, 0, 20, 0);
    restore();
  }
}

This code heavily relies on the graphics API, and so trying to import it will throw errors at runtime.

Further reading top

That covers the "regular" documentation, but head on over to the API documentation to learn all about which constants and functions you can use to create cool graphics!

And if this made a difference in your dev life, consider (temporarily, even?) becoming a patron of my work over on my Patreon page, or send a one-time donatation to help keep this project, and others like it, funded. Any amount is appreciated!