npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@guinetik/gcanvas

v1.0.5

Published

A batteries-included 2D canvas framework for games and generative art

Readme

GCanvas 🎨

demo screenshot

A minimalist 2D canvas rendering library built for learning, expression, and creative coding.

Inspired by the simplicity of p5.js and the composability of game engines — GCanvas gives you structured primitives, a game loop, interactivity, and a growing set of intuitive, declarative shapes.


What is this?

GCanvas is a modular 2D rendering and game framework built on top of the HTML5 Canvas API.

At its core, it's split into two powerful layers:

  • 🎨 Shape Layer — for clean, declarative drawing via transformable primitives like Circle, Rectangle, and Group.
  • 🎮 Game Layer — for interactive, updatable GameObjects managed through a pipeline and real-time loop.

You can use GCanvas purely for visuals, or build full simulations, games, or creative tools on top.


🌟 Features

  • 💎 Primitives like Circle, Line, Rectangle, Star, Polygon, and more
  • 2.5D Shapes like Cube, Cylinder, Prism, Sphere, Cone
  • 📦 Groups for transformable shape collections
  • 📈 Transforms — rotation, scale, constraints, group nesting
  • 🖌️ Painter API for imperative canvas control
  • 👾 GameObjects for updatable, interactive entities
  • 📱 UI Components like Button, ToggleButton, Cursor, and layout managers
  • 🎞️ Motion API for game-time-driven animations
  • 📅 Tweenetik for UI-style, self-running tweens
  • 🏘 Event system — hover, click, mouse, and touch
  • 🚀 Works with Vite, Rollup, or standalone

📊 Demo

Open https://gcanvas.guinetik.com or run:

npm run dev

🔢 API Docs

Each shape and utility is JSDoc-annotated. I'm organizing a docs folder on Github Pages. For now, you can do npm run docs to generate the docs site.

TypeScript Support

GCanvas includes TypeScript definitions (gcanvas.d.ts) for full IDE intellisense and type checking. The types are automatically included when you install the package.

🧑‍💻 Installation

NPM (Recommended):

npm install @guinetik/gcanvas

Or clone this repo:

git clone https://github.com/guinetik/gcanvas.git
cd gcanvas
npm install

To run our tech demos:

npm run dev

To build the library:

npm run build

To generate a readable single-file debug build:

npm run build:debug

🚀 Quick Start

Using via NPM:

import { Circle, Painter } from '@guinetik/gcanvas';

Painter.init(ctx);
const circle = new Circle(50, { x: 100, y: 100, color: 'red' });
circle.draw();

Using via ESM (standalone):

import { Circle } from './dist/gcanvas.es.min.js';

const circle = new Circle(50, { x: 100, y: 100, color: 'red' });
circle.draw();

Using via inline script:

<script src="./dist/gcanvas.umd.min.js"></script>
<script>
  const circle = new GCanvas.Circle(200, 200, 40, { fillColor: 'blue' });
  circle.draw();
</script>

🧠 Core Architecture

Our engine follows a layered architecture that builds from simple drawing primitives up to interactive game objects and scenes.

🖌️ Painter Layer

At the foundation is Painter - a wrapper around the canvas context that centralizes all drawing operations. Instead of working with the Canvas API directly, we use Painter as an intermediate layer:

// Painter is initialized with a canvas context
Painter.init(ctx);

// Now drawing operations use Painter's methods
Painter.shapes.circle(100, 100, 50, { fillColor: "red" });
Painter.shapes.rect(200, 200, 80, 40, { strokeColor: "blue" });

🔄 Transformable Properties

Next we have Transformable, which provides standard properties for positioning and displaying objects:

  • Position (x, y)
  • Size (width, height)
  • Visual properties (opacity, visibility)
  • Transformations (rotation, scaleX, scaleY)

Every visual element in the engine inherits from Transformable.

💎 Shape Layer

The Shape Layer is built on a hierarchy of classes that provide increasingly sophisticated rendering capabilities:

  1. Euclidian - The base class that defines spatial properties (x, y, width, height)
  2. Geometry2d - Adds bounding box calculations and constraints
  3. Transformable - Introduces rotation and scaling capabilities
  4. Renderable - Adds visual properties like opacity and visibility
  5. Shape - The final layer that adds styling (fill, stroke, line width)

Each layer uses underscore-prefixed private fields (_property) for encapsulation, following a consistent pattern throughout the codebase:

class Shape extends Transformable {
  constructor(options = {}) {
    super(options);
    this._fillColor = options.fillColor ?? null;
    this._strokeColor = options.strokeColor ?? null;
    this._lineWidth = options.lineWidth ?? 1;
  }

  get fillColor() { return this._fillColor; }
  set fillColor(v) { this._fillColor = v; }
}

The engine provides standard shapes like Circle, Rectangle, Star, etc., all built on this foundation.

📦 Group

Group is a special shape that contains other shapes. It applies its transforms to all children, allowing composite visuals to move and transform as a unit:

const group = new Group(400, 300);
group.add(new Circle(0, 0, 40, { fillColor: "red" }));
group.add(new Rectangle(0, 60, 80, 30, { fillColor: "blue" }));

