Lesson 12-Sprite.js Basic Syntax and Usage

Sprite.js Basics and Quick Start Guide

Introduction

Sprite.js is a cross-platform 2D graphics object model library designed for high-performance rendering and animation. It offers a rich API to create and manage 2D graphical elements such as sprites, textures, shapes, and animations.

A standout feature of Sprite.js is its compatibility, particularly when integrated with libraries like d3.js for data visualization. This enables the creation of complex charts, including bar graphs, layered diagrams, maps, and force-directed layouts.

Additionally, Sprite.js can work with the lightweight Proton JavaScript engine to achieve special visual effects, such as fire or custom background animations, greatly expanding its applications in creative design and game development.

Features and Use Cases

Features

  • High-Performance Rendering: Optimized rendering engine ensures smooth display of complex 2D graphics and animations across devices.
  • Cross-Platform Compatibility: Supports multiple browsers and Node.js server-side rendering, meeting diverse development needs.
  • Easy Integration: Seamless compatibility with libraries like d3.js, simplifying use in data visualization projects.
  • Flexible Animation System: Supports complex animation sequences for character animations or UI effects.
  • Resource Management: Efficiently manages texture atlases to reduce HTTP requests and improve load times.
  • Extensibility: Open API design allows developers to customize and extend functionality for specific project requirements.

Use Cases

  • Game Development: Leverages sprite sheet technology to manage game characters and scene elements, ideal for 2D games or game UIs.
  • Data Visualization: Combines with d3.js to create dynamic, interactive charts and infographics for business intelligence and data analysis.
  • Educational Software: Develops engaging interactive content, such as scientific simulations or historical timelines.
  • Creative Design and Advertising: Crafts eye-catching animated ads, landing pages, or brand showcases to enhance user experience.
  • Desktop and Mobile Apps: Builds rich graphical interfaces for cross-platform apps, like animated emojis in chat apps or dynamic headlines in news apps.
  • Art and Experimental Projects: Enables artists and designers to explore creative visual expressions and create digital artworks.

Installation

If using npm for package management, install Sprite.js with:

npm install spritejs

Then, import it using ES Modules:

import * as spritejs from 'spritejs';

For Node.js environments, install the dependency node-canvas:

npm install canvas@next

If using Yarn:

yarn add spritejs

For browser use, include the CDN version:

<script src="https://unpkg.com/spritejs@3/dist/spritejs.js"></script>

For server-side rendering in Node.js, install node-canvas. First, ensure dependencies are met (on Debian-based systems):

sudo apt-get install libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++

Then install node-canvas 2.x:

npm install canvas@next

Architecture

Scene

  • The top-level container representing the entire visible area. A scene can hold multiple layers to organize visual elements.
  • Manages layer rendering order and update logic, providing scene-level event handling.

Layer

  • An independent rendering area within a scene, containing groups or primitives. Layers allow grouped management of elements for effects like occlusion or blending.
  • Supports various rendering modes (2D, 3D, WebGL) for flexible graphics rendering.

Group

  • A container for multiple graphical elements (e.g., rectangles, circles, images). Groups simplify complex structure management, supporting transformations (rotation, scaling, translation) and hierarchies.
  • Elements within a group can share transformations for unified operations while allowing individual control.

Primitive

  • Basic shapes like rectangles (Rect), circles (Circle), text (Text), and images (Image), serving as building blocks for complex visuals.
  • Each primitive has attributes defining its appearance, such as size, color, borders, and shadows.

Animation System

  • Manages time-driven visual changes, including position, size, and color transitions.
  • Supports keyframe animations, sequences, and physics-based animations for intuitive and powerful creation.

Event System

  • Handles user inputs and interactions (e.g., clicks, drags) with support for event bubbling and delegation.
  • Events can be bound to any graphical object, enabling interactivity.

Resource Management

  • Manages loading, caching, and releasing of textures and images to optimize memory and performance.
  • Supports asynchronous resource loading to maintain responsiveness.

Renderer

  • The core component that converts layers and elements into pixels for display.
  • Supports post-processing effects like filters, blurs, and shadows for enhanced visuals.

Basic Usage

Below is a simple example demonstrating how to create a canvas and draw a red rectangle with Sprite.js:

// Import Sprite.js
const { Scene, Group, Rect } = spritejs;

// Create a scene with dimensions and rendering mode (default: 2D)
const scene = new Scene('#container', {
  width: 800,
  height: 600,
  mode: '2d',
});

// Create a layer as a container
const layer = scene.layer();

// Create a red rectangle
const rect = new Rect({
  size: [100, 100], // Width and height
  pos: [400, 300], // Position
  fillColor: '#ff0000', // Fill color
});

// Add rectangle to the layer
layer.append(rect);

// Start rendering
scene.render();

This code creates a Scene as the container for all graphics, then adds a Layer to manage related elements. A red Rect is created with specified size, position, and color, added to the Layer, and rendered with scene.render().

Sprite.js Basic Usage

Coordinates

Understanding the coordinate system and drawing dimensions in Sprite.js is fundamental for creating 2D graphics.

Coordinate System

Sprite.js uses a standard 2D Cartesian coordinate system, with the origin (0, 0) at the top-left corner of the canvas. The x-axis extends rightward, and the y-axis downward. All position and size parameters are based on this system.

  • Position (pos): Set via the pos attribute as an array [x, y], representing the top-left corner of the element.

Drawing Dimensions

  • Size: Defined by the size attribute as an array [width, height], specifying the element’s width and height.

Example: Draw a red rectangle centered in an 800×600 scene, with a size of 200×100.

const { Scene, Layer, Rect } = spritejs;

// Create scene
const scene = new Scene('#container', {
  width: 800,
  height: 600,
});

// Create layer
const layer = scene.layer();

// Calculate rectangle's center position
const centerX = scene.width / 2;
const centerY = scene.height / 2;
const rectSize = [200, 100];

