📜 ⬆️ ⬇️

Canvas - almost like SVG


Link to canvas ninju at the end.

Specifically, we will focus on the Path element and how to implement it on canvas.

As we remember the path in svg can draw bezier curves, splines of these curves, as well as circles. The canvas in this area have far less possibilities, so we will work with it. To begin, learn to draw curves. In svg, as in canvas, curves are limited to only 3 degrees, this is done for the sake of optimization, we will use the canonical equation to calculate them, so we will have curves of any order.

There is nothing complicated in the calculation of the curves. Below, the function code that returns an array of the form [x, y] is a point on the curve. The input parameters for the function are: an array of control points and an offset coefficient relative to the beginning of the curve. The offset is a value in percent from 0% to 100% (0 is the beginning of the curve, 100 is the end), I hope this point is clear.
')
//      function factorial (number) { var result = 1; while(number){ result *= number--; } return result; } function getPointOnCurve (shift, points) { var result = [0,0]; var powerOfCurve = points.length - 1; shift = shift/100; for(var i = 0;points[i];i++){ var polynom = (factorial(powerOfCurve)/(factorial(i)*factorial(powerOfCurve - i))) * Math.pow(shift,i) * Math.pow(1-shift,powerOfCurve - i); result[0] += points[i][0] * polynom; result[1] += points[i][1] * polynom; } return result; } getPointOnCurve(60, [[0,0],[100,0],[100,100]]); // -> [84, 36] 

The trick here is the polynomial, which plays the role of a coefficient for calculating a point. I once wrote about curves, only calculated them through recursion ( here ).

Next stop is the splines.

In general, using the previous function, you can draw an arbitrary section of a curve, but with splines it is more and more difficult, in order to draw only a part of the spline, you need to know its length and, accordingly, the lengths of its curves. This is not required if we display the entire spline, but who is looking for easy ways?

Now the array of control points will look like this:

 [[0,0],[100,0],[100,100,true],[200,100],[200,0]] 

Points with true flag are reference points.

Function code
 function getCenterToPointDistance (coordinates){ return Math.sqrt(Math.pow(coordinates[0],2) + Math.pow(coordinates[1],2)); } function getLengthOfCurve (points, step) { step = step || 1; var result = 0; var lastPoint = points[0]; for(var sift = 0;sift <= 100;sift += step){ var coord = getPointOnCurve(sift,points); result += getCenterToPointDistance([ coord[0] - lastPoint[0], coord[1] - lastPoint[1] ]); lastPoint = coord; } return result; }; function getMapOfSpline (points, step) { var map = [[]]; var index = 0; for(var i = 0;points[i];i++){ var curvePointsCount = map[index].length; map[index][+curvePointsCount] = points[i]; if(points[i][2] && i != points.length - 1){ map[index] = getLengthOfCurve(map[index],step); index++; map[index] = [points[i]]; } } map[index] = getLengthOfCurve(map[index],step); return map; }; function getPointOnSpline (shift, points, services) { var shiftLength = services.length / 100 * shift; if(shift >= 100){ shiftLength = services.length; } var counter = 0; var lastControlPoint = 0; var controlPointsCounter = 0; var checkedCurve = []; for(; services.map[lastControlPoint] && counter + services.map[lastControlPoint] < shiftLength; lastControlPoint++ ){ counter += services.map[lastControlPoint]; } for( var pointIndex = 0; points[pointIndex] && controlPointsCounter <= lastControlPoint; pointIndex++ ){ if(points[pointIndex][2] === true){ controlPointsCounter++; } if(controlPointsCounter >= lastControlPoint){ checkedCurve.push(points[pointIndex]); } } return getPointOnCurve( (shiftLength - counter) / (services.map[lastControlPoint] / 100), checkedCurve ); }; var points = [[0,0],[100,0],[100,100,true],[200,100],[200,0]]; var services = {}; services.map = getMapOfSpline(points); services.length= 0; for(var key in services.map){ services.length += services.map[key]; } getPointOnSpline(60, points, services); // -> [136, 95.(9)] 


Well, it remains the case for small, learn to include in our splines arc of ellipses. Here it is worth telling about the native SVG algorithm and how we will differ from it.

For the arc in the path SVG provides 7 parameters, which in my opinion is unnecessary, these parameters:
A rx ry x-axis-rotation large-arc-flag sweep-flag xy

Decryption:


The arc can also be completely controlled by a smaller number of parameters; moreover, in practice, the coordinates of the end point are not always known. In addition, an incorrectly specified end coordinate deforms the ellipse and, if its value is too large, the parameters of the semi-axes no longer correspond to the given ones.

For the arc, we need to know:


Only 5 parameters.

