Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active February 4, 2023 21:57
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save mbostock/1276463 to your computer and use it in GitHub Desktop.
Save mbostock/1276463 to your computer and use it in GitHub Desktop.
DOM-to-Canvas using D3
license: gpl-3.0

Mouseover to draw circles!

This is a quick proof-of-concept example demonstrating how to create a canvas scenegraph in the DOM using custom namespaced elements. The scenegraph in this example consists of a simple container sketch element and a number of child circle elements:

<custom:sketch width="960" height="500">
  <custom:circle x="300" y="400" r="128" strokeStyle="red"/>
  <custom:circle x="302" y="404" r="129" strokeStyle="red"/>
  …
</custom:sketch>

The browser ignores these elements because they exist in our "custom" namespace. To render them, we use a timer that iterates over the child elements and draws them to a canvas element.

Why do this? Well, if you wanted your own custom representation tailored to a specific application or domain, you can! This example demonstrates how to use the DOM to implement your own element hierarchy and render it to canvas. If you're designing a general-purpose graphical representation, though, I recommend using SVG instead. For comparison, see the original OMG Particles! example.

Implementation note: I'd prefer to use DOM Mutation Events to listen for changes to our custom elements, but browsers seem a bit sluggish in reporting them, particularly when elements are removed. If you have a slow-moving scene, you could probably get away with using mutation events rather than a timer that runs continuously. Alternatively, you could improve this example by stopping the timer after extended periods of activity.

<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
// Register the "custom" namespace prefix for our custom elements.
d3.ns.prefix.custom = "https://d3js.org/namespace/custom";
var width = 960,
height = 500;
// Add our "custom" sketch element to the body.
var sketch = d3.select("body").append("custom:sketch")
.attr("width", width)
.attr("height", height)
.call(custom);
// On each mouse move, create a circle that increases in size and fades away.
d3.select(window).on("mousemove", function() {
sketch.append("custom:circle")
.attr("x", d3.event.clientX)
.attr("y", d3.event.clientY)
.attr("radius", 0)
.attr("strokeStyle", "red")
.transition()
.duration(2000)
.ease(Math.sqrt)
.attr("radius", 200)
.attr("strokeStyle", "white")
.remove();
});
function custom(selection) {
selection.each(function() {
var root = this,
canvas = root.parentNode.appendChild(document.createElement("canvas")),
context = canvas.getContext("2d");
canvas.style.position = "absolute";
canvas.style.top = root.offsetTop + "px";
canvas.style.left = root.offsetLeft + "px";
// It'd be nice to use DOM Mutation Events here instead.
// However, they appear to arrive irregularly, causing choppy animation.
d3.timer(redraw);
// Clear the canvas and then iterate over child elements.
function redraw() {
canvas.width = root.getAttribute("width");
canvas.height = root.getAttribute("height");
for (var child = root.firstChild; child; child = child.nextSibling) draw(child);
}
// For now we only support circles with strokeStyle.
// But you should imagine extending this to arbitrary shapes and groups!
function draw(element) {
switch (element.tagName) {
case "circle": {
context.strokeStyle = element.getAttribute("strokeStyle");
context.beginPath();
context.arc(element.getAttribute("x"), element.getAttribute("y"), element.getAttribute("radius"), 0, 2 * Math.PI);
context.stroke();
break;
}
}
}
});
};
</script>
@abd1rahmane
Copy link

Interesting . thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment