Converting Pointer Inputs to a Curve
A Simple Curve
We want to display a curve that mimics what a real pen would look like on paper. Determining the simplest curve that closely follows an array of input points is a bit of work so we will let the simplify function from paper.js do the heavy lifting.
Paper.js has lots of other utilities for dealing with curves if you need them, but we will only include this one function. The actual simplification function has nicely been bundled by @luncheon to be imported via jsDeliver or copy/pasted easily.
Dozens of points can be very closely approximated with just a handful of cubic bezier curves so we save a lot of space in addition to having a smoother curve. Once the SVG path is generated, we can create an SVG path and set the "d" attribute. Then set the stroke width, color, and any other characteristics as desired.
//el is the SVG element
let d = simplifySvgPath([[0,0],[100,100]]);
var newPath = document.createElementNS("http://www.w3.org/2000/svg",'path');
newPath.setAttributeNS(null,'d',d);
newPath.setAttributeNS(null,'fill','none');
newPath.setAttributeNS(null,'stroke','black');
newPath.setAttributeNS(null,'stroke-width',10);
el.appendChild(newPath);
Pointer Inputs
Capturing mouse or pen input is not too hard with JavaScript on most browsers by using pointer events. Basically just add the new coordinates to an array on each move event and then simplify the stroke on the up event. We will capture from an SVG element and want to translate each pointer coordinate into the coordinates of the SVG.
let clientRect = evt.currentTarget.getBoundingClientRect();
let x = (evt.clientX-clientRect.left)/clientRect.width*svgwidth;
let y = (evt.clientY-clientRect.top)/clientRect.height*svgheight;
The above code finds where the absolute coordinate is within the SVG element and scales based on the width in our SVG units. If your viewBox does not start at the origin you will need to shift appropriately. And remember that y is zero at the top of the screen (and the top of the SVG) and then becomes positive as we travel down the screen.
The above assumes the event is attached to the SVG element, but you can get the correct bounding rectangle however is easiest for your application. The variable svgwidth is the width in terms of SVG coordinates so if the viewBox is "0 0 200 200" then svgwidth and svgheight will be 200. You do need to make sure the viewBox exactly fills the element to get an accurate conversion.
Ignoring Extra Touches
On touchscreen devices, touch events can trigger lots of behavior that prevents clean input and output. We can ignore most touch inputs with either JS or CSS. I'm not sure which of the following lines are strictly needed, but these lines or their CSS equivalents should handle almost all browsers.
//el is the SVG element
el.style.webkitTouchCallout = "none";
el.style.khtmlUserSelect = "none";
el.style.MozUserSelect = "none";
el.style.msUserSelect = "none";
el.style.userSelect = "none";
Additionally, we will ignore the pointer event if
evt.pointerType == "touch"
. Handling touch or pen is fairly simple but trying to handle both starts to get messy. If you do want users to be able to use their finger then you can add an option to enable touch events.It is also possible to disable touch-actions for the element in CSS, but I don't think that helps anything once we have done the below. Depending on your exact needs, though, you may want to toggle various options to get the right events to trigger.
Handling iOS
On iOS devices, starting a new stroke quickly after ending the last stroke will not work unless we do even more to disable touch events. The solution, as I discovered at this blog post by Michael Kowalchik, turns out to be adding preventDefault to the touchmove event. It seems like using touchstart instead also works.
//el is the SVG element
el.addEventListener('touchstart',function(evt){evt.preventDefault();});
Adding the above code has the side effect of making it impossible to move around the screen by touching the SVG element. To reenable this functionality we do not want to preventDefault when the pointerType is "touch". Unfortunately you must track the pointerType from the pointerdown event since touchstart does not store pointerType. Exactly how to pull this off, and whether it is necessary, will depend on your application.
Download Images
It is possible to convert our SVG paths to a PNG or JPEG image by using the canvas element. We have saved the SVG paths, and it is easy to add them to the canvas by using Path2D().
let svgPath = "M0,0L100,100";
let canvasPath = new Path2D(svgPath);
let ctx = canvas.getContext("2d");
ctx.stroke(canvasPath);
You need to know more about the canvas element to get the right image, but once you have the image you can convert it to a dataURL with
canvas.toDataURL();
and then set that url as the href attribute of a link. Set the download attribute to the desired filename and then either click the link for the user or allow them to do so.Test
Stroke Width:
Smoothness:
Draw in the box above to check out the basic functionality. Draw lots of interesting things, change the smoothness and/or the width of each stroke, and then download your final image if desired. For more fun you can complete sudoku puzzles, color by numbers, create memes, and more.
Future
Eventually, we will make more complex paths that account for differing levels of pressure applied by the pen. Also it is possible to get the status of the eraser button on the pen and perform different actions. We can also animate the strokes and create videos.