Package Exports
- @niuee/board
- @niuee/board/index.cjs
- @niuee/board/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@niuee/board) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
board
This project is experimental (only to show a way to have a canvas that can be panned, zoomed, and rotated) please do not use it in production until you modify it to your needs, test it out, and make sure that it suits all your needs
Demo The demo version is rather old and outdated but it showcases the basic functionalities so I am keeping it here. I will replace it with the newer version later on when it's stable
Initially, I needed a way to have a pannable and zoomable canvas for a racetrack maker project that I was working on. I stumbled upon a codepen example that is posted in stackoverflow. It was a fantastic starting point. It provided everything that I need. However, some functionalities I needed was not there; I started to build on top of this and make a canvas element that is easier to work with and modify and has the functionalities that I need.
This project is written entirely with vanilla JavaScript (with TypeScript). However, in the future I plan to have a React version of this and maybe support other frontend libraries and/or frameworks.
It's using the web component api and supports Safari, Chrome, and Firefox as for the eariliest version of each browser I would need time to investigate more on that. Webkit does not support extending from existing HTMLElement like HTMLCanvasElement, thus the canvas element is inheriting from just simple HTMLElement with an internal canvas element attached to the shadow dom.
The panning, zooming, rotating supports keyboard-mouse, trackpad, and also touch on iOS and iPadOS devices. I haven't tested this with screen that has touch capability. Android devices are not tested either.
The design document contains a more detailed explanation of the underlying mechanisms of the canvas including the camera system that is essentially the backbone of this custom canvas.
v0.0.1 -> v0.0.2
The default export for this package is changed to a Board
class. This is different from the original class used for custom element. To use the custom element class use the named export
import { BoardElement } from "@niuee/board";
or the default export of the sub directory in the pacakge. (If you want to keep the name Board
in your source code)
import Board from "@niuee/board/board-element";
Installation
You can install the package from npm using
npm install @niuee/board
Or you can just use the files in the release and import it directly
They are in the release tab of this repo. (both the minified JavaScript file and the map)
Usage
There are two ways to use this package. One is to use the custom element BoardElement
class. This is the custom element however I can't quite to sort out the issue where the style and dimensions of the custom element not lining up with the canvas element attached to the shadow dom. Therefore, I have an alternative and is the recommended way to use this package: to use a Board class to provide the extended functionalities and leave the HTML element and CSS to the html and CSS files.
The BoardElement
way.
Just like any other web component, you have to define the custom element first.
// you can put the name that you want in here; but you have to make sure that it's more than one word (two words and up) and with dash(es) in between otherwise it won't work. This is the constraint imposed by web component
import { BoardElement } from "@niuee/board";
customElements.define('tag-name-you-desire', BoardElement);
Then in a html file you can use it like this.
<tag-name-you-desire></tag-name-you-desire>
To operate on the BoardElement with JavaScript
const boardElement = document.querySelector("tag-name-you-desire") as BoardElement
// do what you want to do with boardElement; detailed example in later sections
The Board
way.
The Board
class constructor takes in a canvas element as an argument to add extended funcionalities.
import Board from "@niuee/board"
const canvasElement = document.querySelect("canvas");
const board = new Board(canvasElement);
Coordinate System
I know the coordinate system for the native canvas html element down is positive y and right is positive x. However, I decided to flip the y-axis around to make up positive y and right positve x. In the demo I have drawn the x and y axis on the canvas element. Green is the y axis and the extending direction is the positive direction. Red is the x axis. The coordinate of the rendering context is still down for positive y and right for positive x. It's just when a user clicked on the canvas, the coordinate that is reported is in up for positive y and right for positive x coordinate system.
panning
Keyboard-Mouse: ⌘ + Hold Down Left Mouse Button or on windows ⊞ + Hold Down Left Mouse Button or Hold the wheel button.
Trackpad: Two fingers swipe to pan
Touch: Two fingers swipe to pan (plan to change it to one finger drag to pan)
zooming
Keyboard-Mouse: scroll the wheel while holding down the control key, (zoom is anchor to the cursor position)
Trackpad: Two fingers pinch to zoom
Touch: Two fingers pinch to zoom
rotating
Rotating is a little more complicated because I couldn't figure out an intuitive way to directly rotate the canvas. However, the functionality is there. I'll explain in more detail in a later section.
RequestAnimationFrame
HTML Canvas is essentially a static image. To make it look like an actual canvas that you can manipulate and mess around. It relies on the requestAnimationFrame function. Clearing the canvas and redrawing at each frame so that it looks like the canvas is actually moving like an animation.
Currently, the default is that the canvas will not call the requestAnimationFrame itself. It relies on the user to call requestAnimationFrame for it. I will demonstrate it below.
Get the step function of the canvas.
You can think of the step function similar to the render
function of a renderer
from threejs which you have to call in the animate
function in this example. The step function takes in an argument timestamp: number
; you can get this directly as the arugment of the call back function passed into requestAnimationFrame
. You can also just use the step function as the callback passed into requestAnimationFrame
; but this way you would not be able to do much stuff with the canvas. To get the step
function simply call the getStepFunction()
from the canvas element like this.
const canvasElement = document.querySelector("tag-name-you-desire") as BoardElement; // this is the tag name that you assign to the custom element; or you can assign id to the tag and select using id
const stepFunction = canvasElement.getStepFunction(); // canvas element is of type vCanvas you can get it using the queryselector
const context = canvasElement.getContext(); // this is the drawing context for the canvas element
// this is the step function that wraps the canvas step function so you can also do stuff at each frame
function step(timestamp: number){
// make sure to step the canvas element first otherwise your stuff is going to get wiped when the canvas element steps
stepFunction(timestamp);
// this is an example of drawing a circle
context.beginPath();
context.arc(200, -100, 5, 0, Math.PI * 2);
context.stroke();
// call the requestAnimationFrame to keep stepping
window.requestAnimationFrame(step);
}
// remember to call the function to start
step(0);
The Board
way is very similar.
const board = new Board(canvasElement); // instantiate a Board object with a canvas element
const stepFunction = board.getStepFunction(); // canvas element is of type vCanvas you can get it using the queryselector
const context = board.getContext(); // this is the drawing context for the canvas element
// this is the step function that wraps the canvas step function so you can also do stuff at each frame
function step(timestamp: number){
// make sure to step the canvas element first otherwise your stuff is going to get wiped when the canvas element steps
stepFunction(timestamp);
// this is an example of drawing a circle
context.beginPath();
context.arc(200, -100, 5, 0, Math.PI * 2);
context.stroke();
// call the requestAnimationFrame to keep stepping
window.requestAnimationFrame(step);
}
// remember to call the function to start
step(0);
Attributes
restrict-{x-translation | y-translation | rotation | zoom}
This is to restrict any kind of input from the gestures (mouse-keyboard input, trackpad gesture, touch points) to move, rotate, or zoom the canvas.
For Example, to limit the absolute x direction translation set the attribute restrict-x-translation on the html tag.
This will restrict the ability to pan the canvas in x-direction.
The BoardElement
way. It's to add an attribute to the html custom element.
<canvas-board restrict-x-translation></canvas-board>
The Board
way. It's to just set the property restrictXTranslation
to true
.
board.restrictXTranslation = true;
restrict-relative-{x-translation | y-translation}
This is to restrict any kind of input from the gestures (mouse-keyboard input, trackpad gesture, touch points) to move relative to the camera viewport. X is the left and right direction of the view port and Y is the up and down direction.
The BoadElement
way. It's to add an attribute to the html custom element.
<canvas-board restrict-relative-x-translation></canvas-board>
The Board
way. It's to just set the property restrictYTranslation
to true
.
board.restrictRelativeYTranslation = true;
full-screen
This is to set the dimensions of the canvas to be the same as window.innerHeight
and window.innerWidth
.
This will override the width
and height
attribute.
<canvas-board full-screen></canvas-board>
width
The BoardElement
way.
This is to set the width of the canvas.
<canvas-board width="300"></canvas-board>
The Board
way.
This will change the width of the canvas element.
The Board class instance also listens to the attribute change of the underlying canvas element. So if you can also change the width of the canvas element; the board class instance will take care of the change under the hood.
board.width = 300;
OR
<canvas width="300"></canvas>
height
The BoardElement
way.
This is to set the height of the canvas.
<canvas-board height="300"></canvas-board>
The Board
way.
This is similar to the width attribute. The board also listens to the height change of the canvas element it controls; you can change the height using JavasScript or directly in html.
board.height = 300;
OR
<canvas height="300"></canvas>
control-step
This is to prevent the canvas from calling the window.requestAnimationFrame
automatically. Default is "true"(meaning that the canvas element would not call rAF itself the user would have to "control the step function"; I know it's kind of confusing I am still working on the name though)
The BoardElement
way.
<canvas-board control-step="false"></canvas-board>
Setting this attribute to "false"(string as attribute value can only be string), the canvas would handle the calling of rAF and the user would just get the pan, zoom, and rotate functionality automatically. However, in this mode you would probably have to go into the source code of the canvas and add stuff to the step function to actually acheive anything.
The Board
way.
board.stepControl = false;
debug-mode
This would switch on the debug mode for the canvas. Currently, the debug mode is drawing the reference circle in green, the axis in their respective color, the bounding box in blue. The cursor icon would be replaced with a red crosshair and at the top right to the crosshair would be the position of the cursor in world coordinate.
The BoardElement
way.
<canvas-board debug-mode></canvas-board>
The Board
way.
board.debugMode = true;
max-half-trans-width
This is to set the horizontal boundaries for the viewport. (where the camera can move to) Currently, the boundaries are set mirrored at the origin. Hence the "half" in the attribute name. Left and right both gets the same value. The entire horizontal boundary is then 2 * half width wide.
The BoardElement
way.
<!-- This would set the entire horizontal boundary of the camera to be 2000-->
<canvas-board max-half-trans-width="1000"></canvas-board>
The Board
way
board.maxHalfTransWidth = 1000;
max-half-trans-height
This is to set the vertical boundaries for the viewport. Currently, the boundaries are set mirrored at the origin. Hence the "half" in the attribute name. Top and bottom both gets the same value. The entire vertical boundary is then 2 * half width wide.
The BoardElement
way.
<!-- This would set the entire vertical boundary of the camera to be 2000-->
<canvas-board max-half-trans-height="1000"></canvas-board>
The Board
way.
board.maxHalfTransHeight = 1000;
grid
This is to toggle the grid displayed on the canvas. The spacing currently is not adjustable; it is the same as the ruler (it flexible depending on the zoom).
The BoardElement
way.
<canvas-board grid></canvas-board>
The Board
way.
board.displayGrid = true;
ruler
This is to toggle the ruler displayed on the canvas. The spacing depends on the zoom level.
The BoardElement
way.
<canvas-board ruler></canvas-board>
The Board
way.
javascript board.displayRuler = true;
Listen to the event of panning, zooming, rotating movement
This is one of the revamped feature of the canvas. The rotation of the canvas needed to be controlled by an external element. That element would have to sync up with the rotation of the canvas. This was originally done through custom event; the canvas orientation would be dispatch through custom events at every frame even if the canvas is stationary. The mechanism in place now is to set up an event listener just like before but the canvas would only report the current orientation when the canvas is moved in any way.
Pan Event
To listen to the pan event of the canvas.
The BoardElement
way.
// the pan call back
function panCallback(event, cameraState) {
// payload for pan event would contain the diff of the canvas pan
// cameraState would be the current state of the abstracted camera
console.log(event); // {diff: {x: number, y: number}}
console.log(cameraState) // {position: Point, rotation: number, zoomLevel: number}
}
// listen to the pan event and when a pan event occur the callback would execute
canvasElement.on("pan", panCallback);
// pan event payload looks like
// {
// diff: Point;
// }
// cameraState looks like
// {
// position: Point;
// rotation: nubmer;
// zoomLevel: number;
// }
The Board
way.
It's not too different from the BoardElement
way. It's just the .on
function is called on the Board
class instance.
// the pan call back
function panCallback(event, cameraState) {
// payload for pan event would contain the diff of the canvas pan
// cameraState would be the current state of the abstracted camera
console.log(event); // {diff: {x: number, y: number}}
console.log(cameraState) // {position: Point, rotation: number, zoomLevel: number}
}
// listen to the pan event and when a pan event occur the callback would execute
board.on("pan", panCallback);
// pan event payload looks like
// {
// diff: Point;
// }
// cameraState looks like
// {
// position: Point;
// rotation: nubmer;
// zoomLevel: number;
// }
Rotate Event
To Listen to the rotate event of the canvas.
The BoardElement
way.
// the rotate call back
function rotateCallback(event, cameraState) {
console.log(event);
console.log(cameraState) // {position: Point, rotation: number, zoomLevel: number}
}
// listen to the rotate event and when a rotate event occur the callback would execute
canvasElement.on("rotate", rotateCallback);
// rotate event payload looks like
// {
// deltaRotation: number;
// }
The Board
way.
board.on("rotate", rotateCallback);
Zoom Event
To Listen to the zoom event of the canvas.
The BoardElement
way.
// the zoom call back
function zoomCallback(event, cameraState) {
console.log(event);
console.log(cameraState) // {position: Point, rotation: number, zoomLevel: number}
}
// listen to the zoom event and when a zoom event occur the callback would execute
canvasElement.on("zoom", zoomCallback);
// zoom event payload looks like
// {
// deltaZoomAmount: number;
// anchorPoint: Point;
// }
The Board
way.
javascript board.on("zoom", zoomCallback);
Controlling the Camera
Currently, there is no function of the Board
class nor the BoardElement
class that directly control the underlying camera system. However, you can get the underlying BoardCamera
instance using the board.getCamera()
and the boardElement.getCamera()
functions to get the internal BoardCamera
object.
Once you have the boardCamera
you can do a lot of things.
Command Conventions
Set or Move, (Set or spin for rotation operation), (Not applicable to zoom operation)
Set directly set the position of the camera. It takes in the destination as argument. Move takes in the delta as an argument making it suitable for when you don't care the current position of the camera.
FromGesture or not
FromGesture is affected by the restrictions (restrictXTranslation, etc.) The design document has a detailed relationship between different kinds of camera commands.
LimitEntireViewPort or not
If limiting the entire view port the entire view port would be contrained within the boundaries not just the center of the view port (or the camera position). I would post a gif demonstrating the difference.
Clamp or not
If there is no clamping then the command would not take effect if the destination is out of bounds. There is a graph in the design document.
For example you can call the moveWithClampFromGesture(delta: {x: number, y: number})
to "move" the camera to a position clamped inside the boundaries from gesture so that the restrictions imposed would limit this command.