// Create centered rectangle
const rect = new Rect({
  size: rectSize,
  anchor: [0.5, 0.5], // Set anchor to center
  pos: [centerX, centerY], // Center position
  fillColor: '#ff0000', // Fill color
});

// Add to layer
layer.append(rect);

// Render
scene.render();
  • Anchor: Instead of using pos for the top-left corner, the anchor attribute (with pos) centers the rectangle. anchor: [0.5, 0.5] sets the anchor to the rectangle’s center.
  • Size and Position: Calculating positions relative to the canvas size ensures accurate placement.

Elements

Anchor

The anchor defines the reference point for transformations (e.g., position, rotation). By default, it’s at the top-left corner (0, 0), but can be adjusted.

const rect = new Rect({
  size: [100, 100],
  pos: [200, 200],
  anchor: [0.5, 0.5], // Center anchor
});

Border

Borders are set using strokeColor and lineWidth.

const rect = new Rect({
  size: [100, 100],
  pos: [200, 200],
  strokeColor: '#000', // Border color
  lineWidth: 5, // Border width
});

Padding and Box Model

Sprite.js doesn’t directly support padding, but effects can be achieved by combining elements or custom drawing.

Grouping

Groups organize multiple elements for unified operations.

const group = new Group();
group.append(new Rect({ size: [50, 50], pos: [0, 0], fillColor: '#F00' }));
group.append(new Circle({ radius: 90, pos: [100, 50], fillColor: '#0F0' }));
layer.append(group);

SVG Integration

Sprite.js supports SVG paths for importing graphics.

const pathData = 'M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80';
const svgShape = new Shape({
  draw(context) {
    context.fillStyle = '#000';
    context.beginPath();
    context.svgPath(pathData);
    context.fill();
  },
});

Label (Text)

The Text class creates text labels with rich styling.

const label = new Text({
  text: 'Hello, Sprite.js!',
  fontSize: 30,
  pos: [200, 300],
  fontStyle: 'italic',
  fontWeight: 'bold',
});

Rectangle (Rect)

const rect = new Rect({
  size: [100, 100],
  pos: [200, 150],
  fillColor: '#FF0000',
  strokeColor: '#000',
  lineWidth: 2,
});

Analysis: Creates a red-filled rectangle with a black border. size sets dimensions, pos sets position, fillColor and strokeColor define colors, and lineWidth controls border thickness.

Circle

const circle = new Circle({
  radius: 50,
  pos: [300, 200],
  fillColor: '#00FF00',
});

Analysis: Creates a green-filled circle. radius defines size, with pos and fillColor similar to Rect.

Text

const text = new Text({
  text: 'Hello, World!',
  fontSize: 24,
  pos: [400, 250],
  fillColor: '#000',
  textAlign: 'center',
  textBaseline: 'middle',
});

Analysis: Creates centered text. text sets content, fontSize sets size, fillColor sets color, and textAlign/textBaseline control alignment.

Image

const img = new Sprite({
  texture: 'path/to/image.png',
  size: [200, 200],
  pos: [500, 300],
});

Analysis: Loads an image. texture specifies the URL, size sets display dimensions, and pos sets position.

Line

Sprite.js lacks a direct Line class, but lines can be drawn using Shape.

const line = new Shape({
  draw(context) {
    context.beginPath();
    context.moveTo(100, 100);
    context.lineTo(500, 300);
    context.strokeStyle = '#000';
    context.lineWidth = 2;
    context.stroke();
  },
});

Analysis: Draws a line from (100, 100) to (500, 300) using Shape’s draw method.

Beyond basic classes (Rect, Circle, etc.), the Shape class enables custom path drawing, including triangles, parallelograms, polylines, polygons, squares, stars, arcs, elliptical arcs, and rings.

Triangle

const triangle = new Shape();
triangle.draw((ctx, width, height) => {
  ctx.beginPath();
  ctx.moveTo(0, 0);
  ctx.lineTo(width, 0);
  ctx.lineTo(width / 2, height);
  ctx.closePath();
  ctx.fillStyle = 'blue';
  ctx.fill();
});

Parallelogram

const parallelogram = new Shape();
parallelogram.draw((ctx, width, height) => {
  ctx.beginPath();
  ctx.moveTo(0, 0);
  ctx.lineTo(width, 0);
  ctx.lineTo(width + height / 2, height);
  ctx.lineTo(-height / 2, height);
  ctx.closePath();
  ctx.fillStyle = 'green';
  ctx.fill();
});

Polyline

const polyline = new Shape();
polyline.draw((ctx, width, height) => {
  ctx.beginPath();
  ctx.moveTo(0, 0);
  ctx.lineTo(width / 2, height / 2);
  ctx.lineTo(width, 0);
  ctx.strokeStyle = 'red';
  ctx.lineWidth = 2;
  ctx.stroke();
});

Polygon

const polygon = new Shape();
polygon.draw((ctx, width, height) => {
  const sides = 5;
  const radius = Math.min(width, height) / 2;
  ctx.beginPath();
  for (let i = 0; i < sides; i++) {
    const angle = (i * 2 * Math.PI) / sides;
    ctx.lineTo(radius * Math.cos(angle) + radius, radius * Math.sin(angle) + radius);
  }
  ctx.closePath();
  ctx.fillStyle = 'purple';
  ctx.fill();
});

Square

const square = new Shape();
square.attr({ size: [100, 100] });
square.draw((ctx, width, height) => {
  ctx.fillStyle = 'black';
  ctx.fillRect(0, 0, width, height);
});

Star

