Migrate from v3
v4 is a complete rewrite in TypeScript. The public API stays the same: constructors, options, methods and events keep their names and signatures. This page lists everything that could affect an existing project.
Installation and imports
The 48 @pencil.js/* packages are replaced by the single pencil.js package. Each module is still importable on its own.
// v3
import Circle from "@pencil.js/circle";
import "@pencil.js/draggable";
// v4
import Circle from "pencil.js/circle";
import "pencil.js/draggable";Imports from pencil.js (named or default) don't change.
- The CDN files are still
dist/pencil.esm.jsanddist/pencil.min.js. The globalPencilvariable exposes every named export. - TypeScript declarations are included, no need for
@types. - Node.js 22.12 or later is required for tooling (the library itself runs in any modern browser).
Behavior changes
These are bug fixes or improvements that can change what you see.
Rendering
Opacity is multiplied with the ancestors opacity. A child with
opacity: 0.5in a parent withopacity: 0.5is now drawn at 25% (it was 50%).Polygon,Heartand closedPathclose their outline properly (no more doubled first segment).Container.setContextonly sets styles. Position, clipping, rotation and scale are applied by the newapplyTransformmethod. Update your overrides if you usedsetContextto transform the context.Clipping takes the clip component's own rotation, scale and origin into account, and clipped areas can't be hovered anymore.
The render is much faster: each element sets its absolute transformation instead of saving and restoring the context, unchanged context properties are not set again, simple shapes keep their path between renders and plain rectangles use
fillRect/strokeRect. As a consequence, a parent no longer calls its children'srendermethod (it calls the internalrenderTree). OverridemakePathto customize how a component is drawn, notrender.drawevents are only created when someone listens to them.
Pointer events
- Mouse, touch and pen all trigger the usual
mousedown,mousemove,mouseup,click… events (the scene uses Pointer Events and captures the pointer while pressed). - Positions account for the canvas CSS size and its current position on the page (scroll, layout changes).
scene.containerPositionis removed. MouseEvent#buttonis now a string ("0","1"…) matchingMouseEvent.buttons, so button modifiers like`mousedown.${MouseEvent.buttons.right}`now work.- Leaving the canvas fires
leaveon the hovered element.
Interactions
.draggable()works inside rotated or scaled parents (#60)..rotatable()rotates around the element'srotationCenterand no longer mutates the event position..resizable()handle follows size changes made by code.
Geometry
getAbsolutePosition()accounts for every transformation, scale included. New helpers:getMatrix(),getAbsoluteMatrix(),toLocal()andtoGlobal().Vector#multiply(position | vector)scales the vector's delta (start stays in place). It used to multiply the end coordinates directly.Math.moduloreturns0instead of the divisor or-0for exact multiples of a negative number.Math.truncateworks beyond 32 bits.RegularPolygonrequires its number of sides.
Widgets
ProgressBarandProgressPienow extend the newWidgetclass instead ofInput: they don't react to hover or click and don't firechange.- Inputs get their initial
valueat construction (it was set when attached to a parent). Buttonshows a pressed state (newactiveoption).Sliderkeeps its handle vertically centered on click and keeps its value when its width changes.Select's options list is drawn above its siblings.
Text, images and sprites
Textfiresreadyasynchronously, so a listener added right after construction is called. A font that fails to load fireserror.Imageignores an outdated load when the url changes before the end of the previous one.Sprite.sheetresolves the image path relative to the JSON file, firessprite-startand uses zeptomatch for glob selectors.Patternaccepts an image url and is transparent until loaded.
Particles
- Particles are updated once per render with the new
update()method (the updater was called each time the path was traced, hit-testing included).
Serialization
toJSONreturns plain data ([x, y]arrays instead ofPositioninstances).JSON.stringifyoutput is the same as before.- Gradients, patterns, path instructions and particles can be serialized and revived with
from. clone()copies children too.- Classes are identified by a static
className, so serialization survives minification. Useregister()to make your own classes revivable.
Colors
- Hex numbers keep their leading zeros (
new Color(0x00ff00)was invalid). - CSS color strings keep their alpha (
"rgba(0, 0, 0, 0.5)"). Color.fromaccepts the[red, green, blue, alpha]array produced bytoJSON, and so dofillandstroke(a serialized color stays usable once revived).Color#lerpandColor#mixblend in the CIE LAB color space, which gives smoother, perceptually even transitions. Results differ slightly from v3, which blended RGB channels (#000to#fffat 0.5 gives#777777instead of#808080).
Navigation
back()returnsnullwhen the history is empty instead of throwing.
New features
scene.destroy()stops rendering, removes every event listener and the canvas it created.emitter.once(name, callback)listens to an event only once.Path.waveToandPath.sinToare implemented.- Gradients' and
Pattern'stoJSON/from. register()to make custom classes revivable byfrom().
