274 lines
10 KiB
JavaScript
274 lines
10 KiB
JavaScript
import { getCubicExtremaPoints, getQuadraticExtremaPoints, } from "../BezierFunctions.js";
|
|
import { Factory } from "../Factory.js";
|
|
import { _registerNode } from "../Global.js";
|
|
import { Shape } from "../Shape.js";
|
|
import { Util } from "../Util.js";
|
|
import { getNumberArrayValidator, getNumberValidator } from "../Validators.js";
|
|
function getControlPoints(x0, y0, x1, y1, x2, y2, t) {
|
|
const d01 = Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)), d12 = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)), dSum = d01 + d12;
|
|
// all three points are the same, so the control points collapse into that
|
|
// point. Without this the divisions below are 0 / 0 = NaN.
|
|
if (dSum === 0) {
|
|
return [x1, y1, x1, y1];
|
|
}
|
|
const fa = (t * d01) / dSum, fb = (t * d12) / dSum, p1x = x1 - fa * (x2 - x0), p1y = y1 - fa * (y2 - y0), p2x = x1 + fb * (x2 - x0), p2y = y1 + fb * (y2 - y0);
|
|
return [p1x, p1y, p2x, p2y];
|
|
}
|
|
function expandPoints(p, tension) {
|
|
const len = p.length, allPoints = [];
|
|
for (let n = 2; n < len - 2; n += 2) {
|
|
const cp = getControlPoints(p[n - 2], p[n - 1], p[n], p[n + 1], p[n + 2], p[n + 3], tension);
|
|
if (isNaN(cp[0])) {
|
|
continue;
|
|
}
|
|
allPoints.push(cp[0]);
|
|
allPoints.push(cp[1]);
|
|
allPoints.push(p[n]);
|
|
allPoints.push(p[n + 1]);
|
|
allPoints.push(cp[2]);
|
|
allPoints.push(cp[3]);
|
|
}
|
|
return allPoints;
|
|
}
|
|
function getBezierExtremaPoints(points) {
|
|
const extrema = [];
|
|
// points[0], points[1] is the starting point; every subsequent group of
|
|
// 6 values is one cubic bezier segment (cp1x, cp1y, cp2x, cp2y, x, y),
|
|
// whose start is the previous segment's end point (matching the layout
|
|
// Line#_sceneFunc feeds to context.bezierCurveTo in a loop).
|
|
for (let n = 0; n + 7 < points.length; n += 6) {
|
|
// the end point of the segment. It is a joint with the next segment, and
|
|
// the curve can reach its highest or lowest value there without the
|
|
// derivative going to zero on either side.
|
|
extrema.push(points[n + 6], points[n + 7], ...getCubicExtremaPoints(points[n], points[n + 1], points[n + 2], points[n + 3], points[n + 4], points[n + 5], points[n + 6], points[n + 7]));
|
|
}
|
|
return extrema;
|
|
}
|
|
/**
|
|
* Line constructor. Lines are defined by an array of points and
|
|
* a tension
|
|
* @constructor
|
|
* @memberof Konva
|
|
* @augments Konva.Shape
|
|
* @param {Object} config
|
|
* @param {Array} config.points Flat array of points coordinates. You should define them as [x1, y1, x2, y2, x3, y3].
|
|
* @param {Number} [config.tension] Higher values will result in a more curvy line. A value of 0 will result in no interpolation.
|
|
* The default is 0
|
|
* @param {Boolean} [config.closed] defines whether or not the line shape is closed, creating a polygon or blob
|
|
* @param {Boolean} [config.bezier] if no tension is provided but bezier=true, we draw the line as a bezier using the passed points
|
|
* @@shapeParams
|
|
* @@nodeParams
|
|
* @example
|
|
* var line = new Konva.Line({
|
|
* x: 100,
|
|
* y: 50,
|
|
* points: [73, 70, 340, 23, 450, 60, 500, 20],
|
|
* stroke: 'red',
|
|
* tension: 1
|
|
* });
|
|
*/
|
|
export class Line extends Shape {
|
|
constructor(config) {
|
|
super(config);
|
|
this.on('pointsChange.konva tensionChange.konva closedChange.konva bezierChange.konva', function () {
|
|
this._clearCache('tensionPoints');
|
|
});
|
|
}
|
|
_hasTension() {
|
|
return this.tension() !== 0 && this.points().length > 4;
|
|
}
|
|
/**
|
|
* Report every curve segment of a line with a tension, in draw order. Both
|
|
* the scene function and the bounding rect read the shape through this, so
|
|
* that they can not disagree about which curve the line is.
|
|
*
|
|
* The handlers take plain numbers, because this runs on every frame.
|
|
*/
|
|
_eachTensionSegment(onQuadratic, onCubic) {
|
|
const points = this.points(), length = points.length, closed = this.closed(), tp = this.getTensionPoints(), len = tp.length;
|
|
let x0 = points[0], y0 = points[1],
|
|
// a closed line has a curve on both sides of the first point, so it
|
|
// starts on a full cubic. An open one is capped by a quadratic instead
|
|
n = closed ? 0 : 4;
|
|
if (!closed) {
|
|
onQuadratic(x0, y0, tp[0], tp[1], tp[2], tp[3]);
|
|
x0 = tp[2];
|
|
y0 = tp[3];
|
|
}
|
|
while (n < len - 2) {
|
|
const cp1x = tp[n++], cp1y = tp[n++], cp2x = tp[n++], cp2y = tp[n++], x = tp[n++], y = tp[n++];
|
|
onCubic(x0, y0, cp1x, cp1y, cp2x, cp2y, x, y);
|
|
x0 = x;
|
|
y0 = y;
|
|
}
|
|
if (!closed) {
|
|
onQuadratic(x0, y0, tp[len - 2], tp[len - 1], points[length - 2], points[length - 1]);
|
|
}
|
|
}
|
|
_getStrokePadding(miterLimit = this.points().length === 4 && !this.closed() ? 1 : undefined) {
|
|
return super._getStrokePadding(miterLimit);
|
|
}
|
|
_sceneFunc(context) {
|
|
const points = this.points(), length = points.length, closed = this.closed(), bezier = this.bezier();
|
|
if (!length) {
|
|
return;
|
|
}
|
|
let n = 0;
|
|
context.beginPath();
|
|
context.moveTo(points[0], points[1]);
|
|
// tension
|
|
if (this._hasTension()) {
|
|
this._eachTensionSegment((_x0, _y0, cpx, cpy, x, y) => context.quadraticCurveTo(cpx, cpy, x, y), (_x0, _y0, cp1x, cp1y, cp2x, cp2y, x, y) => context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y));
|
|
}
|
|
else if (bezier) {
|
|
// no tension but bezier
|
|
n = 2;
|
|
while (n < length) {
|
|
context.bezierCurveTo(points[n++], points[n++], points[n++], points[n++], points[n++], points[n++]);
|
|
}
|
|
}
|
|
else {
|
|
// no tension
|
|
for (n = 2; n < length; n += 2) {
|
|
context.lineTo(points[n], points[n + 1]);
|
|
}
|
|
}
|
|
// closed e.g. polygons and blobs
|
|
if (closed) {
|
|
context.closePath();
|
|
context.fillStrokeShape(this);
|
|
}
|
|
else {
|
|
// open e.g. lines and splines
|
|
context.strokeShape(this);
|
|
}
|
|
}
|
|
getTensionPoints() {
|
|
return this._getCache('tensionPoints', this._getTensionPoints);
|
|
}
|
|
_getTensionPoints() {
|
|
if (this.closed()) {
|
|
return this._getTensionPointsClosed();
|
|
}
|
|
else {
|
|
return expandPoints(this.points(), this.tension());
|
|
}
|
|
}
|
|
_getTensionPointsClosed() {
|
|
const p = this.points(), len = p.length, tension = this.tension(), firstControlPoints = getControlPoints(p[len - 2], p[len - 1], p[0], p[1], p[2], p[3], tension), lastControlPoints = getControlPoints(p[len - 4], p[len - 3], p[len - 2], p[len - 1], p[0], p[1], tension), middle = expandPoints(p, tension), tp = [firstControlPoints[2], firstControlPoints[3]]
|
|
.concat(middle)
|
|
.concat([
|
|
lastControlPoints[0],
|
|
lastControlPoints[1],
|
|
p[len - 2],
|
|
p[len - 1],
|
|
lastControlPoints[2],
|
|
lastControlPoints[3],
|
|
firstControlPoints[0],
|
|
firstControlPoints[1],
|
|
p[0],
|
|
p[1],
|
|
]);
|
|
return tp;
|
|
}
|
|
getWidth() {
|
|
return this.getSelfRect().width;
|
|
}
|
|
getHeight() {
|
|
return this.getSelfRect().height;
|
|
}
|
|
// overload size detection
|
|
getSelfRect() {
|
|
let points = this.points();
|
|
if (points.length < 4) {
|
|
return {
|
|
x: points[0] || 0,
|
|
y: points[1] || 0,
|
|
width: 0,
|
|
height: 0,
|
|
};
|
|
}
|
|
if (this._hasTension()) {
|
|
// the two end points of every segment, plus the points where it turns
|
|
// back on either axis. Together they are the exact bounds of the curve
|
|
const bounds = [points[0], points[1]];
|
|
this._eachTensionSegment((x0, y0, cpx, cpy, x, y) => bounds.push(x, y, ...getQuadraticExtremaPoints(x0, y0, cpx, cpy, x, y)), (x0, y0, cp1x, cp1y, cp2x, cp2y, x, y) => bounds.push(x, y, ...getCubicExtremaPoints(x0, y0, cp1x, cp1y, cp2x, cp2y, x, y)));
|
|
points = bounds;
|
|
}
|
|
else if (this.bezier()) {
|
|
// no trailing point here: the extrema already carry the end point of
|
|
// every segment that is drawn. Adding the last pair of the array back
|
|
// would include a control point of a trailing partial segment, which
|
|
// _sceneFunc never draws.
|
|
points = [points[0], points[1], ...getBezierExtremaPoints(points)];
|
|
}
|
|
return Util._getPointsRect(points);
|
|
}
|
|
}
|
|
Line.prototype.className = 'Line';
|
|
Line.prototype._attrsAffectingSize = ['points', 'bezier', 'tension', 'closed'];
|
|
_registerNode(Line, true);
|
|
// add getters setters
|
|
Factory.addGetterSetter(Line, 'closed', false);
|
|
/**
|
|
* get/set closed flag. The default is false
|
|
* @name Konva.Line#closed
|
|
* @method
|
|
* @param {Boolean} closed
|
|
* @returns {Boolean}
|
|
* @example
|
|
* // get closed flag
|
|
* var closed = line.closed();
|
|
*
|
|
* // close the shape
|
|
* line.closed(true);
|
|
*
|
|
* // open the shape
|
|
* line.closed(false);
|
|
*/
|
|
Factory.addGetterSetter(Line, 'bezier', false);
|
|
/**
|
|
* get/set bezier flag. The default is false
|
|
* @name Konva.Line#bezier
|
|
* @method
|
|
* @param {Boolean} bezier
|
|
* @returns {Boolean}
|
|
* @example
|
|
* // get whether the line is a bezier
|
|
* var isBezier = line.bezier();
|
|
*
|
|
* // set whether the line is a bezier
|
|
* line.bezier(true);
|
|
*/
|
|
Factory.addGetterSetter(Line, 'tension', 0, getNumberValidator());
|
|
/**
|
|
* get/set tension
|
|
* @name Konva.Line#tension
|
|
* @method
|
|
* @param {Number} tension Higher values will result in a more curvy line. A value of 0 will result in no interpolation. The default is 0
|
|
* @returns {Number}
|
|
* @example
|
|
* // get tension
|
|
* var tension = line.tension();
|
|
*
|
|
* // set tension
|
|
* line.tension(3);
|
|
*/
|
|
Factory.addGetterSetter(Line, 'points', [], getNumberArrayValidator());
|
|
/**
|
|
* get/set points array. Points is a flat array [x1, y1, x2, y2]. It is flat for performance reasons.
|
|
* @name Konva.Line#points
|
|
* @method
|
|
* @param {Array} points
|
|
* @returns {Array}
|
|
* @example
|
|
* // get points
|
|
* var points = line.points();
|
|
*
|
|
* // set points
|
|
* line.points([10, 20, 30, 40, 50, 60]);
|
|
*
|
|
* // push a new point
|
|
* line.points(line.points().concat([70, 80]));
|
|
*/
|