const star = new Shape();
star.draw((ctx, width, height) => {
  const spikes = 5;
  const outerRadius = width / 2;
  const innerRadius = outerRadius * 0.5;
  let rot = (Math.PI / 2) * 3;
  ctx.beginPath();
  for (let i = 0; i < spikes; i++) {
    ctx.lineTo(outerRadius + outerRadius * Math.cos(rot), outerRadius + outerRadius * Math.sin(rot));
    rot += Math.PI / spikes;
    ctx.lineTo(outerRadius + innerRadius * Math.cos(rot), outerRadius + innerRadius * Math.sin(rot));
    rot += Math.PI / spikes;
  }
  ctx.closePath();
  ctx.fillStyle = 'yellow';
  ctx.fill();
});

Arc

const arc = new Shape();
arc.draw((ctx, width, height) => {
  ctx.beginPath();
  ctx.arc(width / 2, height / 2, 50, 0, (Math.PI * 2) / 3);
  ctx.strokeStyle = 'black';
  ctx.stroke();
});

Elliptical Arc

const ellipseArc = new Shape();
ellipseArc.draw((ctx, width, height) => {
  ctx.beginPath();
  ctx.ellipse(width / 2, height / 2, width / 4, height / 8, 0, 0, Math.PI * 2);
  ctx.strokeStyle = 'black';
  ctx.stroke();
});

Ring

const ring = new Shape();
ring.draw((ctx, width, height) => {
  const outerRadius = width / 2;
  const innerRadius = outerRadius * 0.7;
  ctx.beginPath();
  ctx.arc(width / 2, height / 2, outerRadius, 0, Math.PI * 2);
  ctx.arc(width / 2, height / 2, innerRadius, 0, Math.PI * 2, true);
  ctx.fillStyle = 'gray';
  ctx.fill();
});

Using the Shape class’s draw method, you can create complex paths and shapes by leveraging geometric principles and the Canvas API.

Effects

Transitions

Call transition before changing attributes to create smooth transitions, specifying duration and optional easing.

Example: Rectangle color and size transition.

const { Scene, Arc } = spritejs;
const scene = new Scene({
  container: document.getElementById('adaptive'),
  width: 1200,
  height: 600,
});
const layer = scene.layer();

async function createBubble() {
  const x = 100 + Math.random() * 1000;
  const y = 100 + Math.random() * 400;
  const r = Math.round(255 * Math.random());
  const g = Math.round(255 * Math.random());
  const b = Math.round(255 * Math.random());

  const fillColor = `rgb(${r},${g},${b})`;
  const bubble = new Arc();
  bubble.attr({
    fillColor,
    radius: 25,
    x,
    y,
  });
  layer.append(bubble);
  await bubble.transition(2.0).attr({
    scale: [2.0, 2.0],
    opacity: 0,
  });
  bubble.remove();
}

setInterval(() => {
  createBubble();
}, 50);

sprite.transition(...) returns a special object (not the original sprite) that creates attribute animations when attr is called. Subsequent calls end the previous animation, enabling smooth state changes. Use reverse to roll back the current transition.

const { Scene, Sprite, Label } = spritejs;
const scene = new Scene({
  container: document.getElementById('adaptive'),
  width: 1200,
  height: 600,
});
const layer = scene.layer();

const label = new Label('Try moving the mouse over the two squares:');
label.attr({
  anchor: 0.5,
  pos: [400, 50],
  font: '2rem Arial',
});
layer.append(label);

const left = new Sprite();
left.attr({
  anchor: 0.5,
  pos: [300, 300],
  size: [200, 200],
  bgcolor: 'red',
});
layer.append(left);

const right = left.cloneNode();
right.attr({
  pos: [700, 300],
  bgcolor: 'green',
});
layer.append(right);

let leftTrans = null;
left.addEventListener('mouseenter', () => {
  if (leftTrans) leftTrans.cancel();
  leftTrans = left.transition(1.0);
  leftTrans.attr({
    rotate: 180,
    bgcolor: 'green',
  });
});
left.addEventListener('mouseleave', () => {
  leftTrans.attr({
    rotate: 0,
    bgcolor: 'red',
  });
});

let rightTrans = null;
right.addEventListener('mouseenter', () => {
  if (rightTrans) rightTrans.cancel();
  rightTrans = right.transition(3.0);
  rightTrans.attr({
    rotate: 720,
    bgcolor: 'red',
  });
});
right.addEventListener('mouseleave', () => {
  rightTrans.reverse();
});

Animations

The animate function is key for creating smooth transitions by declaratively defining property changes over time.

Basic Usage
const { Scene, Layer, Rect } = spritejs;

const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

const rect = new Rect({
  size: [100, 100],
  pos: [400, 300],
  fillColor: '#f00',
});
layer.append(rect);

rect.animate([
  { pos: [600, 400], size: [150, 150], rotate: 180 },
], {
  duration: 2000,
  easing: 'easeInOut',
}).play();

Parameters:

  • Target Object: The element to animate (e.g., rect).
  • Animation Properties: An array or object defining target values (e.g., pos, size, rotate, fillColor).
  • Duration: Animation length in milliseconds.
  • Easing: Controls speed variation (e.g., linear, easeIn, easeOut, easeInOut).
  • play(): Starts the animation.
Chained Animations

Chain animations to run sequentially.

rect.animate([{ pos: [600, 400] }], { duration: 1000 }).play().then(() => {
  rect.animate([{ pos: [400, 300] }], { duration: 1000 }).play();
});
Composite Animations

Animate multiple properties simultaneously.

rect.animate([
  { pos: [600, 400], size: [150, 150], rotate: 180, opacity: 0.5 },
], {
  duration: 2000,
  easing: 'easeInOut',
}).play();
Custom Animation Logic

Use update and complete callbacks for complex animations.

rect.animate([], {
  update(progress) {
    rect.attr({ rotate: progress * 360 });
  },
  complete() {
    console.log('Animation complete');
  },
  duration: 2000,
}).play();

Filters

Sprite.js supports filters for visual effects like blur, shadows, or color adjustments, implemented via WebGL shaders.