// All shapes draw relative to the group position
group.draw();

🪜 Pipeline

The Pipeline manages collections of objects that need to be updated and rendered each frame. It handles:

  • The update/render sequence
  • Addition/removal of objects
  • Input event dispatch to the proper objects
// Create and add objects to the pipeline
pipeline.add(player);
pipeline.add(enemy);

// Pipeline handles updates in sequence
pipeline.update(dt);
pipeline.render();

🕹️ Game

The Game class brings everything together with:

  • Canvas management
  • Animation frame timing
  • Input system initialization
  • Pipeline management
  • Motion system integration

It's the core engine that drives everything:

const game = new Game(canvasElement);
game.init();  // Set up your game
game.start(); // Start the game loop

☄️ GameObject

GameObject extends Transformable and adds lifecycle methods:

  • update(dt) — Run game logic each frame
  • render() — Optional custom rendering
  • Event handling through enableInteractivity(shape)

This is the base class for all interactive entities:

class Player extends GameObject {
  constructor(game) {
    super(game);
    this.shape = new Circle(100, 100, 40, { fillColor: "blue" });
    this.enableInteractivity(this.shape);
  }
  
  update(dt) {
    // Move based on input
    if (this.game.input.isKeyDown('ArrowRight')) {
      this.x += 200 * dt;
    }
  }
  
  render() {
    this.shape.draw();
  }
  
  onPointerDown(e) {
    console.log('Player clicked!');
  }
}

🖼️ Scene

Scene is a special GameObject that manages child GameObjects:

  • Organizes objects into logical groups
  • Handles nested input events
  • Manages coordinate systems for children
// Create a level scene
const level = new Scene(game);
level.add(new Player(game));
level.add(new Enemy(game));

// Add scene to the game
game.pipeline.add(level);

📱 UI System

Built on top of GameObject and Scene, the UI system provides:

  • Button — Clickable elements with visual states
  • ToggleButton — On/off toggleable buttons
  • Cursor — Custom cursor shapes
  • Layout containers for automatic positioning:
    • HorizontalLayout
    • VerticalLayout
    • TileLayout
const ui = new Scene(game);
const menu = new VerticalLayout(game, { 
  spacing: 20,
  align: "center" 
});

menu.add(new Button(game, "Play"));
menu.add(new Button(game, "Options"));
menu.add(new Button(game, "Quit"));

ui.add(menu);
game.pipeline.add(ui);

All UI elements integrate with the animation systems for smooth transitions and effects.

Building with Layers

This architecture lets you work at the appropriate level of abstraction:

  1. Simple visuals: Use Shape and Group directly
  2. Interactive elements: Create GameObjects
  3. Organized worlds: Use Scenes to structure content
  4. Complete games: Subclass Game with your own logic
  5. Rich interfaces: Add UI elements for player interaction

Each layer builds on the ones below, creating a flexible and powerful framework for 2D canvas games.


🎞️ Motion System

The framework provides standard easing functions along with two complementary animation systems:

1. Motion - Stateless animation primitives

The Motion system provides mathematical animation functions that calculate positions based on time input. These functions don't modify objects directly - instead they return values that you apply in your game logic.

class Asteroid extends GameObject {
  constructor(game) {
    super(game);
    this.shape = new Circle(0, 0, 40, { fillColor: "#555" });
    this.time = 0;
  }
  
  update(dt) {
    this.time += dt;
    
    // Get position from a spiral pattern
    const result = Motion.spiral(
      400, 300,           // center point
      50, 200,            // start and end radius
      0, 3,               // start angle, revolutions
      this.time, 5,       // current time, duration
      true,               // loop
      Easing.easeInOutSine
    );
    
    // Apply the result to our shape
    this.shape.x = result.x;
    this.shape.y = result.y;
  }
  
  render() {
    this.shape.draw();
  }
}

Motion functions:

  • bezier - Follows along a cubic bezier curve.
  • bounce - Objects that drop and bounce with decreasing height
  • float - Natural floating motion with randomness
  • follow - Follow paths of waypoints with turning
  • orbit - Circular or elliptical orbits
  • oscillate - Values that cycle between min and max
  • parabolic - Arcing projectile motion
  • patrol - Entities that wander within bounds
  • pendulum - Swinging motion around a pivot (cosine)
  • pulse - Values that pulse up and down
  • shake - Camera or object shake effects
  • spiral - Spiral paths with variable radius
  • spring - Physics-based spring motion
  • swing - Rotational swinging (sine)
  • waypoint - Character movement between points

Each function returns standardized results with position, rotation, and animation metadata like progress and completion state.

Key advantages:

  • Deterministic (same input time = same output)
  • No state to track between frames
  • Compatible with any rendering system
  • Composable through grouping and sequencing

2. Tweenetik - Property animation

The Tweenetik system animates object properties directly over time using easing functions:

// Animate a button when pressed
onPointerDown() {
  Tweenetik.to(this.shape, 
    { scaleX: 1.2, scaleY: 1.2 }, 
    0.2, 
    Easing.easeOutBack,
    {
      onComplete: () => {
        Tweenetik.to(this.shape, 
          { scaleX: 1.0, scaleY: 1.0 }, 
          0.3, 
          Easing.easeInOutQuad
        );
      }
    }
  );
}

Tweenetik is ideal for:

  • UI animations and transitions
  • Simple property tweens
  • Sequential animations with callbacks

All animations integrate with the game's update/render cycle. Tweenetik is automatically updated by the Pipeline, while Motion functions are called directly in your GameObject's update method.

3. Easing - Timing functions

Both systems use the same easing functions for consistent animation curves:

Easing.easeInQuad      // accelerating
Easing.easeOutBounce   // bouncy ending
Easing.easeInOutCubic  // smooth acceleration and deceleration
// ...and many more

These functions transform linear time (0-1) into curved progressions for natural motion.

🧩 Mixins

Mixins are behavior modules that add optional functionality to any GameObject. They're lightweight and composable: apply only what you need.

Current mixins include:

  • applyAnchor(go, options) — anchors the GameObject to a screen position like "top-left", "bottom-right", etc. Automatically repositions on resize.
  • applyDraggable(go, shape, options) — enables drag-and-drop behavior with pointer support and optional friction.

Example:

applyAnchor(myUIElement, { anchor: "top-right", padding: 20 });
applyDraggable(myCard, myCard.shape);

You can combine multiple mixins to build rich UI/GameObject interactions without subclassing.

Mixins can also be passed as options:

// Add FPS counter
this.scene.add(
    new FPSCounter(game, {
        anchor: "bottom-right",
    })
);

...

🎓 Example

Hello World

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Basic Game Template</title>
</head>
<body>
  <canvas id="game"></canvas>
  <script type="module">
    import { Game, Scene, GameObject, FPSCounter, Rectangle, TextShape, Group } from "gcanvas";
    class HelloWorld extends Game {
      constructor(canvas) {
        super(canvas);
        this.enableFluidSize();
        this.backgroundColor = "black";
      }
      init() {
        // Set up scenes
        this.scene = new Scene(this);
        this.ui = new Scene(this);
        this.pipeline.add(this.scene); // game layer
        this.pipeline.add(this.ui);    // UI layer
        // Hello World box in the game scene
        this.scene.add(new HelloWorldBox(this));
        // FPS counter in the UI scene
        this.ui.add(new FPSCounter(this, { anchor: "bottom-right" }));
      }
    }

    //A Simple game object with a Shape and a Text box
    class HelloWorldBox extends GameObject {
      constructor(game) {
        super(game);

        const box = new Rectangle(0, 0, 200, 80, {
          fillColor: "#111",
          strokeColor: "#0f0",
          lineWidth: 2,
        });

        const label = new TextShape(0, 0, "Hello World!", {
          font: "18px monospace",
          color: "#0f0",
          align: "center",
          baseline: "middle"
        });

        this.group = new Group(game.width / 2, game.height / 2);
        this.group.add(box);
        this.group.add(label);
      }
      /**
       * Render the Group 
       */
      render() {
        this.group.draw();
      }
    }

    window.addEventListener("load", () => {
      const canvas = document.getElementById("game");
      const game = new HelloWorld(canvas);
      game.init();
      game.start();
    });
  </script>
</body>

</html>

🌟 GameObject with Shape

class Bob extends GameObject {
  constructor(game) {
    super(game);
    this.shape = new Circle(100, 100, 40, { fillColor: "tomato" });
    this.enableInteractivity(this.shape);
  }

  update(dt) {
    this.shape.x += Math.sin(Date.now() * 0.001) * dt * 60;
  }

  render() {
    this.shape.draw();
  }
}

If you want to quickly wrap a Shape in a GameObject without writing a full class, you can use ShapeGOFactory.create(...):

const shape = new Rectangle(0, 0, 100, 50, { fillColor: "lime" });
const go = ShapeGOFactory.create(game, shape);
game.pipeline.add(go);

This creates a GameObjectShapeWrapper behind the scenes that keeps the transform in sync and draws the shape on each frame.

💫 Spinning Shape on Hover

class SpinningShape extends GameObject {
  constructor(game) {
    super(game);
    this.shape = new Circle(200, 200, 50, { fillColor: 'cyan' });
    this.enableInteractivity(this.shape);
    this.hovered = false;

    this.on('mouseover', () => this.hovered = true);
    this.on('mouseout', () => this.hovered = false);
  }

  update(dt) {
    if (this.hovered) this.shape.rotation += 2 * dt;
  }

  render() {
    this.shape.draw();
  }
}

📂 File Structure

src/
├── shapes/             # All shape definitions (Circle, Triangle, Cube, etc.)
├── motion/             # Motion & Tweening systems
├── io/                 # Mouse, Touch, Input
├── game/               # Game loop, GameObject, Pipeline
├── painter.js          # Canvas context abstraction
├── index.js            # Public API entry point

"jsdoc": "^4.0.4"
"better-docs": "^2.7.3"