Pablito
An HTML 5 drawing tool based largely off existing tools.
This package makes use of fabricjs-webpack and Canvas-Drawing-Tool. It now uses Webpack 4, and imports, no more scripts in the header. This was easier than forking the original tool and updating the tooling. Please note that a large portion of this read-me is taken directly from the Canvas-Drawing-Tool repository.
Basic Usage
npm install --save @britannica/pablito
To create a pablito drawing, you'll first need to include the script on the page:
In your javascript source import the library into your project:
import Pablito from '@britannica/pablito';
const container = document.getElementById('stickerbook-container');
document.body.appendChild(container);
const pablito = new Pablito({
container : container,
stickers: {
enabled: [
'path/to/first/image.png',
'path/to/other/image.png'
],
controls: {
cornerColor: 'rgba(0,0,0,0.5)',
cornerSize: 20,
hasBorders: true
},
defaults: {
x: 0,
y: 0,
xScale: 1,
yScale: 1,
rotation: 0
}
},
background: {
enabled: [
'first/bg.png',
'other/bg.png'
],
default: 'first/bg.png'
},
brush: {
enabled: [
'eraser',
'bitmap',
'bitmap-eraser',
'fill',
'marker',
'pattern',
'pencil',
'spray',
'mycustombrush'
],
widths: [1, 10],
colors: [
'#0000FF',
'#FF0000'
],
custom: {
mycustombrush: MyCustomBrush
}
},
mobileEnabled: true,
useDefaultEventHandlers: true
});
Note that the canvas fits to fill the containing element, so any height and width rules you've set will apply to the
canvases inside. We advocate that you use viewport units
to maintain aspect ratio, so that scaling preserves sizes properly. We'll loosen that suggestion as we improve our
resizing algorithm. However, in the meantime the demo code stylesheet has an example of how to do this.
Available Methods
Rerendering
You can always re-render the stickerbook by hand using the triggerRender
method. This will simply call the internal
fabric instance's renderAll
. However, if you'd like to not only re-render but also want to recalculate positioning
(due to a change in the container size etc.), you can call resize
which will recalculate positioning for each canvas
element and then redraw.
Undo and Redo
Any operation that was previously done can be undone (or redone) via the undo
and redo
method respectively:
pablito.undo();
pablito.redo();
Setters
Brushes, stickers, and the background can all be set on the fly. Keep in mind that they are validated when set, so if
you attempt to set a brush/sticker/background that is not enabled, an Error will be thrown. Here's an example:
pablito.setSticker('some/enabled/sticker.png');
pablito.setBrush('pencil');
pablito.setBrushWidth(10);
pablito.setBackground('some/enabled/bg.png');
try {
pablito.setColor('PapayaWhip');
} catch(e) {
alert('ORLY?');
}
Note that pablito.setSticker
is ansynchronous due to the fact the browser will have to load the image before it
can add it to the canvas. Therefore, pablito.setSticker
returns a promise that resolves to the pablito once
the image has loaded.
Along with setting the background, you can remove it too:
pablito.clearBackground();
You can also programmatically unselect any UI items with
pablito.deselectAll();
Configurable Brushes
When setting a brush via setBrush
, some brushes allow for configuration to be provided for that brush.
Bitmap Brush
The bitmap brush takes an image and turns it into a "stamp" by replacing it's RGB channels with the color you specify.
It preserves the alpha channel however, so you can use that shape the stamp in any way you want. Here's an example
usage:
pablito.setBrush('bitmap', {
image: 'pathToSomeImage'
});
However, don't forget to enable this brush!
Bitmap Eraser Brush
The bitmap eraser behaves exactly like the bitmap brush, but works as an eraser instead:
pablito.setBrush('bitmap-eraser', {
image: 'pathToSomeImage'
});
Pattern Brush
The pattern brush takes an array of images and stamps them as the user drags their mouse/finger across the canvas. Here is an example configuration:
pablito.setBrush('pattern', {
images: [
'image1',
'image2'
]
})
Fill Brush
The fill brush allows you to configure some of its behavior. Since the fill algorithm is computationally expensive, in some circumstances it's
better to spread the fill algorithm's intermediate work over multiple frames. This has the benefits of:
- Keeping framerate up and
- Giving the user feedback on the progress of a long-running task.
By default, the fill tool is configured to do all the work on a single frame. For a smaller canvas, or on desktop, this may be sufficient. This is the default behavior:
pablito.setBrush('fill');
There is, however a way to spread this work over multiple frames. Consider the following configuration:
pablito.setBrush('fill', {
isAsync: true,
stepsPerFrame: 10,
partialFill: false
});
In this case the fill tool will animate it's progress, but stop animating when you mouse up or touch end (the default behavior). This means
that the filled region might not be completely finished, but stopped on the end of user interaction. Moreover, the algorithm
will attempt to fill 10 scanlines per frame (the default if don't provide stepsPerFrame
configuration).
Now, consider this other configuration:
pablito.setBrush('fill', {
isAsync: true,
stepsPerFrame: 10,
partialFill: true
});
In this case the fill tool will animate it's progress, and keep going even when the user mouses up (or touch end). The image will
be added whenever the algorithm finishes.
Exporting
You can also export the pablito to a data url for saving, printing, whatever like so:
var dataUrl = pablito.toDataURL();
var html = "<!DOCTYPE html>\n<html><body><img src=\"" + dataUrl + "\"/></body></html>";
var popup = window.open();
popup.document.write(html);
popup.focus();
popup.print();
The above example will attempt to print the image. You can download and save to camera roll with even less code:
window.location.href = pablito.toDataURL().replace("image/png", "image/octet-stream");
Bear in mind that this the internal toDataURL()
call we make to the canvas element will fail if any sticker
placed on the canvas came from a different origin (perhaps a CDN or the like). This can alleviated by setting the
proper Access-Control-Allow-Origin
header on the resource to allow it to be used freely by your page.
Background Positioning
The pablito provides background positioning methods, so you can adjust how the background looks as your canvas
scales and grows. There are three options (currently):
pablito.backgroundManager.setPositioning('default');
pablito.backgroundManager.setPositioning('fit-height');
pablito.backgroundManager.setPositioning('fit-width');
default
is the default behavior, and simply positions the image in the top corner with no scaling at all.
fit-height
will scale the image so that it is as tall as the canvas and centered horizontally. fit-width
is the
opposite and fills the image horizontally, while centering it vertically. If an unknown option is provided for
positioning, the default will be used.
You can also provide positioning that fits once, but never scales again (after a canvas resize for instance):
fit-width-no-rescale
and fit-height-no-rescale
will acheive this. This are helpful for maintaining the background
image's position relative to the position of the drawings on the canvas.
Events
A pablito instance fires events from the underlying Fabric canvas, and you can register custom callbacks to respond to them using the on
and off
methods.
If you configure the pablito with useDefaultEventHandlers: true
, it will register built-in event handlers for manipulating stickers with mouse and touch events. With useDefaultEventHandlers: true
, those handlers won't be registered. See event-handlers.js
for the source code of the default handlers.
You can register callbacks for the following events:
'before:selection:cleared',
'mouse:down',
'mouse:move',
'mouse:out',
'mouse:over',
'mouse:up',
'object:modified',
'object:moving',
'object:rotating',
'object:scaling',
'object:selected',
'path:created',
'selection:cleared',
'selection:created',
'touch:drag',
'touch:gesture',
'touch:longpress',
'touch:orientation',
'touch:shake'
pablito.on('touch:longpress', function handleLongPress(evt) {
});
You can also de-register event handlers
const handleLongPress = function (evt) {
};
pablito.on('touch:longpress', handleLongPress);
pablito.off('touch:longpress', handleLongPress);
Sticker manipulation
It's possible to manipulate stickers directly, for example in custom event handlers:
pablito.getTopSticker().setAngle(90);
pablito.triggerRender();
Remember to call triggerRender()
to make your manipulations visible.
low-level sticker API is from fabric:
http://fabricjs.com/docs/fabric.Object.html
Example methods:
sticker.setAngle()
(http://fabricjs.com/docs/fabric.Object.html#setAngle)
sticker.scale()
(http://fabricjs.com/docs/fabric.Object.html#scale)
If you have called pablito.setSticker
with some url, you can place the image programmatically as well. By calling
pablito.placeSticker({ x: 0, y: 0 })
you can place an image manually onto the canvas. You can also provide an
optional xScale
, yScale
and rotation
to place the image in a particular starting layout.
Finishing Up
If you're done with the pablito, you can simply call pablito.destroy()
to remove any DOM
nodes, listeners, memory added by the pablito. However, it will not remove the container you
provide. Please note that when you call destroy
, the pablito is no longer usable and any
method call is then considered undefined behavior.
Building for development
npm start
Live reloading is available for the package, however the demo will need to be refreshed if you make a change.
Final notes
- As of 3.0.0, we're no longer binding a Promise polyfill to this library to help cut down on library size. If you need
to run in a browser that doesn't have native Promises, you'll need to polyfill
it yourself.
- Background images and erasers are a little tricky to handle. When rendering the eraser as you move your mouse/finger
we are only able to render your configured background image composited with the background color of your stickerbook
container. Otherwise, we will assume white. If your background image has alpha in it, make sure you set a reasonable
non-transparent background for your container, otherwise we can't guarantee eraser paths will properly reflect what
the user expects to see.