Blur Filter
const { Scene, Layer, Rect } = spritejs;

const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

const rect = new Rect({
  size: [200, 200],
  pos: [300, 250],
  fillColor: '#f00',
});

const blurFilter = {
  type: 'blur',
  blurRadius: 10,
};

rect.filters = [blurFilter];
layer.append(rect);

scene.render();
  • Filter Creation: Defines a filter with a type and parameters (e.g., blurRadius).
  • Application: Assign filters to an element’s filters array.

Gradients

Sprite.js creates gradients using the Gradient class, supporting LinearGradient or RadialGradient based on the vector parameter.

const { Scene, Sprite, Gradient, Label, Path } = spritejs;
const scene = new Scene({
  container: document.getElementById('adaptive'),
  width: 1200,
  height: 600,
});
const layer = scene.layer();

const box = new Sprite();
box.attr({
  borderWidth: 10,
  border: {
    color: new Gradient({
      vector: [0, 0, 170, 170],
      stops: [
        { offset: 0, color: 'red' },
        { offset: 0.5, color: 'yellow' },
        { offset: 1, color: 'green' },
      ],
    }),
  },
  bgcolor: new Gradient({
    vector: [0, 150, 150, 0],
    stops: [
      { offset: 0, color: '#fff' },
      { offset: 0.5, color: 'rgba(33, 33, 77, 0.7)' },
      { offset: 1, color: 'rgba(128, 45, 88, 0.5)' },
    ],
  }),
  pos: [150, 50],
  size: [150, 150],
  borderRadius: 15,
});
layer.append(box);

const label = new Label('Hello SpriteJS~~');
label.attr({
  lineWidth: 6,
  fillColor: new Gradient({
    vector: [35, 35, 350, 350],
    stops: [
      { offset: 0, color: '#777' },
      { offset: 0.5, color: '#ccc' },
      { offset: 1, color: '#333' },
    ],
  }),
  pos: [500, 50],
  font: '48px Arial',
});
layer.append(label);

const path = new Path();
path.attr({
  d: 'M480,50L423.8,182.6L280,194.8L389.2,289.4L356.4,430L480,355.4L603.6,430L570.8,289.4L680,194.8L536.2,182.6Z',
  normalize: true,
  rotate: 30,
  scale: 0.7,
  fillColor: new Gradient({
    vector: [300, 300, 100, 100],
    stops: [
      { offset: 0, color: 'red' },
      { offset: 0.5, color: 'yellow' },
      { offset: 1, color: 'green' },
    ],
  }),
  pos: [700, 360],
});
layer.append(path);

Behaviors

Events

The Scene automatically proxies mouse and touch events, making them easy to handle with addEventListener.

const { Scene, Sprite, Label, Path } = spritejs;
const scene = new Scene({
  container: document.getElementById('adaptive'),
  width: 1200,
  height: 600,
});
const layer = scene.layer();

const s1 = new Sprite();
s1.attr({
  anchor: [0.5, 0.5],
  pos: [770, 300],
  size: [300, 300],
  rotate: 45,
  bgcolor: '#3c7',
});
layer.append(s1);

s1.addEventListener('mouseenter', () => {
  s1.attr({ border: [4, 'blue'] });
});
s1.addEventListener('mouseleave', () => {
  s1.attr({ border: [0, ''] });
});

const anchorCross = new Path({
  d: 'M-10,0H10M0,-10V10',
  pos: [770, 300],
  strokeColor: 'red',
  rotate: 45,
  lineWidth: 4,
  pointerEvents: 'none',
});
layer.append(anchorCross);

const label = new Label('Mouse position:');
label.attr({
  pos: [20, 50],
  font: '24px Arial',
  lineHeight: 56,
});
layer.append(label);

s1.addEventListener('mousemove', (evt) => {
  const { x, y } = evt;
  label.attr({ text: `Mouse position:\nRelative to anchor: ${s1.getOffsetPosition(x, y).map(Math.round)}` });
  evt.stopPropagation();
});

layer.addEventListener('mousemove', (evt) => {
  const { x, y } = evt;
  label.attr({ text: `Mouse position:\nRelative to Layer: ${[Math.round(x), Math.round(y)]}` });
});

Sprite.js Rendering Engine, Screen Adaptation, and Memory Management

Rendering Engine

Specify the rendering engine via parameters in Scene or Layer.

const { Scene, Rect } = spritejs;
const container = document.getElementById('adaptive');
const scene = new Scene({
  container,
  width: 600,
  height: 600,
  contextType: '2d',
});
const layer = scene.layer({
  autoRender: false,
});

const rect = new Rect({
  normalize: true,
  pos: [300, 300],
  size: [100, 100],
  fillColor: 'red',
  opacity: 0.5,
});
layer.append(rect);

/* globals curvejs */
const { Stage, Curve } = curvejs;
const canvas = layer.canvas;
const stage = new Stage(canvas);
const rd = () => -2 + Math.random() * 2;
const curve = new Curve({
  color: '#00FF00',
  points: [277, 327, 230, 314, 236, 326, 257, 326],
  data: [rd(), rd(), rd(), rd(), rd(), rd(), rd(), rd()],
  motion: function(points, data) {
    points.forEach((item, index) => {
      points[index] += data[index];

      if (points[index] < 0) {
        points[index] = 0;
        data[index] *= -1;
      }
      if (index % 2 === 0) {
        if (points[index] > canvas.width) {
          points[index] = canvas.width;
          data[index] *= -1;
        }
      } else if (points[index] > canvas.height) {
        points[index] = canvas.height;
        data[index] *= -1;
      }
    });
  },
});
stage.add(curve);

let ang = 0;
function tick() {
  stage.update();
  rect.attr({ rotate: ang++ });
  layer.render({ clear: false });
  requestAnimationFrame(tick);
}