So the logic will be like this, we follow the hands. The beginning of our arc of an ellipse (not to be confused with its center) will be the previous point in the array or the origin if there is no previous point. The end of the arc is the reference point for further construction of the path. With the help of parameters, the arc takes any form and goes anywhere, everything is grand. Again, you need to calculate the length of all segments to build only part of the path.

Here is what a set of points will look like with an ellipse included in it:

 [ [x1,y1], [x2,y2], [x3,y3], [radiusX,radiusY,startRadian,endRadian,tilt], // tilt - optional [x5,y5], ... [xN,yN] ] 

Code
 function getPointOnEllipse (radiusX,radiusY,shift,tilt,centerX,centerY){ tilt = tilt || 0; tilt *= -1; centerX = centerX || 0; centerY = centerY || 0; var x1 = radiusX*Math.cos(+shift), y1 = radiusY*Math.sin(+shift), x2 = x1 * Math.cos(tilt) + y1 * Math.sin(tilt), y2 = -x1 * Math.sin(tilt) + y1 * Math.cos(tilt); return [x2 + centerX,y2 + centerY]; } function getLengthOfEllipticArc (radiusX, radiusY, startRadian, endRadian, step) { step = step || 1; var length = 0; var lastPoint = getPointOnEllipse(radiusX,radiusY,startRadian); var radianPercent = (endRadian - startRadian) / 100; for(var i = 0;i<=100;i+=step){ var radian = startRadian + radianPercent * i; var point = getPointOnEllipse(radiusX,radiusY,radian); length += getCenterToPointDistance([point[0]-lastPoint[0],point[1]-lastPoint[1]]); lastPoint = point; } return length; }; function getMapOfPath (points, step) { var map = [[]]; var index = 0; var lastPoint = []; for(var i = 0;points[i];i++){ var point = points[i]; if(point.length > 3){ map[index] = getLengthOfEllipticArc(point[0], point[1], point[2], point[3], step); if(!points[i+1]){continue} var centerOfArc = getPointOnEllipse(point[0], point[1], point[2] + Math.PI, point[4], lastPoint[0], lastPoint[1]); var endOfArc = getPointOnEllipse(point[0], point[1], point[3], point[4], centerOfArc[0], centerOfArc[1]); index++; map[index] = [endOfArc]; lastPoint = endOfArc; continue; } map[index].push(point); if(point[2] === true || (points[i+1] && points[i+1].length > 3)){ map[index] = getLengthOfCurve(map[index],step); index++; map[index] = [point]; } lastPoint = point; } if(typeof map[index] !== 'number'){map[index] = getLengthOfCurve(map[index],step);} return map; }; function getPointOnPath (shift, points, services) { var shiftLength = services.length / 100 * shift; if(shift >= 100){ shiftLength = services.length; } var counter = 0; var lastControlPoint = 0; var controlPointsCounter = 0; var checkedCurve = []; for(; services.map[lastControlPoint] && counter + services.map[lastControlPoint] < shiftLength; lastControlPoint++){ counter += services.map[lastControlPoint]; } var lastPoint = []; for(var pointIndex = 0; points[pointIndex] && controlPointsCounter <= lastControlPoint; pointIndex++){ var point = points[pointIndex]; if(point.length > 3){ var centerOfArc = getPointOnEllipse(point[0], point[1], point[2] + Math.PI, point[4], lastPoint[0], lastPoint[1]); if(controlPointsCounter === lastControlPoint){ var percent = (shiftLength - counter) / (services.map[lastControlPoint] / 100); var resultRadian = point[2] + ((point[3] - point[2])/100*percent); return getPointOnEllipse(point[0], point[1], resultRadian, point[4], centerOfArc[0], centerOfArc[1]); } lastPoint = getPointOnEllipse(point[0], point[1], point[3], point[4], centerOfArc[0], centerOfArc[1]); controlPointsCounter++; if(controlPointsCounter === lastControlPoint){ checkedCurve.push(lastPoint); } continue } if(point[2] === true || (points[pointIndex+1] && points[pointIndex+1].length > 3)){ controlPointsCounter++; } if(controlPointsCounter >= lastControlPoint){ checkedCurve.push(point); } lastPoint = point; } return getPointOnCurve( (shiftLength - counter) / (services.map[lastControlPoint] / 100), checkedCurve ); }; var points = [[0,0],[100,0],[100,100],[20,20,0,Math.PI],[200,100],[200,0]]; var services = {}; services.map = getMapOfPath(points); services.length= 0; for(var key in services.map){ services.length += services.map[key]; } getPointOnPath(60, points, services); // -> [96.495, 98.036] 


Well, now we are cool dudes, a lot of things we can do.
Link to examples here .
UPD. Do not be intimidated by the extra lines in the example - this is the path with the ellipses included in it, since Ninja is drawn only by splines.

Source: https://habr.com/ru/post/332192/


All Articles