In this guide, you will discover most of the features of the Pencil.js library.
It assumes basic knowledge of web technologies (HTML, JavaScript, NPM …).
By the end of the page, you will be able to build your own creations with ease.
First of all, you need to add the library to your page.
You can quickly do so by using a CDN:
<script src="https://unpkg.com/pencil.js"></script>
<!-- or -->
<script src="https://cdn.jsdelivr.net/npm/pencil.js"></script>
Or you can load it with NPM:
$ npm install pencil.js
import Pencil from "pencil.js";
To test, if it was loaded correctly, you can log its version to the JavaScript console of your browser:
console.log(Pencil.version);
The first and main element of your code should be a {Scene}. A scene uses the whole screen by default, but can be bound to a specific node.
const myFirstScene = new Pencil.Scene();
// or
const wrapper = document.querySelector("#wrapper");
const myFirstScene = new Pencil.Scene(wrapper);
If you only need a still frame, you can render it once.
myFirstScene.render();
Or, you can make it alive by refreshing every frame.
myFirstScene.startLoop();
A {Container} allows you to group elements together.
Every class inherits from {Container}, which means that you can insert elements into each other by using the .add
function.
const child = new Pencil.Container();
myScene.add(child);
child.add(new Pencil.Container(), new Pencil.Container());
You can also remove them easily.
child.delete();
// or
myScene.remove(child);
Furthermore, a {Container}'s visibility can be toggled without having to remove it.
child.hide();
child.show();
Each {Container} has a list of options, you can set. Here is that list and the default values:
const containerOptions = {
shown: true,
opacity: null,
rotation: 0,
rotationCenter: [0, 0],
scale: [1, 1],
zIndex: 1,
clip: null,
};
{Component} refers to any drawable part of a scene. As an abstract class, it should not be instantiated by itself, but by any sub-class (ex: Rectangle, Star, Image …).
const myRectangle = new Pencil.Rectangle();
All {Component} can be instantiated using the following signature:
(position, [...a number of specific properties], options)
.
To continue on our previous example:
const position = [100, 25];
const width = 800;
const height = 600;
const drawingOptions = {
fill: "red",
};
const myRectangle = new Pencil.Rectangle(position, width, height, drawingOptions);
To learn about specific properties of any {Component}, refer to the module documentation.
Here is the list of all options with their default values:
const componentOptions = {
fill: "#000",
stroke: null,
strokeWidth: 2,
cursor: Pencil.Component.cursors.default,
join: Pencil.Component.joins.miter,
};
{Position} is an utility class used across the Pencil.js library.
It serves as a wrapper for a x and y coordinate in 2D space. You can create one using the class constructor.
const x = 100;
const y = 200;
const myPosition = new Pencil.Position(x, y);
Alternatively, anywhere a position is needed, you can use an array shorthand.
const myRectangle = new Pencil.Rectangle([100, 200]);
The {Position} class also comes with a lot of methods that can be chained together.
console.log(myPosition.x, myPosition.y); // => 100, 200
myPosition.add(10, 20).multiply(2).rotate(0.25);
console.log(myPosition.x, myPosition.y); // => -440, 220
These functions don't return a new instance, but modify the existing one.
If you don't want that behavior, you can call .clone
first.
console.log(myPosition.x, myPosition.y); // => 100, 200
const clonePosition = myPosition.clone().add(10, 20).multiply(2).rotate(0.25);
console.log(myPosition.x, myPosition.y); // => 100, 200
console.log(clonePosition.x, clonePosition.y); // => -440, 220
{Vector} is another utility wrapper containing a starting and an ending {Position}.
const myVector = new Pencil.Vector(fromPosition, toPosition);
// alternatively
const myVector = new Pencil.Vector([startX, startY], [endX, endY]);
Each instances carries useful chainable methods.
myVector.add(otherVector).multiply(2);
if (myVector.intersect(someVector)) {
// myVector intersects someVector
}
You can also use the .clone
method to prevent operations from mutating your instance.
All classes fire events and you can listen to those.
// Mouse events
const eventName = Pencil.MouseEvent.events.hover;
const callback = () => myRectangle.options.fill = "red";
myRectangle.on(eventName, callback);
// Keyboard events
const eventName = Pencil.KeyboardEvent.events.keydown;
myScene.on(eventName, (event) => {
switch(event.key) {
case Pencil.KeyboardEvent.keys.enter:
console.log("Enter key pressed");
break;
case Pencil.KeyboardEvent.keys.delete:
console.log("Delete key pressed");
break;
// ...
}
});
// Network events
const eventName = Pencil.NetworkEvent.events.ready;
const myImage = new Pencil.Image([10, 20], url);
myImage.on(eventName, () => myScene.add(myImage).render());
Events bubble to parent container and all his ancestor.
If you want to listen to events only targeting a specific element, you have to set the third parameter to true
.
container.on("eventName", callback); // Listen to all "eventName" triggered by any of container's children
container.on("eventName", callback, true); // Listen only to "eventName" triggered by container
You can write on the scene with the {Text} class. This one is a bit different from the other {Component}s.
const position = [0, 0];
const text = new Pencil.Text(position, "Some text", options);
{Text} has more options on top of options from Component. Here is the list with default values:
const textOptions = {
font: "sans-serif",
fontSize: 20,
align: Pencil.Text.alignments.start,
bold: false,
italic: false,
underscore: false,
lineHeight: 1,
};
If the font
property of options
is an URL, you have to wait for it to be loaded before rendering it.
const fontURL = "https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxK.woff2";
const text = new Pencil.Text(aPosition, "Robot ♥ you", {
font: fontURL,
});
text.on("ready", () => myScene.add(text).render());
A {Component} can be set {draggable} with a simple call of a function.
myRectangle.draggable();
// or with some options
myRectangle.draggable({
x: false,
});
// or
myRectangle.draggable({
constrain: new Pencil.Vector(min, max),
});
A {Rectangle} can be set {resizable} with a simple call of a function.
myRectangle.resizable();
// or with some options
myRectangle.resizable({
x: false,
});
// or
myRectangle.resizable({
constrain: new Pencil.Vector(min, max),
});
Pencil.js provides ways to let your users interacts with common inputs components ({Button}, {Checkbox}, {Select}, {Slider}, {Knob}).
new Pencil.Slider(aPosition, {
value: 1,
min: 1,
max: 100,
});
new Pencil.Select(anotherPosition, [
"First option",
"Second option",
"Third option",
], {
value: 1,
});
These classes have their own set of options to customize how they look.
new Pencil.Button(yetAnotherPosition, {
value: "Click me",
fill: "red",
background: "#222",
border: "gold",
hover: "#444",
});
Whenever you have to provide a color (fill or stroke option for example), you can use a CSS color value or the {Color} class.
This utility class has several ways to be created.
new Pencil.Color(0.1, 0.5, 0.3);
new Pencil.Color("indigo");
new Pencil.Color("#123456");
new Pencil.Color("#123");
new Pencil.Color(0x123456);
Every one of this definitions can have one more parameter to define its opacity.
const opacity = 0.7;
new Pencil.Color(0.1, 0.5, 0.3, opacity);
Then, you have access to numerous methods to manipulate the color.
const color = new Pencil.Color("olive");
color.saturation(0.9).reverse();
console.log(color.name); // => "mediumslateblue"
{Linear-gradient} and {Radial-gradient} can be used in any fill or stroke option. Both are described with a single object.
const linear = new Pencil.LinearGradient(start, end, colorStops);
const radial = new Pencil.RadialGradient(center, radius, colorStops);
In both case, colorStops
works the same way.
You define colors the gradient go through. Use the keys for the position ratio and the values for the color.
const colorStops = {
0: "red",
0.5: "#0f0",
1: new Color(0, 0, 1),
};
All keys should be comprise between 0 and 1. You don't have to set a value for 0 or 1, the closest color will be used in that case.
Pencil.js supports having more than one {Scene} at once and going back and forth between them. In order to simplify this process, you can use the {Navigation} helper.
const firstScene = Pencil.Navigation.prepareScenes({
main: (scene) => {
// Add whatever you want to the scene
},
other: (scene) => {
// ...
},
});
console.log(firstScene.options.shown && firstScene.isLooped); // => true
The first on the list is going to be the default scene and displayed right away.
All scenes are going to share the same canvas element. If you already have one in your DOM,
you can put it as second parameter of the prepareScenes
function.
If you want to go from a scene to another,
you should fire a "change-scene"
event and give it the new scene id.
const scene = Pencil.Navigation.getCurrentScene();
scene.fire(new BaseEvent(Scene.events.change, "other");
1. Keep your code cleaner and lighter by only importing parts of Pencil.js that you need.
import { Star, Image as PencilImage } from "pencil.js";
// same as
import Star from "@pencil.js/star";
import PencilImage from "@pencil.js/image";
2. You can make you own class extending existing ones.
import Circle from "@pencil.js/circle";
class BlueCircle extends Circle {
static get defaultOptions () {
return {
...super.defaultOptions,
fill: "blue",
};
}
}
3. You can create and fire your own events.
const myEventName = "something append";
eventTarget.fire(new Pencil.BaseEvent(myEventName, eventTarget);
// and listen using the same event name.
myScene.on(myEventName, doSomething);
4. You can use the scale option to flip components.
const flippedImage = new Image(position, url, width, height, {
scale: [-1, 1], // Flip the image horizontally
});
Add your tip, by sending a pull request to this guide's repository.