tick();

WebGL2/WebGL engines are not always superior to Canvas2D. In memory-constrained scenarios, Canvas2D may be more efficient due to lower memory usage.

Memory

To manage memory in constrained environments, set the bufferSize parameter in Scene or Layer to reduce the number of merged vertices, lowering memory consumption.

const scene = new Scene({
  container,
  width: 1000,
  height: 1000,
  bufferSize: 500,
});

Screen Adaptation

Adaptation Parameters (mode options)

  • scale (default): Stretches the canvas to match the container size, potentially distorting the canvas.
  • stickyWidth: Sets canvas width to container width, adjusts height proportionally, and centers vertically.
  • stickyTop: Sets canvas width to container width, adjusts height proportionally, and aligns with container top.
  • stickyBottom: Sets canvas width to container width, adjusts height proportionally, and aligns with container bottom.
  • stickyHeight: Sets canvas height to container height, adjusts width proportionally, and centers horizontally.
  • stickyLeft: Sets canvas height to container height, adjusts width proportionally, and aligns with container left.
  • stickyRight: Sets canvas height to container height, adjusts width proportionally, and aligns with container right.
const { Scene, Sprite } = spritejs;
const container = document.getElementById('adaptive');
const scene = new Scene({
  container,
  width: 640,
  height: 1000,
  mode: 'stickyWidth',
});
const layer = scene.layer();

(async function () {
  await scene.preload(
    { id: 'snow', src: 'https://p5.ssl.qhimg.com/t01bfde08606e87f1fe.png' },
    { id: 'cloud', src: 'https://p5.ssl.qhimg.com/t01d2ff600bae7fe897.png' }
  );

  const cloud = new Sprite({ texture: 'cloud' });
  cloud.attr({
    anchor: [0.5, 0],
    pos: [320, -50],
    size: [200, 130],
  });
  layer.append(cloud);

  function addRandomSnow() {
    const snow = new Sprite({ texture: 'snow' });
    const x0 = 20 + Math.random() * 600;
    const y0 = 0;

    snow.attr({
      anchor: [0.5, 0.5],
      pos: [x0, y0],
      size: [50, 50],
    });

    snow.animate([
      { x: x0 - 10 },
      { x: x0 + 10 },
    ], {
      duration: 1000,
      fill: 'forwards',
      direction: 'alternate',
      iterations: Infinity,
      easing: 'ease-in-out',
    }).play();

    const dropAnim = snow.animate([
      { y: -200, rotate: 0 },
      { y: 2000, rotate: 1880 },
    ], {
      duration: 15000,
      fill: 'forwards',
    }).play();

    dropAnim.then(() => {
      snow.remove();
    });

    layer.append(snow);
  }

  setInterval(addRandomSnow, 200);
})();

Sprite.js Image Loading and Advanced Animations

Asynchronous Image Loading

The Scene class provides a preload method to load and cache image resources asynchronously. It accepts one or more image data objects and returns a promise.

const { Scene, Sprite, Label } = spritejs;
const container = document.getElementById('adaptive');
const scene = new Scene({
  container,
  width: 1200,
  height: 600,
});
const layer = scene.layer();

(async function () {
  await scene.preload({
    id: 'robot',
    src: 'https://p5.ssl.qhimg.com/t01c33383c0e168c3c4.png',
  });

  const robot = new Sprite({ texture: 'robot' });
  robot.attr({
    anchor: [0.5, 0.5],
    pos: [600, 300],
    scale: 0.5,
  });
  layer.append(robot);

  const label = new Label(`Image size: ${robot.contentSize}`);
  label.attr({
    anchor: [0.5, 0.5],
    pos: [600, 100],
    font: '36px Arial',
  });
  layer.append(label);
})();

By preloading and caching the image, the sprite with id: 'robot' displays immediately and provides access to contentSize.

Sprite Sheets

Like CSS sprite sheets, Sprite.js supports sprite sheets, including standard JSON formats generated by tools like TexturePacker.

const { Scene, Sprite, Group } = spritejs;
const container = document.getElementById('adaptive');
const scene = new Scene({
  container,
  width: 1200,
  height: 600,
});
const layer = scene.layer();

(async function () {
  const earthPNG = 'https://p3.ssl.qhimg.com/t01806718065fe97c65.png';
  const earthJSON = 'https://s3.ssl.qhres2.com/static/d479c28f553c6cff.json';

  await scene.preload([earthPNG, earthJSON]);

  const group = new Group();
  group.attr({
    pos: [455, 215],
  });

  const earth = new Sprite();
  earth.attr({
    texture: 'earth_blue.png',
    pos: [115, 115],
    anchor: [0.5, 0.5],
  });
  const earthShadow = new Sprite();
  earthShadow.attr({
    texture: 'earth_shadow2.png',
    pos: [0, 0],
  });

  group.append(earth, earthShadow);
  layer.append(group);

  earth.animate([
    { rotate: 0, texture: 'earth_blue.png' },
    { rotate: 360, texture: 'earth_yellow.png' },
    { rotate: 720, texture: 'earth_green.png' },
    { rotate: 1080, texture: 'earth_white.png' },
    { rotate: 1440, texture: 'earth_blue.png' },
  ], {
    duration: 20000,
    iterations: Infinity,
  }).play();
})();

Transition Animations

The simplest animation in Sprite.js is the transition animation:

// Move sprite 50 pixels right in 1 second
sprite.transition(1.0).attr({
  x: x => x + 50,
});

sprite.transition(sec).attr(...) returns a promise, enabling sequential animations:

const { Scene, Sprite } = spritejs;
const container = document.getElementById('adaptive');
const scene = new Scene({
  container,
  width: 1200,
  height: 600,
});
const layer = scene.layer();

