Ссылка на канвас ниндзю в конце.
Конкретно речь пойдёт об элементе Path и как его реализовать на canvas.
Как мы помним path в svg умеет рисовать кривые безье, сплайны из этих кривых, а так же окружности. У канваса в этом плане возможностей куда меньше, так что будем работать с ним. Для начала научимся рисовать кривые. В svg как и в canvas кривые ограничены лишь 3-мя степенями, это сделано ради оптимизации, мы же будем использовать каноничное уравнение для их вычисления, так что кривые у нас будут любого порядка.
Ничего сложного в расчёте кривых нет. Ниже код функции, которая возвращает массив вида [x,y] — это точка на кривой. Входные параметры для функции: массив контрольных точек и коэффициент смещения относительно начала кривой. Смещение — это значение в процентах от 0% до 100% (0 — начало кривой, 100 — конец), надеюсь этот момент понятен.
// так же понадобится найти факториал
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]
Весь фокус здесь состоит в полиноме, который играет роль коэффициента для вычисления точки. Я уже писал когда-то о кривых, только высчитывал их через рекурсию (вот здесь).
Следующая остановка — сплайны.
Вообще с помощью предыдущей функции можно отрисовать произвольный участок кривой, но со сплайнами всё сложнее, чтобы нарисовать только часть сплайна нужно знать его длину, а соответственно и длины его кривых. Этого не потребуется, если мы будем выводить сплайн целиком, но кто же ищет лёгких путей?
Теперь массив контрольных точек будет иметь такой вид:
[[0,0],[100,0],[100,100,true],[200,100],[200,0]]
Точки с флагом true — опорные.
Код функции
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)]
Ну что ж осталось дело за малым, научиться включать в наши сплайны дуги эллипсов. Тут стоит рассказать о родном алгоритме SVG и чем мы будем от него отличаться.
Для дуги в path SVG предусматривает 7 параметров, что на мой взгляд излишне, вот эти параметры:
A rx ry x-axis-rotation large-arc-flag sweep-flag x y
Расшифровка:
- первые два параметра вполне очевидны — это полуоси ординаты и абсциссы
- третий — это наклон эллипса, заданный в градусах
- четвёртым и пятым задаётся направление отрисовки дуги
- шестой и седьмой — координаты конечной точки
Полностью контролировать дугу можно и меньшим числом параметров, к тому же на практике не всегда известны координаты конечной точки. К тому же неправильно указанная конечная координата деформирует эллипс и при слишком большом её значении, параметры полуосей уже не соответствуют заданным.
Для дуги нам нужно знать:
- полуоси эллипса
- углы начала и конца дуги
- наклон эллипса
Всего 5 параметров.
Итак логика будет такая, следим за руками. Началом нашей дуги эллипса (не путать с его центром) будет предыдущая точка в массиве или начало координат, если предыдущей точки нет. Конец дуги — опорная точка для дальнейшего построения пути. С помощью параметров, дуга принимает любую форму и направляется куда угодно, всё чинно. Опять же нужно посчитать длину всех отрезков для построения лишь части пути.
Вот как будет выглядеть набор точек с включённым в него эллипсом:
[
[x1,y1],
[x2,y2],
[x3,y3],
[radiusX,radiusY,startRadian,endRadian,tilt], // tilt - optional
[x5,y5],
...
[xN,yN]
]
Код
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]
Ну вот теперь мы крутые чуваки, много чего умеем.
Ссылка на примеры вот.
UPD. Не пугайтесь лишних линий в примере — это путь с включёнными в него эллипсами, т.к. ниндзя нарисован только сплайнами.
Поделиться с друзьями
evgeniy2194
Я один не нашел ни одного живого примера?
takovoy
Извиняюсь, это жеж репозиторий :)
GlukKazan
Рекомендую
takovoy
Спасибо за подсказку! Иногда очень полезные вещи лежат под носом.