Frontend Development 18 min read

Design and Implementation of SpriteJS: A Cross‑Platform WebGL/Canvas2D Rendering Engine

This article details the four‑year development of SpriteJS, a high‑performance, cross‑platform graphics system that supports WebGL and Canvas2D rendering, DOM‑like APIs, responsive design, WebWorker integration, and extensive performance optimizations for visualisation and game applications.

ByteFE
ByteFE
ByteFE
Design and Implementation of SpriteJS: A Cross‑Platform WebGL/Canvas2D Rendering Engine

From 2017 to 2020 the author built SpriteJS, a rendering engine that can switch between WebGL and Canvas2D, runs in browsers, SSR, and mini‑programs, and follows a DOM‑style API to provide responsive, high‑performance batch rendering for visualisation and game scenarios.

The original need was unrelated to rendering; while working at Qihoo 360 the team used D3 for visualisation, discovering that D3 is a data‑driven engine rather than a rendering framework, which motivated the creation of a custom graphics system.

SpriteJS mirrors the browser DOM API for consistency, exposing core modules such as node.js , group.js , attribute/node.js , document/index.js , and selector/index.js on GitHub.

In designing the system’s skeleton, the default coordinate system follows the browser’s canvas coordinates, and a projection matrix is computed dynamically (see code below).

updateResolution() {
  const {width, height} = this.canvas;
  const m1 = [
    1, 0, 0,
    0, 1, 0,
    -width / 2, -height / 2, 1,
  ];
  const m2 = [
    2 / width, 0, 0,
    0, -2 / height, 0,
    0, 0, 1,
  ];
  const m3 = mat3(m2) * mat3(m1);
  this.projectionMatrix = m3;
  if (this[_glRenderer]) {
    this[_glRenderer].gl.viewport(0, 0, width, height);
  }
}

SpriteJS uses a scene‑layer‑group hierarchy where each Layer maps to an independent canvas, enabling multi‑threaded drawing via WebWorkers and simplifying event handling, though it may increase memory usage.

Attribute changes trigger an asynchronous update mechanism rather than a fixed‑rate animation loop; only relevant property changes (e.g., geometry‑affecting attributes) cause a re‑render, reducing unnecessary computation.

For integration with external libraries like ThreeJS or ClayGL, SpriteJS provides a manual ticker that can be controlled independently.

Cross‑platform support is achieved through polyfills for browsers, Node.js, WeChat mini‑programs, and games, with a dedicated node-canvas-webgl package for Node environments.

The box model for block elements follows standard CSS, allowing border , padding , and boxSizing adjustments.

Event handling uses a custom event system that maps DOM‑style coordinates to the canvas, with hit‑testing performed on triangle meshes (see code snippet).

function inTriangle(p1, p2, p3, point) {
  const a = p2.copy().sub(p1);
  const b = p3.copy().sub(p2);
  const c = p1.copy().sub(p3);
  const u1 = point.copy().sub(p1);
  const u2 = point.copy().sub(p2);
  const u3 = point.copy().sub(p3);
  const s1 = Math.sign(a.cross(u1));
  // ... (omitted for brevity) ...
  return s1 === s2 && s2 === s3;
}

Animation is powered by the sprite-timeline library, which provides a forkable timeline with playbackRate control, entropy tracking, and integration with the Web Animations API.

From 2D to WebGL, the author introduced a mesh‑based rendering pipeline (mesh.js) that converts SVG paths into contours and triangulated meshes, using GLU Tessellator for robust triangulation.

Stroke rendering for thick polylines is implemented via JavaScript extrusion of vertices, handling end‑caps and joins by calculating normal vectors and extrusion lengths (see code).

function extrudePolyline(gl, points, {thickness = 10} = {}) {
  const halfThick = 0.5 * thickness;
  const innerSide = [];
  const outerSide = [];
  // ... (vertex generation logic) ...
}

Batch rendering compresses vertex data into large typed arrays for efficient draw calls, and custom shaders can be attached to elements via programs; post‑processing passes are also supported.

Performance optimizations include conditional blending (skip when no alpha), contour caching (re‑compute only when geometry‑affecting attributes change), and the seal method to merge static group geometry into a single renderable mesh.

Additional details cover screen resolution handling, resource loading, and other platform‑specific adaptations.

performancegraphicsRenderingWebGLvisualizationCanvas2DSpriteJS
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.