(async function () {
  const sprite = new Sprite();
  sprite.attr({
    anchor: 0.5,
    bgcolor: 'red',
    pos: [500, 300],
    size: [200, 200],
    borderRadius: 50,
  });

  layer.append(sprite);

  await sprite.transition(2.0).attr({
    bgcolor: 'green',
    width: width => width + 100,
  });

  await sprite.transition(1.0).attr({
    bgcolor: 'orange',
    height: height => height + 100,
  });
})();

sprite.transition(sec) returns a Transition object. Multiple attr() calls automatically end the previous transition, making effects like hover intuitive:

const { Scene, Sprite, Path, Group } = spritejs;
const container = document.getElementById('adaptive');
const scene = new Scene({
  container,
  width: 1200,
  height: 600,
});
const layer = scene.layer();

(async function () {
  await scene.preload([
    'https://p5.ssl.qhimg.com/t01f47a319aebf27174.png',
    'https://s3.ssl.qhres2.com/static/a6a7509c33a290a6.json',
  ]);

  const robot = new Sprite({ texture: 'guanguan1.png' });
  robot.attr({
    anchor: 0.5,
    pos: [710, 210],
    scale: 0.4,
    rotate: 45,
  });
  layer.append(robot);

  const d = 'M235.946483,75.0041277 C229.109329,53.4046689 214.063766,34.845093 195.469876,22.3846101 C175.428247,8.9577702 151.414895,2 127.314132,2 C75.430432,2 31.6212932,32.8626807 18.323944,74.9130141 C8.97646468,77.1439182 2,85.5871171 2,95.7172992 C2,104.709941 7.49867791,112.371771 15.2700334,115.546944 C15.8218133,115.773348 16.6030463,122.336292 16.8270361,123.236385 C22.1235768,144.534892 35.4236577,163.530709 52.5998558,176.952027 C52.6299032,176.976876 52.6626822,177.001726 52.6954612,177.026575 C72.5513428,192.535224 98.5478246,202 127.043705,202 C152.034964,202 176.867791,194.597706 197.428422,180.146527 C215.659011,167.335395 230.201962,148.621202 236.52831,126.969284 C237.566312,123.421373 238.549682,119.685713 239.038636,116.019079 C239.044099,115.983185 239.074146,115.444787 239.082341,115.442025 C246.673412,112.184022 252,104.580173 252,95.7172992 C252,85.6892748 245.15192,77.3371896 235.946483,75.0041277';
  const shadowD = 'M82.1534529,43 C127.525552,43 164.306906,33.6283134 164.306906,21.663753 C164.306906,9.6991926 127.525552,0 82.1534529,0 C36.7813537,0 0,9.6991926 0,21.663753 C0,33.6283134 36.7813537,43 82.1534529,43 Z';
  const shadow = new Path();
  shadow.attr({
    d: shadowD,
    normalize: true,
    fillColor: '#000000',
    opacity: 0.05,
    pos: [500, 434],
    scale: [1.3, 1.2],
  });
  layer.append(shadow);

  const lemon = new Path();
  lemon.attr({
    d,
    normalize: true,
    pos: [500, 300],
    fillColor: '#fed330',
    scale: 1.4,
  });
  layer.append(lemon);

  const lemonGroup = new Group();
  lemonGroup.attr({
    anchor: 0.5,
    pos: [610, 300],
    size: [180, 180],
    bgcolor: '#faee35',
    border: { width: 6, color: '#fdbd2c' },
    borderRadius: 90,
    scale: 1.5,
  });
  layer.append(lemonGroup);

  const d2 = 'M0,0L0,100A15,15,0,0,0,50,86.6z';
  for (let i = 0; i < 12; i++) {
    const t = new Path();
    t.attr({
      d: d2,
      scale: 0.65,
      lineWidth: 2,
      strokeColor: '#fff',
      fillColor: '#f8c32d',
      rotate: 30 * i,
    });
    lemonGroup.append(t);
  }

  lemonGroup.animate([
    { rotate: 360 },
  ], {
    duration: 10000,
    iterations: Infinity,
  }).play();

  const transition = robot.transition(0.3);

  lemonGroup.addEventListener('mouseenter', () => {
    layer.timeline.playbackRate = 3.0;
    transition.attr({
      pos: [730, 190],
    });
  });
  lemonGroup.addEventListener('mouseleave', () => {
    layer.timeline.playbackRate = 1.0;
    transition.attr({
      pos: [710, 210],
    });
  });
})();

Web Animations API

const { Scene, Sprite } = spritejs;
const container = document.getElementById('adaptive');
const scene = new Scene({
  container,
  width: 1200,
  height: 600,
});
const layer = scene.layer();

const sprite = new Sprite();
sprite.attr({
  anchor: [0.5, 0.5],
  pos: [600, 300],
  bgcolor: 'red',
  size: [50, 50],
  borderRadius: 25,
  translate: [0, -200],
  transformOrigin: [0, 200],
});

sprite.animate([
  { rotate: 0 },
  { rotate: 360 },
], {
  duration: 3000,
  iterations: Infinity,
}).play();

layer.append(sprite);

Animation States:

  • idle: Animation hasn’t started.
  • pending: Animation has started but the element hasn’t moved or has finished moving.
  • running: Animation is active.
  • paused: Animation is paused.
  • finished: Animation has completed.

Animation Timeline

Each Layer has a timeline property, a Timeline object that synchronizes all animations within the layer. Modifying the layer’s timeline affects all animations.

const { Scene, Sprite } = spritejs;
const container = document.getElementById('adaptive');
const scene = new Scene({
  container,
  width: 1200,
  height: 600,
});
const layer = scene.layer();

