UNPKG

konva

Version:

HTML5 2d canvas library for interactive graphics, design editors, whiteboards, and diagrams.

271 lines (270 loc) • 10.3 kB
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.&nbsp; 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]); } } _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); // 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])); */