(async function () {
  await scene.preload({ id: 'snow', src: 'https://p5.ssl.qhimg.com/t01bfde08606e87f1fe.png' });

  const [speed1, speed2, speed4, halfSpeed, pause, reversePlay] =
    document.querySelectorAll('#speed1, #speed2, #speed4, #halfSpeed, #pause, #reversePlay');

  const timeline = layer.timeline;

  speed1.addEventListener('click', () => {
    timeline.playbackRate = 1.0;
  });

  speed2.addEventListener('click', () => {
    timeline.playbackRate = 2.0;
  });

  speed4.addEventListener('click', () => {
    timeline.playbackRate = 4.0;
  });

  halfSpeed.addEventListener('click', () => {
    timeline.playbackRate = 0.5;
  });

  pause.addEventListener('click', () => {
    timeline.playbackRate = 0;
  });

  reversePlay.addEventListener('click', () => {
    timeline.playbackRate = -1.0;
  });

  function addRandomSnow() {
    const snow = new Sprite({ texture: 'snow' });
    const x0 = 20 + Math.random() * 1100;
    const y0 = -100;

    snow.attr({
      anchor: [0.5, 0.5],
      pos: [x0, y0],
      size: [50, 50],
    });

    snow.animate([
      { x: x0 - 10 },
      { x: x0 + 10 },
    ], {
      duration: 1000,
      fill: 'forwards',
      direction: 'alternate',
      iterations: Infinity,
      easing: 'ease-in-out',
    }).play();

    const dropAnim = snow.animate([
      { y: -100, rotate: 0 },
      { y: 700, rotate: 360 },
    ], {
      duration: 10000,
      fill: 'forwards',
    }).play();

    dropAnim.then(() => {
      snow.remove();
    });

    layer.append(snow);
  }

  setInterval(addRandomSnow, 200);
})();

Third-Party Animation Libraries

Beyond Sprite.js’s built-in animations and Web Animations API, third-party libraries can enhance animation capabilities. Below are compatible libraries and integration examples.

GreenSock (GSAP)

GSAP is a high-performance animation library for DOM, SVG, and Canvas elements, ideal for complex animations.

import { Scene, Layer, Rect } from 'spritejs';
import gsap from 'gsap';

const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

const rect = new Rect({
  size: [100, 100],
  pos: [400, 300],
  fillColor: '#f00',
});
layer.append(rect);

gsap.to(rect.element, {
  x: 600,
  duration: 1,
  ease: 'power2.out',
});

scene.render();

anime.js

anime.js is a lightweight library for CSS, SVG, and JS objects, great for complex animation sequences.

import { Scene, Layer, Rect } from 'spritejs';
import anime from 'animejs';

const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

const rect = new Rect({
  size: [100, 100],
  pos: [400, 300],
  fillColor: '#f00',
});
layer.append(rect);

anime({
  targets: rect.element,
  translateX: 200,
  duration: 1000,
  easing: 'easeInOutQuad',
});

scene.render();

Popmotion

Popmotion supports CSS, SVG, and WebGL with physics simulations and interaction handling, suitable for dynamic effects.

import { Scene, Layer, Rect } from 'spritejs';
import { styler, tween } from 'popmotion';

const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

const rect = new Rect({
  size: [100, 100],
  pos: [400, 300],
  fillColor: '#f00',
});
layer.append(rect);

const domNode = rect.element;
const st = styler(domNode);

tween({
  from: { x: 0 },
  to: { x: 200 },
  duration: 1000,
  ease: 'easeInOut',
}).start((v) => st.set(v));

scene.render();

Sprite.js Image Processing

In Sprite.js, image processing includes loading and displaying images as sprites, cropping, and using nine-slice scaling techniques, all of which are essential for creating complex UIs and animations.

Loading and Displaying Images as Sprites

Loading an image and displaying it as a sprite is a fundamental operation.

const { Scene, Layer, Sprite } = spritejs;

const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

// Load image
const img = new Sprite({
  texture: 'path/to/your/image.png',
  size: [200, 90], // Optional: specify display dimensions
});

img.addEventListener('load', () => {
  // Add to layer after loading
  layer.append(img);

  // Set position
  img.attr({
    pos: [400, 300],
  });
});

scene.render();

Image Cropping

Image cropping displays only a portion of the original image. In Sprite.js, this is achieved using the clip attribute.

// Assuming img is loaded and added to the layer
img.attr({
  clip: { // Cropping region
    x: 50, // Left edge offset from image left
    y: 50, // Top edge offset from image top
    width: 100, // Cropped width
    height: 100, // Cropped height
  },
});

Nine-Slice Scaling Technique

Nine-slice scaling is commonly used in UI design to prevent distortion of corners when stretching images, ideal for buttons and dialog borders. Sprite.js doesn’t provide a direct nine-slice API, but you can simulate it by combining cropped images.

// Assuming img is a loaded nine-slice image, manually split and reassemble
const topLeft = new Sprite({ 
  texture: 'path/to/your/image.png',
  clip: { x: 0, y: 0, width: 50, height: 50 },
});
const topCenter = new Sprite({ 
  texture: 'path/to/your/image.png',
  clip: { x: 50, y: 0, width: 100, height: 50 },
});
// Define topRight, middleLeft, middleCenter, middleRight, bottomLeft, bottomCenter, bottomRight similarly

// Add to layer and position
layer.append(topLeft, topCenter /*, ...*/);
topLeft.attr({ pos: [0, 0] });
topCenter.attr({ pos: [50, 0] });
// Set positions for other images

// In practice, automating nine-slice creation may require complex logic or external tools to preprocess images.

Advanced Image Processing Techniques

Beyond basic loading and cropping, Sprite.js leverages WebGL for advanced processing, such as color adjustments and filter effects.

Color Adjustment with WebGL Filters

To modify an image’s hue or saturation, use a custom WebGL filter.

const { Scene, Layer, Sprite } = spritejs;

const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

const img = new Sprite({ texture: 'path/to/your/image.png' });

// Define color adjustment filter
const colorAdjustFilter = {
  type: 'custom',
  fragmentShader: `
    precision mediump float;
    varying vec2 v_texCoord;
    uniform sampler2D u_texture;
    uniform float hueShift;
    uniform float saturation;

    vec3 rgb2hsv(vec3 c) {
      vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
      vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
      vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));

      float d = q.x - min(q.w, q.y);
      float e = 1.0e-10;
      return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
    }

    vec3 hsv2rgb(vec3 c) {
      vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
      vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
      return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
    }

    void main() {
      vec4 color = texture2D(u_texture, v_texCoord);
      vec3 hsv = rgb2hsv(color.rgb);
      hsv.x += hueShift;
      hsv.y *= saturation;
      vec3 rgb = hsv2rgb(hsv);
      gl_FragColor = vec4(rgb, color.a);
    }
  `,
  uniforms: {
    hueShift: 0.1,
    saturation: 1.2,
  },
};

img.filters = [colorAdjustFilter];

img.addEventListener('load', () => {
  layer.append(img);
  img.attr({ pos: [400, 300] });
});

scene.render();

Dynamic Textures and Real-Time Updates

Some scenarios require dynamically updating image content, such as video streams as textures or data-driven charts. Sprite.js supports this through WebGL texture updates.

Video as Texture

const { Scene, Layer, Sprite } = spritejs;

const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

const video = document.createElement('video');
video.src = 'path/to/your/video.mp4';
video.autoplay = true;
video.loop = true;

const videoSprite = new Sprite();
videoSprite.attr({ texture: video });

video.addEventListener('canplay', () => {
  layer.append(videoSprite);
  videoSprite.attr({ pos: [400, 300], size: [600, 400] });
});

scene.render();

Real-Time Texture Updates

For dynamic updates based on real-time data, modify texture data indirectly via WebGL APIs, as Sprite.js doesn’t expose direct texture manipulation.

// Conceptual example; actual implementation requires WebGL expertise
const gl = layer.context; // Get WebGL context
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

// Populate texture data (simplified; real-world cases use dynamic data)
const imageData = new ImageData(width, height);
// Update imageData.data based on real-time data

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imageData);

// Apply texture to a Sprite.js element (requires custom rendering or WebGL integration)

Sprite.js Event Handling

Basic Concepts

Sprite.js supports various event types, including but not limited to:

  • click: Click event.
  • mousedown, mouseup: Mouse press and release events.
  • mousemove: Mouse movement event.
  • touchstart, touchmove, touchend: Touchscreen equivalents.
  • mouseenter, mouseleave: Mouse entering or leaving an element’s area.
  • mouseover, mouseout: Similar to mouseenter and mouseleave, but with bubbling behavior.

Event Binding

Events can be bound to a Scene, Layer, or specific graphical elements using the on method. The event handler receives an event object containing detailed event information.

Event Propagation

  • Capture Phase: The event propagates from the root node down to the target element.
  • Target Phase: The event reaches the target, triggering its listeners.
  • Bubbling Phase: The event bubbles up from the target to the root, triggering listeners on parent elements.

Click Event Handling

const { Scene, Layer, Rect } = spritejs;

const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

const rect = new Rect({
  size: [100, 100],
  pos: [400, 300],
  fillColor: '#f00',
});

// Bind click event
rect.on('click', (evt) => {
  console.log('Rect clicked!');
  // evt.stopPropagation(); // Uncomment to prevent bubbling
});

layer.append(rect);

scene.render();

Touch Event Handling

const { Scene, Layer, Rect } = spritejs;

const scene = new Scene('#container', { width: 800, height: 600, mode: 'stickyTouch' }); // Optimizes touch experience
const layer = scene.layer();

const rect = new Rect({
  size: [100, 100],
  pos: [400, 300],
  fillColor: '#f00',
});

// Bind touchstart event
rect.on('touchstart', (evt) => {
  console.log('Rect touched!');
});

layer.append(rect);

scene.render();

Event Delegation

To handle events for multiple child elements efficiently, bind a listener to a parent element and inspect the event target. This reduces the number of listeners.

// Assuming layer contains multiple Rects
layer.on('click', (evt) => {
  if (evt.target instanceof Rect) {
    console.log('A rect was clicked:', evt.target);
  }
});

Notes:

  • Event Naming: Ensure event names follow W3C standards.
  • Performance: Excessive listeners can impact performance; use delegation when appropriate.
  • Cross-Platform: Account for differences in touch events across desktop and mobile devices.

Listening for Click Events

Ensure Sprite.js is included, and a basic scene and layer are set up. Click events can then be attached to any graphical element (e.g., rectangles, circles, images).

Listening for a Rectangle’s Click Event

const { Scene, Layer, Rect } = spritejs;

// Create scene and layer
const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();

// Create red rectangle
const rect = new Rect({
  size: [100, 100],
  pos: [400, 300],
  fillColor: '#ff0000',
});

// Add rectangle to layer
layer.append(rect);

// Listen for click event
rect.on('click', (evt) => {
  console.log('Rect was clicked!');
  rect.attr('fillColor', '#00ff00'); // Change color on click
});

// Render scene
scene.render();

Event Handling Details

  • Event Object: The evt parameter contains event details, such as evt.target, which identifies the triggered element.
  • Handler Function: Execute any logic inside, like updating attributes, triggering animations, or calling functions.
  • Binding Location: Bind events directly to elements for efficiency, or to Layer/Scene for broader monitoring.

Advanced Techniques for Interactive Graphics

  • State Management: Track states (e.g., clicked status) to modify appearance or behavior in handlers.
  • Event Delegation: For composite graphics, bind listeners to parents and use evt.target to identify children, reducing listener count.
  • Animation Feedback: Use Sprite.js animations for visual feedback on clicks, like translation, scaling, or color transitions.
  • Touch Optimization: Use touchstart instead of click for touch devices and enable stickyTouch mode for better responsiveness.

Share your love