first commit
This commit is contained in:
+67
@@ -0,0 +1,67 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
export interface ArcConfig extends ShapeConfig {
|
||||
angle: number;
|
||||
innerRadius: number;
|
||||
outerRadius: number;
|
||||
clockwise?: boolean;
|
||||
}
|
||||
/**
|
||||
* Arc constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} config.angle in degrees
|
||||
* @param {Number} config.innerRadius
|
||||
* @param {Number} config.outerRadius
|
||||
* @param {Boolean} [config.clockwise]
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* // draw a Arc that's pointing downwards
|
||||
* var arc = new Konva.Arc({
|
||||
* innerRadius: 40,
|
||||
* outerRadius: 80,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5,
|
||||
* angle: 60,
|
||||
* rotation: -120
|
||||
* });
|
||||
*/
|
||||
export declare class Arc extends Shape<ArcConfig> {
|
||||
_sceneFunc(context: Context): void;
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
setWidth(width: number): void;
|
||||
setHeight(height: number): void;
|
||||
getSelfRect(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
innerRadius: GetSet<number, this>;
|
||||
outerRadius: GetSet<number, this>;
|
||||
angle: GetSet<number, this>;
|
||||
clockwise: GetSet<boolean, this>;
|
||||
}
|
||||
/**
|
||||
* get/set clockwise flag
|
||||
* @name Konva.Arc#clockwise
|
||||
* @method
|
||||
* @param {Boolean} clockwise
|
||||
* @returns {Boolean}
|
||||
* @example
|
||||
* // get clockwise flag
|
||||
* var clockwise = arc.clockwise();
|
||||
*
|
||||
* // draw arc counter-clockwise
|
||||
* arc.clockwise(false);
|
||||
*
|
||||
* // draw arc clockwise
|
||||
* arc.clockwise(true);
|
||||
*/
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { Konva } from "../Global.js";
|
||||
import { getNumberValidator, getBooleanValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
/**
|
||||
* Arc constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} config.angle in degrees
|
||||
* @param {Number} config.innerRadius
|
||||
* @param {Number} config.outerRadius
|
||||
* @param {Boolean} [config.clockwise]
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* // draw a Arc that's pointing downwards
|
||||
* var arc = new Konva.Arc({
|
||||
* innerRadius: 40,
|
||||
* outerRadius: 80,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5,
|
||||
* angle: 60,
|
||||
* rotation: -120
|
||||
* });
|
||||
*/
|
||||
export class Arc extends Shape {
|
||||
_sceneFunc(context) {
|
||||
const angle = Konva.getAngle(this.angle()), clockwise = this.clockwise();
|
||||
context.beginPath();
|
||||
context.arc(0, 0, Math.abs(this.outerRadius()), 0, angle, clockwise);
|
||||
context.arc(0, 0, Math.abs(this.innerRadius()), angle, 0, !clockwise);
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
getWidth() {
|
||||
return Math.abs(this.outerRadius()) * 2;
|
||||
}
|
||||
getHeight() {
|
||||
return Math.abs(this.outerRadius()) * 2;
|
||||
}
|
||||
setWidth(width) {
|
||||
this.outerRadius(width / 2);
|
||||
}
|
||||
setHeight(height) {
|
||||
this.outerRadius(height / 2);
|
||||
}
|
||||
getSelfRect() {
|
||||
const r1 = Math.abs(this.innerRadius()), r2 = Math.abs(this.outerRadius());
|
||||
const innerRadius = Math.min(r1, r2), outerRadius = Math.max(r1, r2);
|
||||
const clockwise = this.clockwise();
|
||||
// canvas draws a full circle for any non-zero full-turn end angle regardless
|
||||
// of direction, and nothing for angle 0. Keep exact turns distinct from
|
||||
// the modulo-normalized partial sweep below.
|
||||
const rawAngle = Konva.getAngle(this.angle());
|
||||
if (rawAngle % (Math.PI * 2) === 0) {
|
||||
return rawAngle !== 0
|
||||
? {
|
||||
x: -outerRadius,
|
||||
y: -outerRadius,
|
||||
width: outerRadius * 2,
|
||||
height: outerRadius * 2,
|
||||
}
|
||||
: { x: innerRadius, y: 0, width: outerRadius - innerRadius, height: 0 };
|
||||
}
|
||||
const turn = Math.PI * 2;
|
||||
const sweep = clockwise ? -rawAngle : rawAngle;
|
||||
const angle = sweep >= turn ? turn : ((sweep % turn) + turn) % turn;
|
||||
const boundLeftRatio = Math.cos(Math.min(angle, Math.PI));
|
||||
const boundRightRatio = 1;
|
||||
const boundTopRatio = Math.sin(Math.min(Math.max(Math.PI, angle), (3 * Math.PI) / 2));
|
||||
const boundBottomRatio = Math.sin(Math.min(angle, Math.PI / 2));
|
||||
const boundLeft = boundLeftRatio * (boundLeftRatio > 0 ? innerRadius : outerRadius);
|
||||
const boundRight = boundRightRatio * (boundRightRatio > 0 ? outerRadius : innerRadius);
|
||||
const boundTop = boundTopRatio * (boundTopRatio > 0 ? innerRadius : outerRadius);
|
||||
const boundBottom = boundBottomRatio * (boundBottomRatio > 0 ? outerRadius : innerRadius);
|
||||
return {
|
||||
x: boundLeft,
|
||||
y: clockwise ? -1 * boundBottom : boundTop,
|
||||
width: boundRight - boundLeft,
|
||||
height: boundBottom - boundTop,
|
||||
};
|
||||
}
|
||||
}
|
||||
Arc.prototype._centroid = true;
|
||||
Arc.prototype.className = 'Arc';
|
||||
Arc.prototype._attrsAffectingSize = [
|
||||
'innerRadius',
|
||||
'outerRadius',
|
||||
'angle',
|
||||
'clockwise',
|
||||
];
|
||||
_registerNode(Arc, true);
|
||||
// add getters setters
|
||||
Factory.addGetterSetter(Arc, 'innerRadius', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set innerRadius
|
||||
* @name Konva.Arc#innerRadius
|
||||
* @method
|
||||
* @param {Number} innerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get inner radius
|
||||
* var innerRadius = arc.innerRadius();
|
||||
*
|
||||
* // set inner radius
|
||||
* arc.innerRadius(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Arc, 'outerRadius', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set outerRadius
|
||||
* @name Konva.Arc#outerRadius
|
||||
* @method
|
||||
* @param {Number} outerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get outer radius
|
||||
* var outerRadius = arc.outerRadius();
|
||||
*
|
||||
* // set outer radius
|
||||
* arc.outerRadius(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Arc, 'angle', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set angle in degrees
|
||||
* @name Konva.Arc#angle
|
||||
* @method
|
||||
* @param {Number} angle
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get angle
|
||||
* var angle = arc.angle();
|
||||
*
|
||||
* // set angle
|
||||
* arc.angle(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Arc, 'clockwise', false, getBooleanValidator());
|
||||
/**
|
||||
* get/set clockwise flag
|
||||
* @name Konva.Arc#clockwise
|
||||
* @method
|
||||
* @param {Boolean} clockwise
|
||||
* @returns {Boolean}
|
||||
* @example
|
||||
* // get clockwise flag
|
||||
* var clockwise = arc.clockwise();
|
||||
*
|
||||
* // draw arc counter-clockwise
|
||||
* arc.clockwise(false);
|
||||
*
|
||||
* // draw arc clockwise
|
||||
* arc.clockwise(true);
|
||||
*/
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import type { LineConfig } from './Line.ts';
|
||||
import { Line } from './Line.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
export interface ArrowConfig extends LineConfig {
|
||||
tension?: number;
|
||||
closed?: boolean;
|
||||
pointerLength?: number;
|
||||
pointerWidth?: number;
|
||||
pointerAtBeginning?: boolean;
|
||||
pointerAtEnding?: boolean;
|
||||
}
|
||||
/**
|
||||
* Arrow constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Line
|
||||
* @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 {Number} config.pointerLength Arrow pointer length. Default value is 10.
|
||||
* @param {Number} config.pointerWidth Arrow pointer width. Default value is 10.
|
||||
* @param {Boolean} config.pointerAtBeginning Do we need to draw pointer on beginning position?. Default false.
|
||||
* @param {Boolean} config.pointerAtEnding Do we need to draw pointer on ending position?. Default true.
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var line = new Konva.Line({
|
||||
* points: [73, 70, 340, 23, 450, 60, 500, 20],
|
||||
* stroke: 'red',
|
||||
* tension: 1,
|
||||
* pointerLength : 10,
|
||||
* pointerWidth : 12
|
||||
* });
|
||||
*/
|
||||
export declare class Arrow extends Line<ArrowConfig> {
|
||||
_getStrokePadding(): number;
|
||||
_sceneFunc(ctx: Context): void;
|
||||
_getPointerAngle(atBeginning: boolean): number;
|
||||
__fillStroke(ctx: Context): void;
|
||||
getSelfRect(): import("../types.ts").IRect;
|
||||
pointerLength: GetSet<number, this>;
|
||||
pointerWidth: GetSet<number, this>;
|
||||
pointerAtEnding: GetSet<boolean, this>;
|
||||
pointerAtBeginning: GetSet<boolean, this>;
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Line } from "./Line.js";
|
||||
import { getNumberValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
import { Path } from "./Path.js";
|
||||
/**
|
||||
* Arrow constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Line
|
||||
* @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 {Number} config.pointerLength Arrow pointer length. Default value is 10.
|
||||
* @param {Number} config.pointerWidth Arrow pointer width. Default value is 10.
|
||||
* @param {Boolean} config.pointerAtBeginning Do we need to draw pointer on beginning position?. Default false.
|
||||
* @param {Boolean} config.pointerAtEnding Do we need to draw pointer on ending position?. Default true.
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var line = new Konva.Line({
|
||||
* points: [73, 70, 340, 23, 450, 60, 500, 20],
|
||||
* stroke: 'red',
|
||||
* tension: 1,
|
||||
* pointerLength : 10,
|
||||
* pointerWidth : 12
|
||||
* });
|
||||
*/
|
||||
export class Arrow extends Line {
|
||||
_getStrokePadding() {
|
||||
// Arrowheads have joins even when the shaft is a single straight line.
|
||||
return super._getStrokePadding(this.pointerAtBeginning() || this.pointerAtEnding()
|
||||
? this.miterLimit() || 10
|
||||
: undefined);
|
||||
}
|
||||
_sceneFunc(ctx) {
|
||||
super._sceneFunc(ctx);
|
||||
const points = this.points();
|
||||
const n = points.length;
|
||||
if (n < 4)
|
||||
return;
|
||||
const length = this.pointerLength();
|
||||
const width = this.pointerWidth();
|
||||
if (this.pointerAtEnding()) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.translate(points[n - 2], points[n - 1]);
|
||||
ctx.rotate(this._getPointerAngle(false));
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(-length, width / 2);
|
||||
ctx.lineTo(-length, -width / 2);
|
||||
ctx.closePath();
|
||||
ctx.restore();
|
||||
this.__fillStroke(ctx);
|
||||
}
|
||||
if (this.pointerAtBeginning()) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.translate(points[0], points[1]);
|
||||
ctx.rotate(this._getPointerAngle(true));
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(-length, width / 2);
|
||||
ctx.lineTo(-length, -width / 2);
|
||||
ctx.closePath();
|
||||
ctx.restore();
|
||||
this.__fillStroke(ctx);
|
||||
}
|
||||
}
|
||||
_getPointerAngle(atBeginning) {
|
||||
const points = this.points();
|
||||
const n = points.length;
|
||||
const fromTension = this.tension() !== 0 && n > 4;
|
||||
const tp = fromTension ? this.getTensionPoints() : points;
|
||||
const ex = atBeginning ? points[0] : points[n - 2], ey = atBeginning ? points[1] : points[n - 1];
|
||||
let dx = 0, dy = 0;
|
||||
if (fromTension && atBeginning) {
|
||||
dx = ex - (tp[0] + tp[2]) / 2;
|
||||
dy = ey - (tp[1] + tp[3]) / 2;
|
||||
}
|
||||
else if (fromTension) {
|
||||
const x = tp[tp.length - 4], y = tp[tp.length - 3];
|
||||
const controlX = tp[tp.length - 2], controlY = tp[tp.length - 1];
|
||||
const lastLength = Path.calcLength(x, y, 'Q', [
|
||||
controlX,
|
||||
controlY,
|
||||
ex,
|
||||
ey,
|
||||
]);
|
||||
const previous = Path.getPointOnQuadraticBezier(lastLength ? Math.max(0, 1 - this.pointerLength() / lastLength) : 0, x, y, controlX, controlY, ex, ey);
|
||||
dx = ex - previous.x;
|
||||
dy = ey - previous.y;
|
||||
}
|
||||
// repeated endpoints give a zero tangent, so walk towards the other end
|
||||
// of the line until a distinct point is found
|
||||
for (let i = atBeginning ? 0 : tp.length - 2; !dx && !dy && i >= 0 && i < tp.length; i += atBeginning ? 2 : -2) {
|
||||
dx = ex - tp[i];
|
||||
dy = ey - tp[i + 1];
|
||||
}
|
||||
const turn = Math.PI * 2;
|
||||
return (Math.atan2(dy, dx) + turn) % turn;
|
||||
}
|
||||
__fillStroke(ctx) {
|
||||
// here is a tricky part
|
||||
// we need to disable dash for arrow pointers
|
||||
const isDashEnabled = this.dashEnabled();
|
||||
if (isDashEnabled) {
|
||||
// manually disable dash for head
|
||||
// it is better not to use setter here,
|
||||
// because it will trigger attr change event
|
||||
this.attrs.dashEnabled = false;
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
ctx.fillStrokeShape(this);
|
||||
// restore old value
|
||||
if (isDashEnabled) {
|
||||
this.attrs.dashEnabled = true;
|
||||
}
|
||||
}
|
||||
getSelfRect() {
|
||||
const lineRect = super.getSelfRect();
|
||||
const points = this.points();
|
||||
if (points.length < 4)
|
||||
return lineRect;
|
||||
let minX = lineRect.x, minY = lineRect.y;
|
||||
let maxX = minX + lineRect.width, maxY = minY + lineRect.height;
|
||||
for (const beginning of [false, true]) {
|
||||
if (!(beginning ? this.pointerAtBeginning() : this.pointerAtEnding()))
|
||||
continue;
|
||||
const index = beginning ? 0 : points.length - 2;
|
||||
const angle = this._getPointerAngle(beginning);
|
||||
const cos = Math.cos(angle), sin = Math.sin(angle);
|
||||
const x = points[index] - this.pointerLength() * cos;
|
||||
const y = points[index + 1] - this.pointerLength() * sin;
|
||||
const dx = Math.abs((this.pointerWidth() * sin) / 2);
|
||||
const dy = Math.abs((this.pointerWidth() * cos) / 2);
|
||||
minX = Math.min(minX, x - dx);
|
||||
minY = Math.min(minY, y - dy);
|
||||
maxX = Math.max(maxX, x + dx);
|
||||
maxY = Math.max(maxY, y + dy);
|
||||
}
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
||||
}
|
||||
}
|
||||
Arrow.prototype.className = 'Arrow';
|
||||
Arrow.prototype._attrsAffectingSize = [
|
||||
...Line.prototype._attrsAffectingSize,
|
||||
'pointerLength',
|
||||
'pointerWidth',
|
||||
'pointerAtBeginning',
|
||||
'pointerAtEnding',
|
||||
];
|
||||
_registerNode(Arrow, true);
|
||||
/**
|
||||
* get/set pointerLength
|
||||
* @name Konva.Arrow#pointerLength
|
||||
* @method
|
||||
* @param {Number} Length of pointer of arrow. The default is 10.
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get length
|
||||
* var pointerLength = line.pointerLength();
|
||||
*
|
||||
* // set length
|
||||
* line.pointerLength(15);
|
||||
*/
|
||||
Factory.addGetterSetter(Arrow, 'pointerLength', 10, getNumberValidator());
|
||||
/**
|
||||
* get/set pointerWidth
|
||||
* @name Konva.Arrow#pointerWidth
|
||||
* @method
|
||||
* @param {Number} Width of pointer of arrow.
|
||||
* The default is 10.
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get width
|
||||
* var pointerWidth = line.pointerWidth();
|
||||
*
|
||||
* // set width
|
||||
* line.pointerWidth(15);
|
||||
*/
|
||||
Factory.addGetterSetter(Arrow, 'pointerWidth', 10, getNumberValidator());
|
||||
/**
|
||||
* get/set pointerAtBeginning
|
||||
* @name Konva.Arrow#pointerAtBeginning
|
||||
* @method
|
||||
* @param {Boolean} pointerAtBeginning Whether to draw a pointer at the beginning of the arrow. The default is false.
|
||||
* @returns {Boolean}
|
||||
* @example
|
||||
* // get value
|
||||
* var pointerAtBeginning = line.pointerAtBeginning();
|
||||
*
|
||||
* // set value
|
||||
* line.pointerAtBeginning(true);
|
||||
*/
|
||||
Factory.addGetterSetter(Arrow, 'pointerAtBeginning', false);
|
||||
/**
|
||||
* get/set pointerAtEnding
|
||||
* @name Konva.Arrow#pointerAtEnding
|
||||
* @method
|
||||
* @param {Boolean} pointerAtEnding Whether to draw a pointer at the end of the arrow. The default is true.
|
||||
* @returns {Boolean}
|
||||
* @example
|
||||
* // get value
|
||||
* var pointerAtEnding = line.pointerAtEnding();
|
||||
*
|
||||
* // set value
|
||||
* line.pointerAtEnding(false);
|
||||
*/
|
||||
Factory.addGetterSetter(Arrow, 'pointerAtEnding', true);
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
export interface CircleConfig extends ShapeConfig {
|
||||
radius?: number;
|
||||
}
|
||||
/**
|
||||
* Circle constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} config.radius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* // create circle
|
||||
* var circle = new Konva.Circle({
|
||||
* radius: 40,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5
|
||||
* });
|
||||
*/
|
||||
export declare class Circle extends Shape<CircleConfig> {
|
||||
_getStrokePadding(): number;
|
||||
_sceneFunc(context: Context): void;
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
setWidth(width: number): void;
|
||||
setHeight(height: number): void;
|
||||
radius: GetSet<number, this>;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { getNumberValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
/**
|
||||
* Circle constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} config.radius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* // create circle
|
||||
* var circle = new Konva.Circle({
|
||||
* radius: 40,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5
|
||||
* });
|
||||
*/
|
||||
export class Circle extends Shape {
|
||||
_getStrokePadding() {
|
||||
return super._getStrokePadding(1);
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
context.beginPath();
|
||||
context.arc(0, 0, Math.abs(this.attrs.radius || 0), 0, Math.PI * 2, false);
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
getWidth() {
|
||||
return Math.abs(this.radius()) * 2;
|
||||
}
|
||||
getHeight() {
|
||||
return Math.abs(this.radius()) * 2;
|
||||
}
|
||||
setWidth(width) {
|
||||
this.radius(width / 2);
|
||||
}
|
||||
setHeight(height) {
|
||||
this.radius(height / 2);
|
||||
}
|
||||
}
|
||||
Circle.prototype._centroid = true;
|
||||
Circle.prototype.className = 'Circle';
|
||||
Circle.prototype._attrsAffectingSize = ['radius'];
|
||||
_registerNode(Circle, true);
|
||||
/**
|
||||
* get/set radius
|
||||
* @name Konva.Circle#radius
|
||||
* @method
|
||||
* @param {Number} radius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get radius
|
||||
* var radius = circle.radius();
|
||||
*
|
||||
* // set radius
|
||||
* circle.radius(10);
|
||||
*/
|
||||
Factory.addGetterSetter(Circle, 'radius', 0, getNumberValidator());
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
import type { GetSet, Vector2d } from '../types.ts';
|
||||
export interface EllipseConfig extends ShapeConfig {
|
||||
radius?: Vector2d;
|
||||
radiusX?: number;
|
||||
radiusY?: number;
|
||||
}
|
||||
/**
|
||||
* Ellipse constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Object} config.radius defines x and y radius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var ellipse = new Konva.Ellipse({
|
||||
* radius : {
|
||||
* x : 50,
|
||||
* y : 50
|
||||
* },
|
||||
* fill: 'red'
|
||||
* });
|
||||
*/
|
||||
export declare class Ellipse extends Shape<EllipseConfig> {
|
||||
_getStrokePadding(): number;
|
||||
_sceneFunc(context: Context): void;
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
setWidth(width: number): void;
|
||||
setHeight(height: number): void;
|
||||
radius: GetSet<Vector2d, this>;
|
||||
radiusX: GetSet<number, this>;
|
||||
radiusY: GetSet<number, this>;
|
||||
}
|
||||
/**
|
||||
* get/set radius y
|
||||
* @name Konva.Ellipse#radiusY
|
||||
* @method
|
||||
* @param {Number} y
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get radius y
|
||||
* var radiusY = ellipse.radiusY();
|
||||
*
|
||||
* // set radius y
|
||||
* ellipse.radiusY(200);
|
||||
*/
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { getNumberValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
/**
|
||||
* Ellipse constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Object} config.radius defines x and y radius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var ellipse = new Konva.Ellipse({
|
||||
* radius : {
|
||||
* x : 50,
|
||||
* y : 50
|
||||
* },
|
||||
* fill: 'red'
|
||||
* });
|
||||
*/
|
||||
export class Ellipse extends Shape {
|
||||
_getStrokePadding() {
|
||||
return super._getStrokePadding(1);
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
const rx = Math.abs(this.radiusX()), ry = Math.abs(this.radiusY());
|
||||
context.beginPath();
|
||||
context.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2, false);
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
getWidth() {
|
||||
return Math.abs(this.radiusX()) * 2;
|
||||
}
|
||||
getHeight() {
|
||||
return Math.abs(this.radiusY()) * 2;
|
||||
}
|
||||
setWidth(width) {
|
||||
this.radiusX(width / 2);
|
||||
}
|
||||
setHeight(height) {
|
||||
this.radiusY(height / 2);
|
||||
}
|
||||
}
|
||||
Ellipse.prototype.className = 'Ellipse';
|
||||
Ellipse.prototype._centroid = true;
|
||||
Ellipse.prototype._attrsAffectingSize = ['radiusX', 'radiusY'];
|
||||
_registerNode(Ellipse, true);
|
||||
// add getters setters
|
||||
Factory.addComponentsGetterSetter(Ellipse, 'radius', ['x', 'y']);
|
||||
/**
|
||||
* get/set radius
|
||||
* @name Konva.Ellipse#radius
|
||||
* @method
|
||||
* @param {Object} radius
|
||||
* @param {Number} radius.x
|
||||
* @param {Number} radius.y
|
||||
* @returns {Object}
|
||||
* @example
|
||||
* // get radius
|
||||
* var radius = ellipse.radius();
|
||||
*
|
||||
* // set radius
|
||||
* ellipse.radius({
|
||||
* x: 200,
|
||||
* y: 100
|
||||
* });
|
||||
*/
|
||||
Factory.addGetterSetter(Ellipse, 'radiusX', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set radius x
|
||||
* @name Konva.Ellipse#radiusX
|
||||
* @method
|
||||
* @param {Number} x
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get radius x
|
||||
* var radiusX = ellipse.radiusX();
|
||||
*
|
||||
* // set radius x
|
||||
* ellipse.radiusX(200);
|
||||
*/
|
||||
Factory.addGetterSetter(Ellipse, 'radiusY', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set radius y
|
||||
* @name Konva.Ellipse#radiusY
|
||||
* @method
|
||||
* @param {Number} y
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get radius y
|
||||
* var radiusY = ellipse.radiusY();
|
||||
*
|
||||
* // set radius y
|
||||
* ellipse.radiusY(200);
|
||||
*/
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet, IRect } from '../types.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
export interface ImageConfig extends ShapeConfig {
|
||||
image: CanvasImageSource | undefined;
|
||||
crop?: IRect;
|
||||
cornerRadius?: number | number[];
|
||||
}
|
||||
/**
|
||||
* Image constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Image} config.image
|
||||
* @param {Object} [config.crop]
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var imageObj = new Image();
|
||||
* imageObj.onload = function() {
|
||||
* var image = new Konva.Image({
|
||||
* x: 200,
|
||||
* y: 50,
|
||||
* image: imageObj,
|
||||
* width: 100,
|
||||
* height: 100
|
||||
* });
|
||||
* };
|
||||
* imageObj.src = '/path/to/image.jpg'
|
||||
*/
|
||||
export declare class Image extends Shape<ImageConfig> {
|
||||
private _loadListener;
|
||||
constructor(attrs?: ImageConfig);
|
||||
_setImageLoad(): void;
|
||||
_removeImageLoad(image: any): void;
|
||||
destroy(): this;
|
||||
_useBufferCanvas(forceFill?: boolean, opacity?: any): boolean;
|
||||
_getStrokePadding(): number;
|
||||
_sceneFunc(context: Context): void;
|
||||
_hitFunc(context: Context): void;
|
||||
getWidth(): any;
|
||||
getHeight(): any;
|
||||
/**
|
||||
* load image from given url and create `Konva.Image` instance
|
||||
* @method
|
||||
* @memberof Konva.Image
|
||||
* @param {String} url image source
|
||||
* @param {Function} callback with Konva.Image instance as first argument
|
||||
* @param {Function} onError optional error handler
|
||||
* @example
|
||||
* Konva.Image.fromURL(imageURL, function(image){
|
||||
* // image is Konva.Image instance
|
||||
* layer.add(image);
|
||||
* });
|
||||
*/
|
||||
static fromURL(url: string, callback: (img: Image) => void, onError?: OnErrorEventHandler): void;
|
||||
image: GetSet<CanvasImageSource | undefined, this>;
|
||||
crop: GetSet<IRect, this>;
|
||||
cropX: GetSet<number, this>;
|
||||
cropY: GetSet<number, this>;
|
||||
cropWidth: GetSet<number, this>;
|
||||
cropHeight: GetSet<number, this>;
|
||||
cornerRadius: GetSet<number | number[], this>;
|
||||
}
|
||||
/**
|
||||
* get/set crop height
|
||||
* @name Konva.Image#cropHeight
|
||||
* @method
|
||||
* @param {Number} height
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get crop height
|
||||
* var cropHeight = image.cropHeight();
|
||||
*
|
||||
* // set crop height
|
||||
* image.cropHeight(20);
|
||||
*/
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
import { Util } from "../Util.js";
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
import { getNumberOrArrayOfNumbersValidator, getNumberValidator, } from "../Validators.js";
|
||||
/**
|
||||
* Image constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Image} config.image
|
||||
* @param {Object} [config.crop]
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var imageObj = new Image();
|
||||
* imageObj.onload = function() {
|
||||
* var image = new Konva.Image({
|
||||
* x: 200,
|
||||
* y: 50,
|
||||
* image: imageObj,
|
||||
* width: 100,
|
||||
* height: 100
|
||||
* });
|
||||
* };
|
||||
* imageObj.src = '/path/to/image.jpg'
|
||||
*/
|
||||
export class Image extends Shape {
|
||||
constructor(attrs) {
|
||||
super(attrs);
|
||||
this._loadListener = () => {
|
||||
this._requestDraw();
|
||||
};
|
||||
this.on('imageChange.konva', (props) => {
|
||||
this._removeImageLoad(props.oldVal);
|
||||
this._setImageLoad();
|
||||
});
|
||||
this._setImageLoad();
|
||||
}
|
||||
_setImageLoad() {
|
||||
var _a;
|
||||
const image = this.image();
|
||||
// A fresh `new Image()` with no src already reports complete === true,
|
||||
// so it still needs the listener.
|
||||
if (((image === null || image === void 0 ? void 0 : image.complete) && image.src) || (image === null || image === void 0 ? void 0 : image.readyState) >= 2) {
|
||||
return;
|
||||
}
|
||||
(_a = image === null || image === void 0 ? void 0 : image.addEventListener) === null || _a === void 0 ? void 0 : _a.call(image, 'videoWidth' in image ? 'loadeddata' : 'load', this._loadListener);
|
||||
}
|
||||
_removeImageLoad(image) {
|
||||
var _a;
|
||||
(_a = image === null || image === void 0 ? void 0 : image.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(image, 'videoWidth' in image ? 'loadeddata' : 'load', this._loadListener);
|
||||
}
|
||||
destroy() {
|
||||
this._removeImageLoad(this.image());
|
||||
super.destroy();
|
||||
return this;
|
||||
}
|
||||
_useBufferCanvas(forceFill, opacity = this.getAbsoluteOpacity()) {
|
||||
if (this.attrs.perfectDrawEnabled === false)
|
||||
return false;
|
||||
const hasCornerRadius = !!this.cornerRadius();
|
||||
const hasShadow = this.hasShadow();
|
||||
if (hasCornerRadius && hasShadow) {
|
||||
return true;
|
||||
}
|
||||
return super._useBufferCanvas(true, opacity);
|
||||
}
|
||||
_getStrokePadding() {
|
||||
return super._getStrokePadding(this.strokeScaleEnabled() ? 1 : undefined);
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
const width = this.getWidth();
|
||||
const height = this.getHeight();
|
||||
const cornerRadius = this.cornerRadius();
|
||||
const image = this.attrs.image;
|
||||
if (this.hasFill() || this.hasStroke() || cornerRadius) {
|
||||
context.beginPath();
|
||||
cornerRadius
|
||||
? Util.drawRoundedRectPath(context, width, height, cornerRadius)
|
||||
: context.rect(0, 0, width, height);
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
if (image) {
|
||||
if (cornerRadius) {
|
||||
context.clip();
|
||||
}
|
||||
const cropWidth = this.attrs.cropWidth;
|
||||
const cropHeight = this.attrs.cropHeight;
|
||||
if (cropWidth && cropHeight) {
|
||||
context.drawImage(image, this.cropX(), this.cropY(), cropWidth, cropHeight, 0, 0, width, height);
|
||||
}
|
||||
else {
|
||||
context.drawImage(image, 0, 0, width, height);
|
||||
}
|
||||
}
|
||||
// If you need to draw later, you need to execute save/restore
|
||||
}
|
||||
_hitFunc(context) {
|
||||
const width = this.width(), height = this.height(), cornerRadius = this.cornerRadius();
|
||||
context.beginPath();
|
||||
if (!cornerRadius) {
|
||||
context.rect(0, 0, width, height);
|
||||
}
|
||||
else {
|
||||
Util.drawRoundedRectPath(context, width, height, cornerRadius);
|
||||
}
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
getWidth() {
|
||||
var _a, _b, _c;
|
||||
const image = this.image();
|
||||
return (_c = (_b = (_a = this.attrs.width) !== null && _a !== void 0 ? _a : image === null || image === void 0 ? void 0 : image.videoWidth) !== null && _b !== void 0 ? _b : image === null || image === void 0 ? void 0 : image.width) !== null && _c !== void 0 ? _c : 0;
|
||||
}
|
||||
getHeight() {
|
||||
var _a, _b, _c;
|
||||
const image = this.image();
|
||||
return (_c = (_b = (_a = this.attrs.height) !== null && _a !== void 0 ? _a : image === null || image === void 0 ? void 0 : image.videoHeight) !== null && _b !== void 0 ? _b : image === null || image === void 0 ? void 0 : image.height) !== null && _c !== void 0 ? _c : 0;
|
||||
}
|
||||
/**
|
||||
* load image from given url and create `Konva.Image` instance
|
||||
* @method
|
||||
* @memberof Konva.Image
|
||||
* @param {String} url image source
|
||||
* @param {Function} callback with Konva.Image instance as first argument
|
||||
* @param {Function} onError optional error handler
|
||||
* @example
|
||||
* Konva.Image.fromURL(imageURL, function(image){
|
||||
* // image is Konva.Image instance
|
||||
* layer.add(image);
|
||||
* });
|
||||
*/
|
||||
static fromURL(url, callback, onError = null) {
|
||||
const img = Util.createImageElement();
|
||||
img.onload = () => {
|
||||
img.onload = img.onerror = null;
|
||||
callback(new Image({ image: img }));
|
||||
};
|
||||
img.onerror = onError;
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = url;
|
||||
}
|
||||
}
|
||||
Image.prototype.className = 'Image';
|
||||
Image.prototype._attrsAffectingSize = ['image'];
|
||||
_registerNode(Image, true);
|
||||
/**
|
||||
* get/set corner radius
|
||||
* @method
|
||||
* @name Konva.Image#cornerRadius
|
||||
* @param {Number} cornerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get corner radius
|
||||
* var cornerRadius = image.cornerRadius();
|
||||
*
|
||||
* // set corner radius
|
||||
* image.cornerRadius(10);
|
||||
*
|
||||
* // set different corner radius values
|
||||
* // top-left, top-right, bottom-right, bottom-left
|
||||
* image.cornerRadius([0, 10, 20, 30]);
|
||||
*/
|
||||
Factory.addGetterSetter(Image, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4));
|
||||
/**
|
||||
* get/set image source. It can be image, canvas or video element
|
||||
* @name Konva.Image#image
|
||||
* @method
|
||||
* @param {Object} image source
|
||||
* @returns {Object}
|
||||
* @example
|
||||
* // get value
|
||||
* var image = shape.image();
|
||||
*
|
||||
* // set value
|
||||
* shape.image(img);
|
||||
*/
|
||||
Factory.addGetterSetter(Image, 'image');
|
||||
Factory.addComponentsGetterSetter(Image, 'crop', ['x', 'y', 'width', 'height']);
|
||||
/**
|
||||
* get/set crop
|
||||
* @method
|
||||
* @name Konva.Image#crop
|
||||
* @param {Object} crop
|
||||
* @param {Number} crop.x
|
||||
* @param {Number} crop.y
|
||||
* @param {Number} crop.width
|
||||
* @param {Number} crop.height
|
||||
* @returns {Object}
|
||||
* @example
|
||||
* // get crop
|
||||
* var crop = image.crop();
|
||||
*
|
||||
* // set crop
|
||||
* image.crop({
|
||||
* x: 20,
|
||||
* y: 20,
|
||||
* width: 20,
|
||||
* height: 20
|
||||
* });
|
||||
*/
|
||||
Factory.addGetterSetter(Image, 'cropX', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set crop x
|
||||
* @method
|
||||
* @name Konva.Image#cropX
|
||||
* @param {Number} x
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get crop x
|
||||
* var cropX = image.cropX();
|
||||
*
|
||||
* // set crop x
|
||||
* image.cropX(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Image, 'cropY', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set crop y
|
||||
* @name Konva.Image#cropY
|
||||
* @method
|
||||
* @param {Number} y
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get crop y
|
||||
* var cropY = image.cropY();
|
||||
*
|
||||
* // set crop y
|
||||
* image.cropY(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Image, 'cropWidth', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set crop width
|
||||
* @name Konva.Image#cropWidth
|
||||
* @method
|
||||
* @param {Number} width
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get crop width
|
||||
* var cropWidth = image.cropWidth();
|
||||
*
|
||||
* // set crop width
|
||||
* image.cropWidth(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Image, 'cropHeight', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set crop height
|
||||
* @name Konva.Image#cropHeight
|
||||
* @method
|
||||
* @param {Number} height
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get crop height
|
||||
* var cropHeight = image.cropHeight();
|
||||
*
|
||||
* // set crop height
|
||||
* image.cropHeight(20);
|
||||
*/
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import { Group } from '../Group.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
import type { ContainerConfig } from '../Container.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
import { Text } from './Text.ts';
|
||||
export interface LabelConfig extends ContainerConfig {
|
||||
}
|
||||
declare const NONE = "none";
|
||||
/**
|
||||
* Label constructor. Labels are groups that contain a Text and Tag shape
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @param {Object} config
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* // create label
|
||||
* var label = new Konva.Label({
|
||||
* x: 100,
|
||||
* y: 100,
|
||||
* draggable: true
|
||||
* });
|
||||
*
|
||||
* // add a tag to the label
|
||||
* label.add(new Konva.Tag({
|
||||
* fill: '#bbb',
|
||||
* stroke: '#333',
|
||||
* shadowColor: 'black',
|
||||
* shadowBlur: 10,
|
||||
* shadowOffset: { x: 10, y: 10 },
|
||||
* shadowOpacity: 0.2,
|
||||
* lineJoin: 'round',
|
||||
* pointerDirection: 'up',
|
||||
* pointerWidth: 20,
|
||||
* pointerHeight: 20,
|
||||
* cornerRadius: 5
|
||||
* }));
|
||||
*
|
||||
* // add text to the label
|
||||
* label.add(new Konva.Text({
|
||||
* text: 'Hello World!',
|
||||
* fontSize: 50,
|
||||
* lineHeight: 1.2,
|
||||
* padding: 10,
|
||||
* fill: 'green'
|
||||
* }));
|
||||
*/
|
||||
export declare class Label extends Group {
|
||||
constructor(config?: LabelConfig);
|
||||
/**
|
||||
* get Text shape for the label. You need to access the Text shape in order to update
|
||||
* the text properties
|
||||
* @name Konva.Label#getText
|
||||
* @method
|
||||
* @example
|
||||
* label.getText().fill('red')
|
||||
*/
|
||||
getText(): Text;
|
||||
/**
|
||||
* get Tag shape for the label. You need to access the Tag shape in order to update
|
||||
* the pointer properties and the corner radius
|
||||
* @name Konva.Label#getTag
|
||||
* @method
|
||||
*/
|
||||
getTag(): Tag;
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
_sync(): void;
|
||||
}
|
||||
export interface TagConfig extends ShapeConfig {
|
||||
pointerDirection?: string;
|
||||
pointerWidth?: number;
|
||||
pointerHeight?: number;
|
||||
cornerRadius?: number | Array<number>;
|
||||
}
|
||||
/**
|
||||
* Tag constructor. A Tag can be configured
|
||||
* to have a pointer element that points up, right, down, or left
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @param {Object} config
|
||||
* @param {String} [config.pointerDirection] can be up, right, down, left, or none; the default
|
||||
* is none. When a pointer is present, the positioning of the label is relative to the tip of the pointer.
|
||||
* @param {Number} [config.pointerWidth]
|
||||
* @param {Number} [config.pointerHeight]
|
||||
* @param {Number} [config.cornerRadius]
|
||||
*/
|
||||
export declare class Tag extends Shape<TagConfig> {
|
||||
_sceneFunc(context: Context): void;
|
||||
getSelfRect(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
pointerDirection: GetSet<'left' | 'up' | 'right' | 'down' | typeof NONE, this>;
|
||||
pointerWidth: GetSet<number, this>;
|
||||
pointerHeight: GetSet<number, this>;
|
||||
cornerRadius: GetSet<number | number[], this>;
|
||||
}
|
||||
export {};
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Util } from "../Util.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { Group } from "../Group.js";
|
||||
import { getNumberOrArrayOfNumbersValidator, getNumberValidator, } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
import { Text } from "./Text.js";
|
||||
// constants
|
||||
const ATTR_CHANGE_LIST = [
|
||||
...Text.prototype._attrsAffectingSize,
|
||||
'width',
|
||||
'height',
|
||||
'pointerDirection',
|
||||
'pointerWidth',
|
||||
'pointerHeight',
|
||||
], CHANGE_EVENTS = ATTR_CHANGE_LIST.map((attr) => attr + 'Change.konva').join(' '), NONE = 'none', UP = 'up', RIGHT = 'right', DOWN = 'down', LEFT = 'left';
|
||||
/**
|
||||
* Label constructor. Labels are groups that contain a Text and Tag shape
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @param {Object} config
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* // create label
|
||||
* var label = new Konva.Label({
|
||||
* x: 100,
|
||||
* y: 100,
|
||||
* draggable: true
|
||||
* });
|
||||
*
|
||||
* // add a tag to the label
|
||||
* label.add(new Konva.Tag({
|
||||
* fill: '#bbb',
|
||||
* stroke: '#333',
|
||||
* shadowColor: 'black',
|
||||
* shadowBlur: 10,
|
||||
* shadowOffset: { x: 10, y: 10 },
|
||||
* shadowOpacity: 0.2,
|
||||
* lineJoin: 'round',
|
||||
* pointerDirection: 'up',
|
||||
* pointerWidth: 20,
|
||||
* pointerHeight: 20,
|
||||
* cornerRadius: 5
|
||||
* }));
|
||||
*
|
||||
* // add text to the label
|
||||
* label.add(new Konva.Text({
|
||||
* text: 'Hello World!',
|
||||
* fontSize: 50,
|
||||
* lineHeight: 1.2,
|
||||
* padding: 10,
|
||||
* fill: 'green'
|
||||
* }));
|
||||
*/
|
||||
export class Label extends Group {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this._sync = this._sync.bind(this);
|
||||
this.on('add.konva', function (evt) {
|
||||
// re-adding the same child must not stack another listener
|
||||
evt.child.off(CHANGE_EVENTS, this._sync).on(CHANGE_EVENTS, this._sync);
|
||||
this._sync();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* get Text shape for the label. You need to access the Text shape in order to update
|
||||
* the text properties
|
||||
* @name Konva.Label#getText
|
||||
* @method
|
||||
* @example
|
||||
* label.getText().fill('red')
|
||||
*/
|
||||
getText() {
|
||||
return this.find('Text')[0];
|
||||
}
|
||||
/**
|
||||
* get Tag shape for the label. You need to access the Tag shape in order to update
|
||||
* the pointer properties and the corner radius
|
||||
* @name Konva.Label#getTag
|
||||
* @method
|
||||
*/
|
||||
getTag() {
|
||||
return this.find('Tag')[0];
|
||||
}
|
||||
getWidth() {
|
||||
return this.getText().width();
|
||||
}
|
||||
getHeight() {
|
||||
return this.getText().height();
|
||||
}
|
||||
_sync() {
|
||||
let text = this.getText(), tag = this.getTag(), width, height, pointerDirection, pointerWidth, x, y, pointerHeight;
|
||||
if (text && tag) {
|
||||
width = text.width();
|
||||
height = text.height();
|
||||
pointerDirection = tag.pointerDirection();
|
||||
pointerWidth = tag.pointerWidth();
|
||||
pointerHeight = tag.pointerHeight();
|
||||
x = 0;
|
||||
y = 0;
|
||||
switch (pointerDirection) {
|
||||
case UP:
|
||||
x = width / 2;
|
||||
y = -1 * pointerHeight;
|
||||
break;
|
||||
case RIGHT:
|
||||
x = width + pointerWidth;
|
||||
y = height / 2;
|
||||
break;
|
||||
case DOWN:
|
||||
x = width / 2;
|
||||
y = height + pointerHeight;
|
||||
break;
|
||||
case LEFT:
|
||||
x = -1 * pointerWidth;
|
||||
y = height / 2;
|
||||
break;
|
||||
}
|
||||
tag.setAttrs({
|
||||
x: -1 * x,
|
||||
y: -1 * y,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
text.setAttrs({
|
||||
x: -1 * x,
|
||||
y: -1 * y,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Label.prototype.className = 'Label';
|
||||
_registerNode(Label);
|
||||
/**
|
||||
* Tag constructor. A Tag can be configured
|
||||
* to have a pointer element that points up, right, down, or left
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @param {Object} config
|
||||
* @param {String} [config.pointerDirection] can be up, right, down, left, or none; the default
|
||||
* is none. When a pointer is present, the positioning of the label is relative to the tip of the pointer.
|
||||
* @param {Number} [config.pointerWidth]
|
||||
* @param {Number} [config.pointerHeight]
|
||||
* @param {Number} [config.cornerRadius]
|
||||
*/
|
||||
export class Tag extends Shape {
|
||||
_sceneFunc(context) {
|
||||
const width = this.width(), height = this.height(), pointerDirection = this.pointerDirection(), pointerWidth = this.pointerWidth(), pointerHeight = this.pointerHeight(), cornerRadius = this.cornerRadius();
|
||||
const [topLeft, topRight, bottomRight, bottomLeft] = Util._cornerRadii(cornerRadius, width, height);
|
||||
context.beginPath();
|
||||
context.moveTo(topLeft, 0);
|
||||
if (pointerDirection === UP) {
|
||||
context.lineTo((width - pointerWidth) / 2, 0);
|
||||
context.lineTo(width / 2, -1 * pointerHeight);
|
||||
context.lineTo((width + pointerWidth) / 2, 0);
|
||||
}
|
||||
context.lineTo(width - topRight, 0);
|
||||
context.arc(width - topRight, topRight, topRight, (Math.PI * 3) / 2, 0, false);
|
||||
if (pointerDirection === RIGHT) {
|
||||
context.lineTo(width, (height - pointerHeight) / 2);
|
||||
context.lineTo(width + pointerWidth, height / 2);
|
||||
context.lineTo(width, (height + pointerHeight) / 2);
|
||||
}
|
||||
context.lineTo(width, height - bottomRight);
|
||||
context.arc(width - bottomRight, height - bottomRight, bottomRight, 0, Math.PI / 2, false);
|
||||
if (pointerDirection === DOWN) {
|
||||
context.lineTo((width + pointerWidth) / 2, height);
|
||||
context.lineTo(width / 2, height + pointerHeight);
|
||||
context.lineTo((width - pointerWidth) / 2, height);
|
||||
}
|
||||
context.lineTo(bottomLeft, height);
|
||||
context.arc(bottomLeft, height - bottomLeft, bottomLeft, Math.PI / 2, Math.PI, false);
|
||||
if (pointerDirection === LEFT) {
|
||||
context.lineTo(0, (height + pointerHeight) / 2);
|
||||
context.lineTo(-1 * pointerWidth, height / 2);
|
||||
context.lineTo(0, (height - pointerHeight) / 2);
|
||||
}
|
||||
context.lineTo(0, topLeft);
|
||||
context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false);
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
getSelfRect() {
|
||||
const pointerWidth = this.pointerWidth(), pointerHeight = this.pointerHeight(), direction = this.pointerDirection(), width = this.width(), height = this.height();
|
||||
// The pointer base is centred on its side and can be longer than it.
|
||||
if (direction === UP || direction === DOWN) {
|
||||
return {
|
||||
x: Math.min(0, (width - pointerWidth) / 2),
|
||||
y: direction === UP ? -pointerHeight : 0,
|
||||
width: Math.max(width, pointerWidth),
|
||||
height: height + pointerHeight,
|
||||
};
|
||||
}
|
||||
if (direction === LEFT || direction === RIGHT) {
|
||||
return {
|
||||
x: direction === LEFT ? -pointerWidth : 0,
|
||||
y: Math.min(0, (height - pointerHeight) / 2),
|
||||
width: width + pointerWidth,
|
||||
height: Math.max(height, pointerHeight),
|
||||
};
|
||||
}
|
||||
return { x: 0, y: 0, width, height };
|
||||
}
|
||||
}
|
||||
Tag.prototype.className = 'Tag';
|
||||
_registerNode(Tag, true);
|
||||
/**
|
||||
* get/set pointer direction
|
||||
* @name Konva.Tag#pointerDirection
|
||||
* @method
|
||||
* @param {String} pointerDirection can be up, right, down, left, or none. The default is none.
|
||||
* @returns {String}
|
||||
* @example
|
||||
* tag.pointerDirection('right');
|
||||
*/
|
||||
Factory.addGetterSetter(Tag, 'pointerDirection', NONE);
|
||||
/**
|
||||
* get/set pointer width
|
||||
* @name Konva.Tag#pointerWidth
|
||||
* @method
|
||||
* @param {Number} pointerWidth
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* tag.pointerWidth(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Tag, 'pointerWidth', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set pointer height
|
||||
* @method
|
||||
* @name Konva.Tag#pointerHeight
|
||||
* @param {Number} pointerHeight
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* tag.pointerHeight(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Tag, 'pointerHeight', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set cornerRadius
|
||||
* @name Konva.Tag#cornerRadius
|
||||
* @method
|
||||
* @param {Number} cornerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* tag.cornerRadius(20);
|
||||
*
|
||||
* // set different corner radius values
|
||||
* // top-left, top-right, bottom-right, bottom-left
|
||||
* tag.cornerRadius([0, 10, 20, 30]);
|
||||
*/
|
||||
Factory.addGetterSetter(Tag, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4));
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
export type LinePoints = number[] | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array;
|
||||
export interface LineConfig extends ShapeConfig {
|
||||
points?: LinePoints;
|
||||
tension?: number;
|
||||
closed?: boolean;
|
||||
bezier?: boolean;
|
||||
}
|
||||
/**
|
||||
* 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 declare class Line<Config extends LineConfig = LineConfig> extends Shape<Config> {
|
||||
constructor(config?: Config);
|
||||
_hasTension(): boolean;
|
||||
/**
|
||||
* 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: (x0: number, y0: number, cpx: number, cpy: number, x: number, y: number) => void, onCubic: (x0: number, y0: number, cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number) => void): void;
|
||||
_getStrokePadding(miterLimit?: number | undefined): number;
|
||||
_sceneFunc(context: Context): void;
|
||||
getTensionPoints(): any;
|
||||
_getTensionPoints(): number[];
|
||||
_getTensionPointsClosed(): number[];
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
getSelfRect(): import("../types.ts").IRect;
|
||||
closed: GetSet<boolean, this>;
|
||||
bezier: GetSet<boolean, this>;
|
||||
tension: GetSet<number, this>;
|
||||
points: GetSet<number[], this, LinePoints | null | undefined>;
|
||||
}
|
||||
/**
|
||||
* 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]));
|
||||
*/
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
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]));
|
||||
*/
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet, PathSegment } from '../types.ts';
|
||||
export interface PathConfig extends ShapeConfig {
|
||||
data?: string;
|
||||
}
|
||||
/**
|
||||
* Path constructor.
|
||||
* @author Jason Follas
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {String} config.data SVG data string
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var path = new Konva.Path({
|
||||
* x: 240,
|
||||
* y: 40,
|
||||
* data: 'M12.582,9.551C3.251,16.237,0.921,29.021,7.08,38.564l-2.36,1.689l4.893,2.262l4.893,2.262l-0.568-5.36l-0.567-5.359l-2.365,1.694c-4.657-7.375-2.83-17.185,4.352-22.33c7.451-5.338,17.817-3.625,23.156,3.824c5.337,7.449,3.625,17.813-3.821,23.152l2.857,3.988c9.617-6.893,11.827-20.277,4.935-29.896C35.591,4.87,22.204,2.658,12.582,9.551z',
|
||||
* fill: 'green',
|
||||
* scaleX: 2,
|
||||
* scaleY: 2
|
||||
* });
|
||||
*/
|
||||
export declare class Path extends Shape<PathConfig> {
|
||||
dataArray: PathSegment[];
|
||||
pathLength: number;
|
||||
constructor(config?: PathConfig);
|
||||
_readDataAttribute(): void;
|
||||
_sceneFunc(context: any): void;
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
getSelfRect(): import("../types.ts").IRect;
|
||||
/**
|
||||
* Return length of the path.
|
||||
* @method
|
||||
* @name Konva.Path#getLength
|
||||
* @returns {Number} length
|
||||
* @example
|
||||
* var length = path.getLength();
|
||||
*/
|
||||
getLength(): number;
|
||||
/**
|
||||
* Get point on path at specific length of the path
|
||||
* @method
|
||||
* @name Konva.Path#getPointAtLength
|
||||
* @param {Number} length length
|
||||
* @returns {Object} point {x,y} point
|
||||
* @example
|
||||
* var point = path.getPointAtLength(10);
|
||||
*/
|
||||
getPointAtLength(length: number): {
|
||||
x: number;
|
||||
y: number;
|
||||
} | null;
|
||||
data: GetSet<string, this>;
|
||||
static getLineLength(x1: any, y1: any, x2: any, y2: any): number;
|
||||
static getPathLength(dataArray: PathSegment[]): number;
|
||||
static getPointAtLengthOfDataArray(length: number, dataArray: PathSegment[], cursor?: {
|
||||
index: number;
|
||||
offset: number;
|
||||
}): {
|
||||
x: number;
|
||||
y: number;
|
||||
} | null;
|
||||
static getPointOnLine(dist: number, P1x: number, P1y: number, P2x: number, P2y: number, fromX?: number, fromY?: number): {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
static getPointOnCubicBezier(pct: number, P1x: number, P1y: number, P2x: number, P2y: number, P3x: number, P3y: number, P4x: number, P4y: number): {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
static getPointOnQuadraticBezier(pct: any, P1x: any, P1y: any, P2x: any, P2y: any, P3x: any, P3y: any): {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
static getPointOnEllipticalArc(cx: number, cy: number, rx: number, ry: number, theta: number, psi: number): {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
static parsePathData(data: any): PathSegment[];
|
||||
/**
|
||||
* Walks an arc in one degree steps, accumulating its length. Returns the
|
||||
* angle `length` along the arc, or its end angle and total length when
|
||||
* `length` is past the end.
|
||||
*/
|
||||
private static _walkArc;
|
||||
static calcLength(x: any, y: any, cmd: any, points: any): number;
|
||||
static convertEndpointToCenterParameterization(x1: number, y1: number, x2: number, y2: number, fa: number, fs: number, rx: number, ry: number, psiDeg: number): number[];
|
||||
}
|
||||
/**
|
||||
* get width of the path. It is computed from the path data and cannot be set
|
||||
* @name Konva.Path#width
|
||||
* @method
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* var width = path.width();
|
||||
*/
|
||||
/**
|
||||
* get height of the path. It is computed from the path data and cannot be set
|
||||
* @name Konva.Path#height
|
||||
* @method
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* var height = path.height();
|
||||
*/
|
||||
+739
@@ -0,0 +1,739 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { Util } from "../Util.js";
|
||||
import { getCubicArcLength, getCubicExtremaPoints, getQuadraticArcLength, getQuadraticExtremaPoints, t2length, } from "../BezierFunctions.js";
|
||||
// how many numbers each path command takes
|
||||
const PARAM_COUNT = {
|
||||
m: 2,
|
||||
l: 2,
|
||||
h: 1,
|
||||
v: 1,
|
||||
c: 6,
|
||||
s: 4,
|
||||
q: 4,
|
||||
t: 2,
|
||||
a: 7,
|
||||
z: 0,
|
||||
};
|
||||
const TAU = Math.PI * 2;
|
||||
/**
|
||||
* Path constructor.
|
||||
* @author Jason Follas
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {String} config.data SVG data string
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var path = new Konva.Path({
|
||||
* x: 240,
|
||||
* y: 40,
|
||||
* data: 'M12.582,9.551C3.251,16.237,0.921,29.021,7.08,38.564l-2.36,1.689l4.893,2.262l4.893,2.262l-0.568-5.36l-0.567-5.359l-2.365,1.694c-4.657-7.375-2.83-17.185,4.352-22.33c7.451-5.338,17.817-3.625,23.156,3.824c5.337,7.449,3.625,17.813-3.821,23.152l2.857,3.988c9.617-6.893,11.827-20.277,4.935-29.896C35.591,4.87,22.204,2.658,12.582,9.551z',
|
||||
* fill: 'green',
|
||||
* scaleX: 2,
|
||||
* scaleY: 2
|
||||
* });
|
||||
*/
|
||||
export class Path extends Shape {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this.dataArray = [];
|
||||
this.pathLength = 0;
|
||||
this._readDataAttribute();
|
||||
this.on('dataChange.konva', function () {
|
||||
this._readDataAttribute();
|
||||
});
|
||||
}
|
||||
_readDataAttribute() {
|
||||
this.dataArray = Path.parsePathData(this.data());
|
||||
this.pathLength = Path.getPathLength(this.dataArray);
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
const ca = this.dataArray;
|
||||
// context position
|
||||
context.beginPath();
|
||||
let isClosed = false;
|
||||
for (let n = 0; n < ca.length; n++) {
|
||||
const c = ca[n].command;
|
||||
const p = ca[n].points;
|
||||
switch (c) {
|
||||
case 'L':
|
||||
context.lineTo(p[0], p[1]);
|
||||
break;
|
||||
case 'M':
|
||||
context.moveTo(p[0], p[1]);
|
||||
break;
|
||||
case 'C':
|
||||
context.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]);
|
||||
break;
|
||||
case 'Q':
|
||||
context.quadraticCurveTo(p[0], p[1], p[2], p[3]);
|
||||
break;
|
||||
case 'A':
|
||||
context.ellipse(p[0], p[1], p[2], p[3], p[6], p[4], p[4] + p[5], !p[7]);
|
||||
break;
|
||||
case 'z':
|
||||
isClosed = true;
|
||||
context.closePath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isClosed && !this.hasFill()) {
|
||||
context.strokeShape(this);
|
||||
}
|
||||
else {
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
}
|
||||
getWidth() {
|
||||
return this.getSelfRect().width;
|
||||
}
|
||||
getHeight() {
|
||||
return this.getSelfRect().height;
|
||||
}
|
||||
getSelfRect() {
|
||||
const points = [];
|
||||
this.dataArray.forEach(function (data) {
|
||||
if (data.command === 'A') {
|
||||
// the two end points, plus the angles where the ellipse turns back on
|
||||
// either axis, when they fall inside the sweep. Together they are the
|
||||
// exact bounds of the segment
|
||||
const [cx, cy, rx, ry, start, dTheta, psi] = data.points;
|
||||
const cos = Math.cos(psi), sin = Math.sin(psi);
|
||||
const end = Path.getPointOnEllipticalArc(cx, cy, rx, ry, start + dTheta, psi);
|
||||
points.push(data.start.x, data.start.y, end.x, end.y);
|
||||
const tx = Math.atan2(-ry * sin, rx * cos);
|
||||
const ty = Math.atan2(ry * cos, rx * sin);
|
||||
[tx, tx + Math.PI, ty, ty + Math.PI].forEach((t) => {
|
||||
// how far into the sweep t is, in the direction of the sweep
|
||||
const k = ((((t - start) * Math.sign(dTheta)) % TAU) + TAU) % TAU;
|
||||
if (k < Math.abs(dTheta)) {
|
||||
const point = Path.getPointOnEllipticalArc(cx, cy, rx, ry, t, psi);
|
||||
points.push(point.x, point.y);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (data.command === 'C') {
|
||||
// the two end points, plus the points where the curve turns back on
|
||||
// either axis. Together they are the exact bounds of the segment
|
||||
points.push(data.start.x, data.start.y, data.points[4], data.points[5], ...getCubicExtremaPoints(data.start.x, data.start.y, data.points[0], data.points[1], data.points[2], data.points[3], data.points[4], data.points[5]));
|
||||
}
|
||||
else if (data.command === 'Q') {
|
||||
// same as 'C'. Note that 'q', 'T' and 't' are all normalised to 'Q' by
|
||||
// the parser, so this one branch covers every quadratic segment
|
||||
points.push(data.start.x, data.start.y, data.points[2], data.points[3], ...getQuadraticExtremaPoints(data.start.x, data.start.y, data.points[0], data.points[1], data.points[2], data.points[3]));
|
||||
}
|
||||
else {
|
||||
points.push(...data.points);
|
||||
}
|
||||
});
|
||||
return Util._getPointsRect(points);
|
||||
}
|
||||
/**
|
||||
* Return length of the path.
|
||||
* @method
|
||||
* @name Konva.Path#getLength
|
||||
* @returns {Number} length
|
||||
* @example
|
||||
* var length = path.getLength();
|
||||
*/
|
||||
getLength() {
|
||||
return this.pathLength;
|
||||
}
|
||||
/**
|
||||
* Get point on path at specific length of the path
|
||||
* @method
|
||||
* @name Konva.Path#getPointAtLength
|
||||
* @param {Number} length length
|
||||
* @returns {Object} point {x,y} point
|
||||
* @example
|
||||
* var point = path.getPointAtLength(10);
|
||||
*/
|
||||
getPointAtLength(length) {
|
||||
return Path.getPointAtLengthOfDataArray(length, this.dataArray);
|
||||
}
|
||||
static getLineLength(x1, y1, x2, y2) {
|
||||
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
|
||||
}
|
||||
static getPathLength(dataArray) {
|
||||
let pathLength = 0;
|
||||
for (let i = 0; i < dataArray.length; ++i) {
|
||||
pathLength += dataArray[i].pathLength;
|
||||
}
|
||||
return pathLength;
|
||||
}
|
||||
// The optional cursor is for sequential lookups within one continuous subpath.
|
||||
static getPointAtLengthOfDataArray(length, dataArray, cursor) {
|
||||
var _a, _b;
|
||||
let points, i = (_a = cursor === null || cursor === void 0 ? void 0 : cursor.index) !== null && _a !== void 0 ? _a : 0, offset = (_b = cursor === null || cursor === void 0 ? void 0 : cursor.offset) !== null && _b !== void 0 ? _b : 0, ii = dataArray.length;
|
||||
if (!ii) {
|
||||
return null;
|
||||
}
|
||||
// Negative spacing can move a text glyph back into an earlier segment.
|
||||
while (i > 0 && length <= offset) {
|
||||
offset -= dataArray[--i].pathLength;
|
||||
}
|
||||
length -= offset;
|
||||
while (i < ii && length > dataArray[i].pathLength) {
|
||||
const segmentLength = dataArray[i].pathLength;
|
||||
length -= segmentLength;
|
||||
offset += segmentLength;
|
||||
++i;
|
||||
}
|
||||
if (cursor && i < ii) {
|
||||
cursor.index = i;
|
||||
cursor.offset = offset;
|
||||
}
|
||||
if (i === ii) {
|
||||
// past the end: the end of the last segment
|
||||
i--;
|
||||
length = dataArray[i].pathLength;
|
||||
}
|
||||
if (length < 0.01) {
|
||||
const cmd = dataArray[i].command;
|
||||
if (cmd === 'M') {
|
||||
points = dataArray[i].points.slice(0, 2);
|
||||
return {
|
||||
x: points[0],
|
||||
y: points[1],
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
x: dataArray[i].start.x,
|
||||
y: dataArray[i].start.y,
|
||||
};
|
||||
}
|
||||
}
|
||||
const cp = dataArray[i];
|
||||
const p = cp.points;
|
||||
switch (cp.command) {
|
||||
case 'L':
|
||||
case 'z':
|
||||
return Path.getPointOnLine(length, cp.start.x, cp.start.y, p[0], p[1]);
|
||||
case 'C':
|
||||
return Path.getPointOnCubicBezier(t2length(length, cp.pathLength, (i) => {
|
||||
return getCubicArcLength([cp.start.x, p[0], p[2], p[4]], [cp.start.y, p[1], p[3], p[5]], i);
|
||||
}), cp.start.x, cp.start.y, p[0], p[1], p[2], p[3], p[4], p[5]);
|
||||
case 'Q':
|
||||
return Path.getPointOnQuadraticBezier(t2length(length, cp.pathLength, (i) => {
|
||||
return getQuadraticArcLength([cp.start.x, p[0], p[2]], [cp.start.y, p[1], p[3]], i);
|
||||
}), cp.start.x, cp.start.y, p[0], p[1], p[2], p[3]);
|
||||
case 'A':
|
||||
return Path.getPointOnEllipticalArc(p[0], p[1], p[2], p[3],
|
||||
// on a circle angle is proportional to distance, so the walk is only
|
||||
// needed for a real ellipse
|
||||
p[2] === p[3]
|
||||
? p[4] + (p[5] * length) / cp.pathLength
|
||||
: Path._walkArc(p, length).theta, p[6]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static getPointOnLine(dist, P1x, P1y, P2x, P2y, fromX, fromY) {
|
||||
fromX = fromX !== null && fromX !== void 0 ? fromX : P1x;
|
||||
fromY = fromY !== null && fromY !== void 0 ? fromY : P1y;
|
||||
const len = this.getLineLength(P1x, P1y, P2x, P2y);
|
||||
if (len < 1e-10) {
|
||||
return { x: P1x, y: P1y };
|
||||
}
|
||||
if (P2x === P1x) {
|
||||
// Vertical line
|
||||
return { x: fromX, y: fromY + (P2y > P1y ? dist : -dist) };
|
||||
}
|
||||
const m = (P2y - P1y) / (P2x - P1x);
|
||||
const run = Math.sqrt((dist * dist) / (1 + m * m)) * (P2x < P1x ? -1 : 1);
|
||||
const rise = m * run;
|
||||
if (Math.abs(fromY - P1y - m * (fromX - P1x)) < 1e-10) {
|
||||
return { x: fromX + run, y: fromY + rise };
|
||||
}
|
||||
const u = ((fromX - P1x) * (P2x - P1x) + (fromY - P1y) * (P2y - P1y)) / (len * len);
|
||||
const ix = P1x + u * (P2x - P1x);
|
||||
const iy = P1y + u * (P2y - P1y);
|
||||
const pRise = this.getLineLength(fromX, fromY, ix, iy);
|
||||
const pRun = Math.sqrt(dist * dist - pRise * pRise);
|
||||
const adjustedRun = Math.sqrt((pRun * pRun) / (1 + m * m)) * (P2x < P1x ? -1 : 1);
|
||||
const adjustedRise = m * adjustedRun;
|
||||
return { x: ix + adjustedRun, y: iy + adjustedRise };
|
||||
}
|
||||
static getPointOnCubicBezier(pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) {
|
||||
function CB1(t) {
|
||||
return t * t * t;
|
||||
}
|
||||
function CB2(t) {
|
||||
return 3 * t * t * (1 - t);
|
||||
}
|
||||
function CB3(t) {
|
||||
return 3 * t * (1 - t) * (1 - t);
|
||||
}
|
||||
function CB4(t) {
|
||||
return (1 - t) * (1 - t) * (1 - t);
|
||||
}
|
||||
const x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct);
|
||||
const y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct);
|
||||
return { x, y };
|
||||
}
|
||||
static getPointOnQuadraticBezier(pct, P1x, P1y, P2x, P2y, P3x, P3y) {
|
||||
function QB1(t) {
|
||||
return t * t;
|
||||
}
|
||||
function QB2(t) {
|
||||
return 2 * t * (1 - t);
|
||||
}
|
||||
function QB3(t) {
|
||||
return (1 - t) * (1 - t);
|
||||
}
|
||||
const x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct);
|
||||
const y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct);
|
||||
return { x, y };
|
||||
}
|
||||
static getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi) {
|
||||
const cosPsi = Math.cos(psi), sinPsi = Math.sin(psi);
|
||||
const pt = {
|
||||
x: rx * Math.cos(theta),
|
||||
y: ry * Math.sin(theta),
|
||||
};
|
||||
return {
|
||||
x: cx + (pt.x * cosPsi - pt.y * sinPsi),
|
||||
y: cy + (pt.x * sinPsi + pt.y * cosPsi),
|
||||
};
|
||||
}
|
||||
/*
|
||||
* get parsed data array from the data
|
||||
* string. V, v, H, h, and l data are converted to
|
||||
* L data for the purpose of high performance Path
|
||||
* rendering
|
||||
*/
|
||||
static parsePathData(data) {
|
||||
// Path Data Segment must begin with a moveTo
|
||||
//m (x y)+ Relative moveTo (subsequent points are treated as lineTo)
|
||||
//M (x y)+ Absolute moveTo (subsequent points are treated as lineTo)
|
||||
//l (x y)+ Relative lineTo
|
||||
//L (x y)+ Absolute LineTo
|
||||
//h (x)+ Relative horizontal lineTo
|
||||
//H (x)+ Absolute horizontal lineTo
|
||||
//v (y)+ Relative vertical lineTo
|
||||
//V (y)+ Absolute vertical lineTo
|
||||
//z (closepath)
|
||||
//Z (closepath)
|
||||
//c (x1 y1 x2 y2 x y)+ Relative Bezier curve
|
||||
//C (x1 y1 x2 y2 x y)+ Absolute Bezier curve
|
||||
//q (x1 y1 x y)+ Relative Quadratic Bezier
|
||||
//Q (x1 y1 x y)+ Absolute Quadratic Bezier
|
||||
//t (x y)+ Shorthand/Smooth Relative Quadratic Bezier
|
||||
//T (x y)+ Shorthand/Smooth Absolute Quadratic Bezier
|
||||
//s (x2 y2 x y)+ Shorthand/Smooth Relative Bezier curve
|
||||
//S (x2 y2 x y)+ Shorthand/Smooth Absolute Bezier curve
|
||||
//a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Relative Elliptical Arc
|
||||
//A (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Absolute Elliptical Arc
|
||||
// return early if data is not defined
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
// command string
|
||||
let cs = data;
|
||||
// command chars
|
||||
const cc = [
|
||||
'm',
|
||||
'M',
|
||||
'l',
|
||||
'L',
|
||||
'v',
|
||||
'V',
|
||||
'h',
|
||||
'H',
|
||||
'z',
|
||||
'Z',
|
||||
'c',
|
||||
'C',
|
||||
'q',
|
||||
'Q',
|
||||
't',
|
||||
'T',
|
||||
's',
|
||||
'S',
|
||||
'a',
|
||||
'A',
|
||||
];
|
||||
// convert white spaces to commas
|
||||
cs = cs.replace(new RegExp(' ', 'g'), ',');
|
||||
// create pipes so that we can split the data
|
||||
for (let n = 0; n < cc.length; n++) {
|
||||
cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
|
||||
}
|
||||
// create array
|
||||
const arr = cs.split('|');
|
||||
const ca = [];
|
||||
const coords = [];
|
||||
// init context point
|
||||
let cpx = 0;
|
||||
let cpy = 0;
|
||||
// start of the current subpath: where z draws back to
|
||||
let spx = 0;
|
||||
let spy = 0;
|
||||
const re = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/gi;
|
||||
let match;
|
||||
for (let n = 1; n < arr.length; n++) {
|
||||
let str = arr[n];
|
||||
let c = str.charAt(0);
|
||||
str = str.slice(1);
|
||||
coords.length = 0;
|
||||
while ((match = re.exec(str))) {
|
||||
coords.push(match[0]);
|
||||
}
|
||||
const p = [];
|
||||
// Track param position for A/a commands: 0..6 => rx, ry, psi, fa, fs, x, y
|
||||
let arcParamIndex = c === 'A' || c === 'a' ? 0 : -1;
|
||||
for (let j = 0, jlen = coords.length; j < jlen; j++) {
|
||||
let token = coords[j];
|
||||
// SVGO merges the arc flags with the number that follows them:
|
||||
// "01.5.5" is fa=0 fs=1 x=.5 y=.5
|
||||
while ((arcParamIndex === 3 || arcParamIndex === 4) &&
|
||||
token.length > 1 &&
|
||||
(token[0] === '0' || token[0] === '1')) {
|
||||
p.push(+token[0]);
|
||||
arcParamIndex++;
|
||||
token = token.slice(1);
|
||||
}
|
||||
const parsed = parseFloat(token);
|
||||
p.push(isNaN(parsed) ? 0 : parsed);
|
||||
if (arcParamIndex >= 0) {
|
||||
arcParamIndex = (arcParamIndex + 1) % 7;
|
||||
}
|
||||
}
|
||||
let pIndex = 0;
|
||||
while (pIndex < p.length) {
|
||||
// z takes no numbers, and a command with too few of them ("L20"
|
||||
// with no y) is dropped rather than parsed into a NaN segment
|
||||
if (p.length - pIndex < PARAM_COUNT[c.toLowerCase()] ||
|
||||
c === 'z' ||
|
||||
c === 'Z') {
|
||||
break;
|
||||
}
|
||||
let cmd = '';
|
||||
let points = [];
|
||||
const startX = cpx, startY = cpy;
|
||||
// Move var from within the switch to up here (jshint)
|
||||
let prevCmd, ctlPtx, ctlPty; // Ss, Tt
|
||||
let rx, ry, psi, fa, fs, x1, y1; // Aa
|
||||
// convert l, H, h, V, and v to L
|
||||
switch (c) {
|
||||
// Note: Keep the lineTo's above the moveTo's in this switch
|
||||
case 'l':
|
||||
cpx += p[pIndex++];
|
||||
cpy += p[pIndex++];
|
||||
cmd = 'L';
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'L':
|
||||
cpx = p[pIndex++];
|
||||
cpy = p[pIndex++];
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
// Note: lineTo handlers need to be above this point
|
||||
case 'm':
|
||||
cpx += p[pIndex++];
|
||||
cpy += p[pIndex++];
|
||||
cmd = 'M';
|
||||
spx = cpx;
|
||||
spy = cpy;
|
||||
points.push(cpx, cpy);
|
||||
c = 'l';
|
||||
// subsequent points are treated as relative lineTo
|
||||
break;
|
||||
case 'M':
|
||||
cpx = p[pIndex++];
|
||||
cpy = p[pIndex++];
|
||||
cmd = 'M';
|
||||
spx = cpx;
|
||||
spy = cpy;
|
||||
points.push(cpx, cpy);
|
||||
c = 'L';
|
||||
// subsequent points are treated as absolute lineTo
|
||||
break;
|
||||
case 'h':
|
||||
cpx += p[pIndex++];
|
||||
cmd = 'L';
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'H':
|
||||
cpx = p[pIndex++];
|
||||
cmd = 'L';
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'v':
|
||||
cpy += p[pIndex++];
|
||||
cmd = 'L';
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'V':
|
||||
cpy = p[pIndex++];
|
||||
cmd = 'L';
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'C':
|
||||
points.push(p[pIndex++], p[pIndex++], p[pIndex++], p[pIndex++]);
|
||||
cpx = p[pIndex++];
|
||||
cpy = p[pIndex++];
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'c':
|
||||
points.push(cpx + p[pIndex++], cpy + p[pIndex++], cpx + p[pIndex++], cpy + p[pIndex++]);
|
||||
cpx += p[pIndex++];
|
||||
cpy += p[pIndex++];
|
||||
cmd = 'C';
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'S':
|
||||
ctlPtx = cpx;
|
||||
ctlPty = cpy;
|
||||
prevCmd = ca[ca.length - 1];
|
||||
if ((prevCmd === null || prevCmd === void 0 ? void 0 : prevCmd.command) === 'C') {
|
||||
ctlPtx = cpx + (cpx - prevCmd.points[2]);
|
||||
ctlPty = cpy + (cpy - prevCmd.points[3]);
|
||||
}
|
||||
points.push(ctlPtx, ctlPty, p[pIndex++], p[pIndex++]);
|
||||
cpx = p[pIndex++];
|
||||
cpy = p[pIndex++];
|
||||
cmd = 'C';
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 's':
|
||||
ctlPtx = cpx;
|
||||
ctlPty = cpy;
|
||||
prevCmd = ca[ca.length - 1];
|
||||
if ((prevCmd === null || prevCmd === void 0 ? void 0 : prevCmd.command) === 'C') {
|
||||
ctlPtx = cpx + (cpx - prevCmd.points[2]);
|
||||
ctlPty = cpy + (cpy - prevCmd.points[3]);
|
||||
}
|
||||
points.push(ctlPtx, ctlPty, cpx + p[pIndex++], cpy + p[pIndex++]);
|
||||
cpx += p[pIndex++];
|
||||
cpy += p[pIndex++];
|
||||
cmd = 'C';
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'Q':
|
||||
points.push(p[pIndex++], p[pIndex++]);
|
||||
cpx = p[pIndex++];
|
||||
cpy = p[pIndex++];
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'q':
|
||||
points.push(cpx + p[pIndex++], cpy + p[pIndex++]);
|
||||
cpx += p[pIndex++];
|
||||
cpy += p[pIndex++];
|
||||
cmd = 'Q';
|
||||
points.push(cpx, cpy);
|
||||
break;
|
||||
case 'T':
|
||||
ctlPtx = cpx;
|
||||
ctlPty = cpy;
|
||||
prevCmd = ca[ca.length - 1];
|
||||
if ((prevCmd === null || prevCmd === void 0 ? void 0 : prevCmd.command) === 'Q') {
|
||||
ctlPtx = cpx + (cpx - prevCmd.points[0]);
|
||||
ctlPty = cpy + (cpy - prevCmd.points[1]);
|
||||
}
|
||||
cpx = p[pIndex++];
|
||||
cpy = p[pIndex++];
|
||||
cmd = 'Q';
|
||||
points.push(ctlPtx, ctlPty, cpx, cpy);
|
||||
break;
|
||||
case 't':
|
||||
ctlPtx = cpx;
|
||||
ctlPty = cpy;
|
||||
prevCmd = ca[ca.length - 1];
|
||||
if ((prevCmd === null || prevCmd === void 0 ? void 0 : prevCmd.command) === 'Q') {
|
||||
ctlPtx = cpx + (cpx - prevCmd.points[0]);
|
||||
ctlPty = cpy + (cpy - prevCmd.points[1]);
|
||||
}
|
||||
cpx += p[pIndex++];
|
||||
cpy += p[pIndex++];
|
||||
cmd = 'Q';
|
||||
points.push(ctlPtx, ctlPty, cpx, cpy);
|
||||
break;
|
||||
case 'A':
|
||||
case 'a':
|
||||
// per SVG, the radii are used as absolute values
|
||||
rx = Math.abs(p[pIndex++]);
|
||||
ry = Math.abs(p[pIndex++]);
|
||||
psi = p[pIndex++];
|
||||
fa = p[pIndex++];
|
||||
fs = p[pIndex++];
|
||||
x1 = cpx;
|
||||
y1 = cpy;
|
||||
if (c === 'a') {
|
||||
cpx += p[pIndex++];
|
||||
cpy += p[pIndex++];
|
||||
}
|
||||
else {
|
||||
cpx = p[pIndex++];
|
||||
cpy = p[pIndex++];
|
||||
}
|
||||
cmd = 'A';
|
||||
// per SVG, an arc between coincident end points is omitted
|
||||
// and a zero radius makes it a straight line
|
||||
if (cpx === x1 && cpy === y1) {
|
||||
continue;
|
||||
}
|
||||
else if (!rx || !ry) {
|
||||
cmd = 'L';
|
||||
points.push(cpx, cpy);
|
||||
}
|
||||
else {
|
||||
points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi);
|
||||
}
|
||||
break;
|
||||
}
|
||||
ca.push({
|
||||
command: cmd || c,
|
||||
points: points,
|
||||
start: {
|
||||
x: startX,
|
||||
y: startY,
|
||||
},
|
||||
pathLength: this.calcLength(startX, startY, cmd || c, points),
|
||||
});
|
||||
}
|
||||
if (c === 'z' || c === 'Z') {
|
||||
// per SVG, z is a line back to the start of the subpath, which then
|
||||
// becomes the current point
|
||||
ca.push({
|
||||
command: 'z',
|
||||
points: [spx, spy],
|
||||
start: { x: cpx, y: cpy },
|
||||
pathLength: this.getLineLength(cpx, cpy, spx, spy),
|
||||
});
|
||||
cpx = spx;
|
||||
cpy = spy;
|
||||
}
|
||||
}
|
||||
return ca;
|
||||
}
|
||||
/**
|
||||
* Walks an arc in one degree steps, accumulating its length. Returns the
|
||||
* angle `length` along the arc, or its end angle and total length when
|
||||
* `length` is past the end.
|
||||
*/
|
||||
static _walkArc(points, length) {
|
||||
const [cx, cy, rx, ry, start, dTheta] = points;
|
||||
const steps = Math.max(1, Math.ceil(Math.abs(dTheta) / (Math.PI / 180)));
|
||||
// the arc length does not depend on the x-axis rotation psi, so it is left out
|
||||
let p1 = Path.getPointOnEllipticalArc(cx, cy, rx, ry, start, 0);
|
||||
let prev = start;
|
||||
let len = 0;
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const t = start + (dTheta * i) / steps;
|
||||
const p2 = Path.getPointOnEllipticalArc(cx, cy, rx, ry, t, 0);
|
||||
const d = Path.getLineLength(p1.x, p1.y, p2.x, p2.y);
|
||||
if (len + d >= length) {
|
||||
return { theta: prev + (t - prev) * ((length - len) / d), length };
|
||||
}
|
||||
len += d;
|
||||
p1 = p2;
|
||||
prev = t;
|
||||
}
|
||||
return { theta: prev, length: len };
|
||||
}
|
||||
static calcLength(x, y, cmd, points) {
|
||||
const path = Path;
|
||||
switch (cmd) {
|
||||
case 'L':
|
||||
return path.getLineLength(x, y, points[0], points[1]);
|
||||
case 'C':
|
||||
return getCubicArcLength([x, points[0], points[2], points[4]], [y, points[1], points[3], points[5]], 1);
|
||||
case 'Q':
|
||||
return getQuadraticArcLength([x, points[0], points[2]], [y, points[1], points[3]], 1);
|
||||
case 'A':
|
||||
return path._walkArc(points, Infinity).length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static convertEndpointToCenterParameterization(x1, y1, x2, y2, fa, fs, rx, ry, psiDeg) {
|
||||
// Derived from: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
const psi = psiDeg * (Math.PI / 180.0);
|
||||
const xp = (Math.cos(psi) * (x1 - x2)) / 2.0 + (Math.sin(psi) * (y1 - y2)) / 2.0;
|
||||
const yp = (-1 * Math.sin(psi) * (x1 - x2)) / 2.0 +
|
||||
(Math.cos(psi) * (y1 - y2)) / 2.0;
|
||||
const lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry);
|
||||
if (lambda > 1) {
|
||||
rx *= Math.sqrt(lambda);
|
||||
ry *= Math.sqrt(lambda);
|
||||
}
|
||||
let f = Math.sqrt((rx * rx * (ry * ry) - rx * rx * (yp * yp) - ry * ry * (xp * xp)) /
|
||||
(rx * rx * (yp * yp) + ry * ry * (xp * xp)));
|
||||
if (fa === fs) {
|
||||
f *= -1;
|
||||
}
|
||||
if (isNaN(f)) {
|
||||
f = 0;
|
||||
}
|
||||
const cxp = (f * rx * yp) / ry;
|
||||
const cyp = (f * -ry * xp) / rx;
|
||||
const cx = (x1 + x2) / 2.0 + Math.cos(psi) * cxp - Math.sin(psi) * cyp;
|
||||
const cy = (y1 + y2) / 2.0 + Math.sin(psi) * cxp + Math.cos(psi) * cyp;
|
||||
const vMag = function (v) {
|
||||
return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
||||
};
|
||||
const vRatio = function (u, v) {
|
||||
return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v));
|
||||
};
|
||||
const vAngle = function (u, v) {
|
||||
return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v));
|
||||
};
|
||||
const theta = vAngle([1, 0], [(xp - cxp) / rx, (yp - cyp) / ry]);
|
||||
const u = [(xp - cxp) / rx, (yp - cyp) / ry];
|
||||
const v = [(-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry];
|
||||
let dTheta = vAngle(u, v);
|
||||
if (vRatio(u, v) <= -1) {
|
||||
dTheta = Math.PI;
|
||||
}
|
||||
if (vRatio(u, v) >= 1) {
|
||||
dTheta = 0;
|
||||
}
|
||||
if (fs === 0 && dTheta > 0) {
|
||||
dTheta = dTheta - 2 * Math.PI;
|
||||
}
|
||||
if (fs === 1 && dTheta < 0) {
|
||||
dTheta = dTheta + 2 * Math.PI;
|
||||
}
|
||||
return [cx, cy, rx, ry, theta, dTheta, psi, fs];
|
||||
}
|
||||
}
|
||||
Path.prototype.className = 'Path';
|
||||
Path.prototype._attrsAffectingSize = ['data'];
|
||||
_registerNode(Path, true);
|
||||
/**
|
||||
* get/set SVG path data string. This method
|
||||
* also automatically parses the data string
|
||||
* into a data array. Currently supported SVG data:
|
||||
* M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, A, a, Z, z
|
||||
* @name Konva.Path#data
|
||||
* @method
|
||||
* @param {String} data svg path string
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get data
|
||||
* var data = path.data();
|
||||
*
|
||||
* // set data
|
||||
* path.data('M200,100h100v50z');
|
||||
*/
|
||||
Factory.addGetterSetter(Path, 'data');
|
||||
/**
|
||||
* get width of the path. It is computed from the path data and cannot be set
|
||||
* @name Konva.Path#width
|
||||
* @method
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* var width = path.width();
|
||||
*/
|
||||
/**
|
||||
* get height of the path. It is computed from the path data and cannot be set
|
||||
* @name Konva.Path#height
|
||||
* @method
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* var height = path.height();
|
||||
*/
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
export type RectConfig = ShapeConfig & {
|
||||
cornerRadius?: number | number[];
|
||||
};
|
||||
/**
|
||||
* Rect constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} [config.cornerRadius]
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var rect = new Konva.Rect({
|
||||
* width: 100,
|
||||
* height: 50,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5
|
||||
* });
|
||||
*/
|
||||
export declare class Rect extends Shape<RectConfig> {
|
||||
_getStrokePadding(): number;
|
||||
_sceneFunc(context: Context): void;
|
||||
cornerRadius: GetSet<number | number[], this>;
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
import { Util } from "../Util.js";
|
||||
import { getNumberOrArrayOfNumbersValidator } from "../Validators.js";
|
||||
/**
|
||||
* Rect constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} [config.cornerRadius]
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var rect = new Konva.Rect({
|
||||
* width: 100,
|
||||
* height: 50,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5
|
||||
* });
|
||||
*/
|
||||
export class Rect extends Shape {
|
||||
_getStrokePadding() {
|
||||
return super._getStrokePadding(this.strokeScaleEnabled() ? 1 : undefined);
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
const cornerRadius = this.cornerRadius(), width = this.width(), height = this.height();
|
||||
context.beginPath();
|
||||
if (!cornerRadius) {
|
||||
// simple rect - don't bother doing all that complicated maths stuff.
|
||||
context.rect(0, 0, width, height);
|
||||
}
|
||||
else {
|
||||
Util.drawRoundedRectPath(context, width, height, cornerRadius);
|
||||
}
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
}
|
||||
Rect.prototype.className = 'Rect';
|
||||
_registerNode(Rect, true);
|
||||
/**
|
||||
* get/set corner radius
|
||||
* @method
|
||||
* @name Konva.Rect#cornerRadius
|
||||
* @param {Number} cornerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get corner radius
|
||||
* var cornerRadius = rect.cornerRadius();
|
||||
*
|
||||
* // set corner radius
|
||||
* rect.cornerRadius(10);
|
||||
*
|
||||
* // set different corner radius values
|
||||
* // top-left, top-right, bottom-right, bottom-left
|
||||
* rect.cornerRadius([0, 10, 20, 30]);
|
||||
*/
|
||||
Factory.addGetterSetter(Rect, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4));
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet, Vector2d } from '../types.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
export interface RegularPolygonConfig extends ShapeConfig {
|
||||
sides: number;
|
||||
radius: number;
|
||||
cornerRadius?: number | number[];
|
||||
}
|
||||
/**
|
||||
* RegularPolygon constructor. Examples include triangles, squares, pentagons, hexagons, etc.
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} [config.cornerRadius]
|
||||
* @param {Number} config.sides
|
||||
* @param {Number} config.radius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var hexagon = new Konva.RegularPolygon({
|
||||
* x: 100,
|
||||
* y: 200,
|
||||
* sides: 6,
|
||||
* radius: 70,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 4
|
||||
* });
|
||||
*/
|
||||
export declare class RegularPolygon extends Shape<RegularPolygonConfig> {
|
||||
_sceneFunc(context: Context): void;
|
||||
_getPoints(): Vector2d[];
|
||||
getSelfRect(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
setWidth(width: number): void;
|
||||
setHeight(height: number): void;
|
||||
radius: GetSet<number, this>;
|
||||
sides: GetSet<number, this>;
|
||||
cornerRadius: GetSet<number | number[], this>;
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { getNumberOrArrayOfNumbersValidator, getNumberValidator, } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
import { Util } from "../Util.js";
|
||||
/**
|
||||
* RegularPolygon constructor. Examples include triangles, squares, pentagons, hexagons, etc.
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} [config.cornerRadius]
|
||||
* @param {Number} config.sides
|
||||
* @param {Number} config.radius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var hexagon = new Konva.RegularPolygon({
|
||||
* x: 100,
|
||||
* y: 200,
|
||||
* sides: 6,
|
||||
* radius: 70,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 4
|
||||
* });
|
||||
*/
|
||||
export class RegularPolygon extends Shape {
|
||||
_sceneFunc(context) {
|
||||
const points = this._getPoints(), radius = this.radius(), sides = this.sides(), cornerRadius = this.cornerRadius();
|
||||
if (!points.length) {
|
||||
return;
|
||||
}
|
||||
context.beginPath();
|
||||
if (!cornerRadius) {
|
||||
context.moveTo(points[0].x, points[0].y);
|
||||
for (let n = 1; n < points.length; n++) {
|
||||
context.lineTo(points[n].x, points[n].y);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Util.drawRoundedPolygonPath(context, points, sides, radius, cornerRadius);
|
||||
}
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
_getPoints() {
|
||||
const sides = this.attrs.sides;
|
||||
const radius = this.attrs.radius || 0;
|
||||
const points = [];
|
||||
for (let n = 0; n < sides; n++) {
|
||||
points.push({
|
||||
x: radius * Math.sin((n * 2 * Math.PI) / sides),
|
||||
y: -1 * radius * Math.cos((n * 2 * Math.PI) / sides),
|
||||
});
|
||||
}
|
||||
return points;
|
||||
}
|
||||
getSelfRect() {
|
||||
const points = this._getPoints();
|
||||
if (!points.length) {
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
}
|
||||
let minX = points[0].x;
|
||||
let maxX = points[0].x;
|
||||
let minY = points[0].y;
|
||||
let maxY = points[0].y;
|
||||
points.forEach((point) => {
|
||||
minX = Math.min(minX, point.x);
|
||||
maxX = Math.max(maxX, point.x);
|
||||
minY = Math.min(minY, point.y);
|
||||
maxY = Math.max(maxY, point.y);
|
||||
});
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
}
|
||||
getWidth() {
|
||||
return this.radius() * 2;
|
||||
}
|
||||
getHeight() {
|
||||
return this.radius() * 2;
|
||||
}
|
||||
setWidth(width) {
|
||||
this.radius(width / 2);
|
||||
}
|
||||
setHeight(height) {
|
||||
this.radius(height / 2);
|
||||
}
|
||||
}
|
||||
RegularPolygon.prototype.className = 'RegularPolygon';
|
||||
RegularPolygon.prototype._centroid = true;
|
||||
RegularPolygon.prototype._attrsAffectingSize = ['radius', 'sides'];
|
||||
_registerNode(RegularPolygon, true);
|
||||
/**
|
||||
* get/set radius
|
||||
* @method
|
||||
* @name Konva.RegularPolygon#radius
|
||||
* @param {Number} radius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get radius
|
||||
* var radius = shape.radius();
|
||||
*
|
||||
* // set radius
|
||||
* shape.radius(10);
|
||||
*/
|
||||
Factory.addGetterSetter(RegularPolygon, 'radius', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set sides
|
||||
* @method
|
||||
* @name Konva.RegularPolygon#sides
|
||||
* @param {Number} sides
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get sides
|
||||
* var sides = shape.sides();
|
||||
*
|
||||
* // set sides
|
||||
* shape.sides(10);
|
||||
*/
|
||||
Factory.addGetterSetter(RegularPolygon, 'sides', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set corner radius
|
||||
* @method
|
||||
* @name Konva.RegularPolygon#cornerRadius
|
||||
* @param {Number} cornerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get corner radius
|
||||
* var cornerRadius = poly.cornerRadius();
|
||||
*
|
||||
* // set corner radius
|
||||
* poly.cornerRadius(10);
|
||||
*
|
||||
* // set different corner radius values (pentagon)
|
||||
* poly.cornerRadius([0, 10, 20, 30, 40]);
|
||||
*/
|
||||
Factory.addGetterSetter(RegularPolygon, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4));
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
import type { Context } from '../Context.ts';
|
||||
export interface RingConfig extends ShapeConfig {
|
||||
innerRadius: number;
|
||||
outerRadius: number;
|
||||
}
|
||||
/**
|
||||
* Ring constructor
|
||||
* @constructor
|
||||
* @augments Konva.Shape
|
||||
* @memberof Konva
|
||||
* @param {Object} config
|
||||
* @param {Number} config.innerRadius
|
||||
* @param {Number} config.outerRadius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var ring = new Konva.Ring({
|
||||
* innerRadius: 40,
|
||||
* outerRadius: 80,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5
|
||||
* });
|
||||
*/
|
||||
export declare class Ring extends Shape<RingConfig> {
|
||||
_getStrokePadding(): number;
|
||||
_sceneFunc(context: Context): void;
|
||||
getSelfRect(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
setWidth(width: number): void;
|
||||
setHeight(height: number): void;
|
||||
outerRadius: GetSet<number, this>;
|
||||
innerRadius: GetSet<number, this>;
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { getNumberValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
const PIx2 = Math.PI * 2;
|
||||
/**
|
||||
* Ring constructor
|
||||
* @constructor
|
||||
* @augments Konva.Shape
|
||||
* @memberof Konva
|
||||
* @param {Object} config
|
||||
* @param {Number} config.innerRadius
|
||||
* @param {Number} config.outerRadius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var ring = new Konva.Ring({
|
||||
* innerRadius: 40,
|
||||
* outerRadius: 80,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5
|
||||
* });
|
||||
*/
|
||||
export class Ring extends Shape {
|
||||
_getStrokePadding() {
|
||||
return super._getStrokePadding(1);
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
const innerRadius = Math.abs(this.innerRadius()), outerRadius = Math.abs(this.outerRadius());
|
||||
context.beginPath();
|
||||
context.arc(0, 0, innerRadius, 0, PIx2, false);
|
||||
context.moveTo(outerRadius, 0);
|
||||
context.arc(0, 0, outerRadius, PIx2, 0, true);
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
getSelfRect() {
|
||||
const radius = Math.max(Math.abs(this.innerRadius()), Math.abs(this.outerRadius()));
|
||||
return { x: -radius, y: -radius, width: radius * 2, height: radius * 2 };
|
||||
}
|
||||
getWidth() {
|
||||
return Math.abs(this.outerRadius()) * 2;
|
||||
}
|
||||
getHeight() {
|
||||
return Math.abs(this.outerRadius()) * 2;
|
||||
}
|
||||
setWidth(width) {
|
||||
this.outerRadius(width / 2);
|
||||
}
|
||||
setHeight(height) {
|
||||
this.outerRadius(height / 2);
|
||||
}
|
||||
}
|
||||
Ring.prototype.className = 'Ring';
|
||||
Ring.prototype._centroid = true;
|
||||
Ring.prototype._attrsAffectingSize = ['innerRadius', 'outerRadius'];
|
||||
_registerNode(Ring, true);
|
||||
/**
|
||||
* get/set innerRadius
|
||||
* @method
|
||||
* @name Konva.Ring#innerRadius
|
||||
* @param {Number} innerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get inner radius
|
||||
* var innerRadius = ring.innerRadius();
|
||||
*
|
||||
* // set inner radius
|
||||
* ring.innerRadius(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Ring, 'innerRadius', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set outerRadius
|
||||
* @name Konva.Ring#outerRadius
|
||||
* @method
|
||||
* @param {Number} outerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get outer radius
|
||||
* var outerRadius = ring.outerRadius();
|
||||
*
|
||||
* // set outer radius
|
||||
* ring.outerRadius(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Ring, 'outerRadius', 0, getNumberValidator());
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import type { Context } from '../Context.ts';
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import { Animation } from '../Animation.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
export interface SpriteConfig extends ShapeConfig {
|
||||
animation: string;
|
||||
animations: any;
|
||||
frameIndex?: number;
|
||||
image: HTMLImageElement;
|
||||
frameRate?: number;
|
||||
}
|
||||
/**
|
||||
* Sprite constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {String} config.animation animation key
|
||||
* @param {Object} config.animations animation map
|
||||
* @param {Integer} [config.frameIndex] animation frame index
|
||||
* @param {Image} config.image image object
|
||||
* @param {Integer} [config.frameRate] animation frame rate
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var imageObj = new Image();
|
||||
* imageObj.onload = function() {
|
||||
* var sprite = new Konva.Sprite({
|
||||
* x: 200,
|
||||
* y: 100,
|
||||
* image: imageObj,
|
||||
* animation: 'standing',
|
||||
* animations: {
|
||||
* standing: [
|
||||
* // x, y, width, height (6 frames)
|
||||
* 0, 0, 49, 109,
|
||||
* 52, 0, 49, 109,
|
||||
* 105, 0, 49, 109,
|
||||
* 158, 0, 49, 109,
|
||||
* 210, 0, 49, 109,
|
||||
* 262, 0, 49, 109
|
||||
* ],
|
||||
* kicking: [
|
||||
* // x, y, width, height (6 frames)
|
||||
* 0, 109, 45, 98,
|
||||
* 45, 109, 45, 98,
|
||||
* 95, 109, 63, 98,
|
||||
* 156, 109, 70, 98,
|
||||
* 229, 109, 60, 98,
|
||||
* 287, 109, 41, 98
|
||||
* ]
|
||||
* },
|
||||
* frameRate: 7,
|
||||
* frameIndex: 0
|
||||
* });
|
||||
* };
|
||||
* imageObj.src = '/path/to/image.jpg'
|
||||
*/
|
||||
export declare class Sprite extends Shape<SpriteConfig> {
|
||||
_updated: boolean;
|
||||
anim: Animation;
|
||||
interval: any;
|
||||
constructor(config: SpriteConfig);
|
||||
_getFrame(): {
|
||||
x: any;
|
||||
y: any;
|
||||
width: any;
|
||||
height: any;
|
||||
offsetX: any;
|
||||
offsetY: any;
|
||||
} | undefined;
|
||||
_getStrokePadding(): number;
|
||||
_sceneFunc(context: Context): void;
|
||||
_hitFunc(context: Context): void;
|
||||
getSelfRect(): {
|
||||
x: any;
|
||||
y: any;
|
||||
width: any;
|
||||
height: any;
|
||||
};
|
||||
_useBufferCanvas(forceFill?: boolean, opacity?: any): boolean;
|
||||
_setInterval(): void;
|
||||
/**
|
||||
* start sprite animation
|
||||
* @method
|
||||
* @name Konva.Sprite#start
|
||||
*/
|
||||
start(): void;
|
||||
/**
|
||||
* stop sprite animation
|
||||
* @method
|
||||
* @name Konva.Sprite#stop
|
||||
*/
|
||||
stop(): void;
|
||||
destroy(): this;
|
||||
/**
|
||||
* determine if animation of sprite is running or not. returns true or false
|
||||
* @method
|
||||
* @name Konva.Sprite#isRunning
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isRunning(): boolean;
|
||||
_updateIndex(): void;
|
||||
frameIndex: GetSet<number, this>;
|
||||
animation: GetSet<string, this>;
|
||||
image: GetSet<CanvasImageSource, this>;
|
||||
animations: GetSet<any, this>;
|
||||
frameOffsets: GetSet<any, this>;
|
||||
frameRate: GetSet<number, this>;
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { Animation } from "../Animation.js";
|
||||
import { getNumberValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
/**
|
||||
* Sprite constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {String} config.animation animation key
|
||||
* @param {Object} config.animations animation map
|
||||
* @param {Integer} [config.frameIndex] animation frame index
|
||||
* @param {Image} config.image image object
|
||||
* @param {Integer} [config.frameRate] animation frame rate
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var imageObj = new Image();
|
||||
* imageObj.onload = function() {
|
||||
* var sprite = new Konva.Sprite({
|
||||
* x: 200,
|
||||
* y: 100,
|
||||
* image: imageObj,
|
||||
* animation: 'standing',
|
||||
* animations: {
|
||||
* standing: [
|
||||
* // x, y, width, height (6 frames)
|
||||
* 0, 0, 49, 109,
|
||||
* 52, 0, 49, 109,
|
||||
* 105, 0, 49, 109,
|
||||
* 158, 0, 49, 109,
|
||||
* 210, 0, 49, 109,
|
||||
* 262, 0, 49, 109
|
||||
* ],
|
||||
* kicking: [
|
||||
* // x, y, width, height (6 frames)
|
||||
* 0, 109, 45, 98,
|
||||
* 45, 109, 45, 98,
|
||||
* 95, 109, 63, 98,
|
||||
* 156, 109, 70, 98,
|
||||
* 229, 109, 60, 98,
|
||||
* 287, 109, 41, 98
|
||||
* ]
|
||||
* },
|
||||
* frameRate: 7,
|
||||
* frameIndex: 0
|
||||
* });
|
||||
* };
|
||||
* imageObj.src = '/path/to/image.jpg'
|
||||
*/
|
||||
export class Sprite extends Shape {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this._updated = true;
|
||||
this.anim = new Animation(() => {
|
||||
// if we don't need to redraw layer we should return false
|
||||
const updated = this._updated;
|
||||
this._updated = false;
|
||||
return updated;
|
||||
});
|
||||
this.on('animationChange.konva', function () {
|
||||
// reset index when animation changes
|
||||
this.frameIndex(0);
|
||||
});
|
||||
this.on('frameIndexChange.konva', function () {
|
||||
this._updated = true;
|
||||
});
|
||||
// smooth change for frameRate
|
||||
this.on('frameRateChange.konva', function () {
|
||||
if (!this.anim.isRunning()) {
|
||||
return;
|
||||
}
|
||||
clearInterval(this.interval);
|
||||
this._setInterval();
|
||||
});
|
||||
}
|
||||
// the current frame: its box in the sprite sheet and where it is drawn,
|
||||
// or nothing when the animation key or the frame index does not exist
|
||||
// (then nothing is drawn, like an image without an image)
|
||||
_getFrame() {
|
||||
var _a, _b;
|
||||
const anim = this.animation(), set = (_a = this.animations()) === null || _a === void 0 ? void 0 : _a[anim], index = this.frameIndex(), ix4 = index * 4;
|
||||
if (!set || !Number.isInteger(index) || ix4 < 0 || ix4 + 4 > set.length) {
|
||||
return;
|
||||
}
|
||||
const offset = ((_b = this.frameOffsets()) === null || _b === void 0 ? void 0 : _b[anim]) || [];
|
||||
return {
|
||||
x: set[ix4],
|
||||
y: set[ix4 + 1],
|
||||
width: set[ix4 + 2],
|
||||
height: set[ix4 + 3],
|
||||
offsetX: offset[index * 2] || 0,
|
||||
offsetY: offset[index * 2 + 1] || 0,
|
||||
};
|
||||
}
|
||||
_getStrokePadding() {
|
||||
return super._getStrokePadding(this.strokeScaleEnabled() ? 1 : undefined);
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
const frame = this._getFrame();
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
const { x, y, width, height, offsetX, offsetY } = frame;
|
||||
const image = this.image();
|
||||
if (this.hasFill() || this.hasStroke()) {
|
||||
context.beginPath();
|
||||
context.rect(offsetX, offsetY, width, height);
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
if (image) {
|
||||
context.drawImage(image, x, y, width, height, offsetX, offsetY, width, height);
|
||||
}
|
||||
}
|
||||
_hitFunc(context) {
|
||||
const { x, y, width, height } = this.getSelfRect();
|
||||
context.beginPath();
|
||||
context.rect(x, y, width, height);
|
||||
context.closePath();
|
||||
context.fillShape(this);
|
||||
}
|
||||
getSelfRect() {
|
||||
const frame = this._getFrame();
|
||||
return frame
|
||||
? {
|
||||
x: frame.offsetX,
|
||||
y: frame.offsetY,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
}
|
||||
: { x: 0, y: 0, width: 0, height: 0 };
|
||||
}
|
||||
_useBufferCanvas(forceFill, opacity = this.getAbsoluteOpacity()) {
|
||||
return super._useBufferCanvas(true, opacity);
|
||||
}
|
||||
_setInterval() {
|
||||
const that = this;
|
||||
this.interval = setInterval(function () {
|
||||
that._updateIndex();
|
||||
}, 1000 / this.frameRate());
|
||||
}
|
||||
/**
|
||||
* start sprite animation
|
||||
* @method
|
||||
* @name Konva.Sprite#start
|
||||
*/
|
||||
start() {
|
||||
if (this.isRunning()) {
|
||||
return;
|
||||
}
|
||||
const layer = this.getLayer();
|
||||
// The interval advances frames; the animation callback requests a redraw
|
||||
// only when a frame has changed.
|
||||
this.anim.setLayers(layer);
|
||||
this._setInterval();
|
||||
this.anim.start();
|
||||
}
|
||||
/**
|
||||
* stop sprite animation
|
||||
* @method
|
||||
* @name Konva.Sprite#stop
|
||||
*/
|
||||
stop() {
|
||||
this.anim.stop();
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
destroy() {
|
||||
this.stop();
|
||||
return super.destroy();
|
||||
}
|
||||
/**
|
||||
* determine if animation of sprite is running or not. returns true or false
|
||||
* @method
|
||||
* @name Konva.Sprite#isRunning
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isRunning() {
|
||||
return this.anim.isRunning();
|
||||
}
|
||||
_updateIndex() {
|
||||
var _a;
|
||||
const index = this.frameIndex(), set = (_a = this.animations()) === null || _a === void 0 ? void 0 : _a[this.animation()], len = set ? set.length / 4 : 0;
|
||||
this.frameIndex(index < len - 1 ? index + 1 : 0);
|
||||
}
|
||||
}
|
||||
Sprite.prototype.className = 'Sprite';
|
||||
_registerNode(Sprite, true);
|
||||
// add getters setters
|
||||
Factory.addGetterSetter(Sprite, 'animation');
|
||||
/**
|
||||
* get/set animation key
|
||||
* @name Konva.Sprite#animation
|
||||
* @method
|
||||
* @param {String} anim animation key
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get animation key
|
||||
* var animation = sprite.animation();
|
||||
*
|
||||
* // set animation key
|
||||
* sprite.animation('kicking');
|
||||
*/
|
||||
Factory.addGetterSetter(Sprite, 'animations');
|
||||
/**
|
||||
* get/set animations map
|
||||
* @name Konva.Sprite#animations
|
||||
* @method
|
||||
* @param {Object} animations
|
||||
* @returns {Object}
|
||||
* @example
|
||||
* // get animations map
|
||||
* var animations = sprite.animations();
|
||||
*
|
||||
* // set animations map
|
||||
* sprite.animations({
|
||||
* standing: [
|
||||
* // x, y, width, height (6 frames)
|
||||
* 0, 0, 49, 109,
|
||||
* 52, 0, 49, 109,
|
||||
* 105, 0, 49, 109,
|
||||
* 158, 0, 49, 109,
|
||||
* 210, 0, 49, 109,
|
||||
* 262, 0, 49, 109
|
||||
* ],
|
||||
* kicking: [
|
||||
* // x, y, width, height (6 frames)
|
||||
* 0, 109, 45, 98,
|
||||
* 45, 109, 45, 98,
|
||||
* 95, 109, 63, 98,
|
||||
* 156, 109, 70, 98,
|
||||
* 229, 109, 60, 98,
|
||||
* 287, 109, 41, 98
|
||||
* ]
|
||||
* });
|
||||
*/
|
||||
Factory.addGetterSetter(Sprite, 'frameOffsets');
|
||||
/**
|
||||
* get/set frame offsets map
|
||||
* @name Konva.Sprite#frameOffsets
|
||||
* @method
|
||||
* @param {Object} frameOffsets
|
||||
* @returns {Object}
|
||||
* @example
|
||||
* // get frame offsets map
|
||||
* var offsets = sprite.frameOffsets();
|
||||
*
|
||||
* // set frame offsets map
|
||||
* sprite.frameOffsets({
|
||||
* standing: [
|
||||
* // x, y (6 frames)
|
||||
* 0, 0,
|
||||
* 0, 0,
|
||||
* 5, 0,
|
||||
* 0, 0,
|
||||
* 0, 3,
|
||||
* 2, 0
|
||||
* ],
|
||||
* kicking: [
|
||||
* // x, y (6 frames)
|
||||
* 0, 5,
|
||||
* 5, 0,
|
||||
* 10, 0,
|
||||
* 0, 0,
|
||||
* 2, 1,
|
||||
* 0, 0
|
||||
* ]
|
||||
* });
|
||||
*/
|
||||
Factory.addGetterSetter(Sprite, 'image');
|
||||
/**
|
||||
* get/set image
|
||||
* @name Konva.Sprite#image
|
||||
* @method
|
||||
* @param {Image} image
|
||||
* @returns {Image}
|
||||
* @example
|
||||
* // get image
|
||||
* var image = sprite.image();
|
||||
*
|
||||
* // set image
|
||||
* sprite.image(imageObj);
|
||||
*/
|
||||
Factory.addGetterSetter(Sprite, 'frameIndex', 0, getNumberValidator());
|
||||
/**
|
||||
* set/set animation frame index
|
||||
* @name Konva.Sprite#frameIndex
|
||||
* @method
|
||||
* @param {Integer} frameIndex
|
||||
* @returns {Integer}
|
||||
* @example
|
||||
* // get animation frame index
|
||||
* var frameIndex = sprite.frameIndex();
|
||||
*
|
||||
* // set animation frame index
|
||||
* sprite.frameIndex(3);
|
||||
*/
|
||||
Factory.addGetterSetter(Sprite, 'frameRate', 17, getNumberValidator());
|
||||
/**
|
||||
* get/set frame rate in frames per second. Increase this number to make the sprite
|
||||
* animation run faster, and decrease the number to make the sprite animation run slower
|
||||
* The default is 17 frames per second
|
||||
* @name Konva.Sprite#frameRate
|
||||
* @method
|
||||
* @param {Integer} frameRate
|
||||
* @returns {Integer}
|
||||
* @example
|
||||
* // get frame rate
|
||||
* var frameRate = sprite.frameRate();
|
||||
*
|
||||
* // set frame rate to 2 frames per second
|
||||
* sprite.frameRate(2);
|
||||
*/
|
||||
Factory.backCompat(Sprite, {
|
||||
index: 'frameIndex',
|
||||
getIndex: 'getFrameIndex',
|
||||
setIndex: 'setFrameIndex',
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import type { Context } from '../Context.ts';
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
export interface StarConfig extends ShapeConfig {
|
||||
numPoints: number;
|
||||
innerRadius: number;
|
||||
outerRadius: number;
|
||||
}
|
||||
/**
|
||||
* Star constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Integer} config.numPoints
|
||||
* @param {Number} config.innerRadius
|
||||
* @param {Number} config.outerRadius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var star = new Konva.Star({
|
||||
* x: 100,
|
||||
* y: 200,
|
||||
* numPoints: 5,
|
||||
* innerRadius: 70,
|
||||
* outerRadius: 70,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 4
|
||||
* });
|
||||
*/
|
||||
export declare class Star extends Shape<StarConfig> {
|
||||
_sceneFunc(context: Context): void;
|
||||
getSelfRect(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
setWidth(width: number): void;
|
||||
setHeight(height: number): void;
|
||||
outerRadius: GetSet<number, this>;
|
||||
innerRadius: GetSet<number, this>;
|
||||
numPoints: GetSet<number, this>;
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { getNumberValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
/**
|
||||
* Star constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Integer} config.numPoints
|
||||
* @param {Number} config.innerRadius
|
||||
* @param {Number} config.outerRadius
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var star = new Konva.Star({
|
||||
* x: 100,
|
||||
* y: 200,
|
||||
* numPoints: 5,
|
||||
* innerRadius: 70,
|
||||
* outerRadius: 70,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 4
|
||||
* });
|
||||
*/
|
||||
export class Star extends Shape {
|
||||
_sceneFunc(context) {
|
||||
const innerRadius = this.innerRadius(), outerRadius = this.outerRadius(), numPoints = this.numPoints();
|
||||
context.beginPath();
|
||||
context.moveTo(0, 0 - outerRadius);
|
||||
for (let n = 1; n < numPoints * 2; n++) {
|
||||
const radius = n % 2 === 0 ? outerRadius : innerRadius;
|
||||
const x = radius * Math.sin((n * Math.PI) / numPoints);
|
||||
const y = -1 * radius * Math.cos((n * Math.PI) / numPoints);
|
||||
context.lineTo(x, y);
|
||||
}
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
getSelfRect() {
|
||||
const radius = Math.max(Math.abs(this.innerRadius()), Math.abs(this.outerRadius()));
|
||||
return { x: -radius, y: -radius, width: radius * 2, height: radius * 2 };
|
||||
}
|
||||
getWidth() {
|
||||
return this.outerRadius() * 2;
|
||||
}
|
||||
getHeight() {
|
||||
return this.outerRadius() * 2;
|
||||
}
|
||||
setWidth(width) {
|
||||
this.outerRadius(width / 2);
|
||||
}
|
||||
setHeight(height) {
|
||||
this.outerRadius(height / 2);
|
||||
}
|
||||
}
|
||||
Star.prototype.className = 'Star';
|
||||
Star.prototype._centroid = true;
|
||||
Star.prototype._attrsAffectingSize = ['innerRadius', 'outerRadius'];
|
||||
_registerNode(Star, true);
|
||||
/**
|
||||
* get/set number of points
|
||||
* @name Konva.Star#numPoints
|
||||
* @method
|
||||
* @param {Number} numPoints
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get inner radius
|
||||
* var numPoints = star.numPoints();
|
||||
*
|
||||
* // set inner radius
|
||||
* star.numPoints(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Star, 'numPoints', 5, getNumberValidator());
|
||||
/**
|
||||
* get/set innerRadius
|
||||
* @name Konva.Star#innerRadius
|
||||
* @method
|
||||
* @param {Number} innerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get inner radius
|
||||
* var innerRadius = star.innerRadius();
|
||||
*
|
||||
* // set inner radius
|
||||
* star.innerRadius(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Star, 'innerRadius', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set outerRadius
|
||||
* @name Konva.Star#outerRadius
|
||||
* @method
|
||||
* @param {Number} outerRadius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get inner radius
|
||||
* var outerRadius = star.outerRadius();
|
||||
*
|
||||
* // set inner radius
|
||||
* star.outerRadius(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Star, 'outerRadius', 0, getNumberValidator());
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import type { Context, SceneContext } from '../Context.ts';
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
export interface CharRenderProps {
|
||||
char: string;
|
||||
index: number;
|
||||
x: number;
|
||||
y: number;
|
||||
lineIndex: number;
|
||||
column: number;
|
||||
isLastInLine: boolean;
|
||||
width: number;
|
||||
context: Context;
|
||||
}
|
||||
export declare function getDecorationLineWidth(fontSize: number): number;
|
||||
export declare function stringToArray(string: string): string[];
|
||||
export interface TextConfig extends ShapeConfig {
|
||||
direction?: string;
|
||||
text?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontStyle?: string;
|
||||
fontVariant?: string;
|
||||
textDecoration?: string;
|
||||
underlineOffset?: number;
|
||||
align?: string;
|
||||
verticalAlign?: string;
|
||||
padding?: number;
|
||||
lineHeight?: number;
|
||||
letterSpacing?: number;
|
||||
wrap?: string;
|
||||
ellipsis?: boolean;
|
||||
charRenderFunc?: null | ((props: CharRenderProps) => void);
|
||||
}
|
||||
export declare function getDummyContext(): CanvasRenderingContext2D;
|
||||
/**
|
||||
* Text constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {String} [config.direction] default is inherit
|
||||
* @param {String} [config.fontFamily] default is Arial
|
||||
* @param {Number} [config.fontSize] in pixels. Default is 12
|
||||
* @param {String} [config.fontStyle] can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default.
|
||||
* @param {String} [config.fontVariant] can be normal or small-caps. Default is normal
|
||||
* @param {String} [config.textDecoration] can be line-through, underline or empty string. Default is empty string.
|
||||
* @param {Number} [config.underlineOffset] offset for underline line. Default is calculated based on font size.
|
||||
* @param {String} config.text
|
||||
* @param {String} [config.align] can be left, center, right or justify
|
||||
* @param {String} [config.verticalAlign] can be top, middle or bottom
|
||||
* @param {Number} [config.padding]
|
||||
* @param {Number} [config.lineHeight] default is 1
|
||||
* @param {String} [config.wrap] can be "word", "char", or "none". Default is word
|
||||
* @param {Boolean} [config.ellipsis] can be true or false. Default is false. If true, text that does not fit is cut and ends with "…". That needs a fixed height or wrap="none"
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var text = new Konva.Text({
|
||||
* x: 10,
|
||||
* y: 15,
|
||||
* text: 'Simple Text',
|
||||
* fontSize: 30,
|
||||
* fontFamily: 'Calibri',
|
||||
* fill: 'green'
|
||||
* });
|
||||
*/
|
||||
export declare class Text extends Shape<TextConfig> {
|
||||
textArr: Array<{
|
||||
text: string;
|
||||
width: number;
|
||||
lastInParagraph: boolean;
|
||||
}>;
|
||||
_partialText: string;
|
||||
_partialTextX: number;
|
||||
_partialTextY: number;
|
||||
_partialFillStyle?: string | CanvasGradient | CanvasPattern;
|
||||
_partialStrokeStyle?: string | CanvasGradient | CanvasPattern;
|
||||
textWidth: number;
|
||||
textHeight: number;
|
||||
_baselineShift: number;
|
||||
constructor(config?: TextConfig);
|
||||
_sceneFunc(context: SceneContext): void;
|
||||
_getTextTop(): number;
|
||||
_getUnderlineOffset(): number;
|
||||
getSelfRect(): import("../types.ts").IRect;
|
||||
_getSelfRectForDrawing(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
_hitFunc(context: Context): void;
|
||||
setText(text: string): this;
|
||||
getWidth(): any;
|
||||
getHeight(): any;
|
||||
/**
|
||||
* get pure text width without padding
|
||||
* @method
|
||||
* @name Konva.Text#getTextWidth
|
||||
* @returns {Number}
|
||||
*/
|
||||
getTextWidth(): number;
|
||||
getTextHeight(): number;
|
||||
/**
|
||||
* measure string with the font of current text shape.
|
||||
* That method can't handle multiline text.
|
||||
* @method
|
||||
* @name Konva.Text#measureSize
|
||||
* @param {String} text text to measure
|
||||
* @returns {Object} { width , height } of measured text
|
||||
*/
|
||||
measureSize(text: string): {
|
||||
actualBoundingBoxAscent: number;
|
||||
actualBoundingBoxDescent: number;
|
||||
actualBoundingBoxLeft: number;
|
||||
actualBoundingBoxRight: number;
|
||||
alphabeticBaseline: number;
|
||||
emHeightAscent: number;
|
||||
emHeightDescent: number;
|
||||
fontBoundingBoxAscent: number;
|
||||
fontBoundingBoxDescent: number;
|
||||
hangingBaseline: number;
|
||||
ideographicBaseline: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
_getContextFont(): string;
|
||||
_addTextLine(line: string): number;
|
||||
_getTextWidth(text: string, graphemes?: number): number;
|
||||
_setTextData(): void;
|
||||
/**
|
||||
* whether to handle ellipsis, there are two cases:
|
||||
* 1. the current line is the last line
|
||||
* 2. wrap is NONE
|
||||
* @param {Number} currentHeightPx
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_shouldHandleEllipsis(currentHeightPx: number): boolean;
|
||||
_tryToAddEllipsisToLastLine(): void;
|
||||
getStrokeScaleEnabled(): boolean;
|
||||
_getStrokePadding(): number;
|
||||
_useBufferCanvas(forceFill?: boolean, opacity?: any): boolean;
|
||||
direction: GetSet<string, this>;
|
||||
fontFamily: GetSet<string, this>;
|
||||
fontSize: GetSet<number, this>;
|
||||
fontStyle: GetSet<string, this>;
|
||||
fontVariant: GetSet<string, this>;
|
||||
align: GetSet<string, this>;
|
||||
letterSpacing: GetSet<number, this>;
|
||||
verticalAlign: GetSet<string, this>;
|
||||
padding: GetSet<number, this>;
|
||||
lineHeight: GetSet<number, this>;
|
||||
textDecoration: GetSet<string, this>;
|
||||
underlineOffset: GetSet<number, this>;
|
||||
text: GetSet<string, this>;
|
||||
wrap: GetSet<string, this>;
|
||||
ellipsis: GetSet<boolean, this>;
|
||||
charRenderFunc: GetSet<null | ((props: CharRenderProps) => void), this>;
|
||||
width: GetSet<number, this, number | 'auto' | null | undefined>;
|
||||
height: GetSet<number, this, number | 'auto' | null | undefined>;
|
||||
}
|
||||
+968
@@ -0,0 +1,968 @@
|
||||
import { Util } from "../Util.js";
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { Konva } from "../Global.js";
|
||||
import { getNumberValidator, getStringValidator, getNumberOrAutoValidator, getBooleanValidator, } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
// grapheme segmentation: one entry per user-perceived character, so flags,
|
||||
// ZWJ emoji sequences and combining marks stay together
|
||||
const segmenter = typeof Intl !== 'undefined' && Intl.Segmenter
|
||||
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
||||
: null;
|
||||
// canvas has no API for the underline thickness of a font. This is close to
|
||||
// what browsers draw for the common fonts
|
||||
export function getDecorationLineWidth(fontSize) {
|
||||
return fontSize / 15;
|
||||
}
|
||||
export function stringToArray(string) {
|
||||
if (!segmenter) {
|
||||
// no Intl.Segmenter (Firefox < 125): flags, then a code point with its
|
||||
// combining marks, skin tone modifiers, variation selectors and ZWJ joins
|
||||
return (string.match(/\p{RI}\p{RI}|\P{M}(?:\p{M}|\p{Emoji_Modifier}|\uFE0F|\u200D\P{M})*|\p{M}+/gu) || []);
|
||||
}
|
||||
return Array.from(segmenter.segment(string), (s) => s.segment);
|
||||
}
|
||||
// constants
|
||||
const AUTO = 'auto',
|
||||
//CANVAS = 'canvas',
|
||||
CENTER = 'center', INHERIT = 'inherit', JUSTIFY = 'justify', CHANGE_KONVA = 'Change.konva', CONTEXT_2D = '2d', DASH = '-', LEFT = 'left', TEXT = 'text', TEXT_UPPER = 'Text', TOP = 'top', BOTTOM = 'bottom', MIDDLE = 'middle', NORMAL = 'normal', PX_SPACE = 'px ', SPACE = ' ', RIGHT = 'right', RTL = 'rtl', WORD = 'word', CHAR = 'char', NONE = 'none', ELLIPSIS = '…', ATTR_CHANGE_LIST = [
|
||||
'direction',
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontStyle',
|
||||
'fontVariant',
|
||||
'padding',
|
||||
'align',
|
||||
'verticalAlign',
|
||||
'lineHeight',
|
||||
'text',
|
||||
'width',
|
||||
'height',
|
||||
'wrap',
|
||||
'ellipsis',
|
||||
'letterSpacing',
|
||||
];
|
||||
// Safari on macOS renders text shadow even when globalAlpha is 0.
|
||||
// This feature check detects that bug so we can work around it with a buffer canvas.
|
||||
// Not covered by automated tests (our CI doesn't run Safari).
|
||||
// Once Safari fixes the issue this check can be removed.
|
||||
let _shadowOpacityBuggy = null;
|
||||
function hasShadowOpacityBug() {
|
||||
if (_shadowOpacityBuggy !== null) {
|
||||
return _shadowOpacityBuggy;
|
||||
}
|
||||
_shadowOpacityBuggy = false;
|
||||
let c;
|
||||
try {
|
||||
c = Util.createCanvasElement();
|
||||
c.width = 10;
|
||||
c.height = 10;
|
||||
const ctx = c.getContext(CONTEXT_2D);
|
||||
if (ctx) {
|
||||
ctx.globalAlpha = 0;
|
||||
ctx.shadowColor = 'black';
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.shadowOffsetX = 5;
|
||||
ctx.shadowOffsetY = 5;
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.font = '10px Arial';
|
||||
ctx.fillText('X', 0, 10);
|
||||
const data = ctx.getImageData(0, 0, 10, 10).data;
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i] > 0) {
|
||||
_shadowOpacityBuggy = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// no-op (SSR or restricted canvas)
|
||||
}
|
||||
finally {
|
||||
if (c) {
|
||||
Util.releaseCanvas(c);
|
||||
}
|
||||
}
|
||||
return _shadowOpacityBuggy;
|
||||
}
|
||||
function normalizeFontFamily(fontFamily) {
|
||||
return fontFamily
|
||||
.split(',')
|
||||
.map((family) => {
|
||||
family = family.trim();
|
||||
const hasSpace = family.indexOf(' ') >= 0;
|
||||
const hasQuotes = family.indexOf('"') >= 0 || family.indexOf("'") >= 0;
|
||||
if (hasSpace && !hasQuotes) {
|
||||
family = `"${family}"`;
|
||||
}
|
||||
return family;
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
let dummyContext;
|
||||
export function getDummyContext() {
|
||||
if (dummyContext) {
|
||||
return dummyContext;
|
||||
}
|
||||
dummyContext = Util.createCanvasElement().getContext(CONTEXT_2D);
|
||||
return dummyContext;
|
||||
}
|
||||
function _fillFunc(context) {
|
||||
// re-apply a per-character style set by charRenderFunc (see _sceneFunc)
|
||||
if (this._partialFillStyle) {
|
||||
context.setAttr('fillStyle', this._partialFillStyle);
|
||||
}
|
||||
context.fillText(this._partialText, this._partialTextX, this._partialTextY);
|
||||
}
|
||||
function _strokeFunc(context) {
|
||||
context.setAttr('miterLimit', 2);
|
||||
if (this._partialStrokeStyle) {
|
||||
context.setAttr('strokeStyle', this._partialStrokeStyle);
|
||||
}
|
||||
context.strokeText(this._partialText, this._partialTextX, this._partialTextY);
|
||||
}
|
||||
function checkDefaultFill(config) {
|
||||
config = config || {};
|
||||
// set default color to black, without mutating the caller's config
|
||||
const hasFill = config.fill ||
|
||||
config.fillLinearGradientColorStops ||
|
||||
config.fillRadialGradientColorStops ||
|
||||
config.fillPatternImage;
|
||||
return hasFill ? config : { ...config, fill: 'black' };
|
||||
}
|
||||
/**
|
||||
* Text constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {String} [config.direction] default is inherit
|
||||
* @param {String} [config.fontFamily] default is Arial
|
||||
* @param {Number} [config.fontSize] in pixels. Default is 12
|
||||
* @param {String} [config.fontStyle] can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default.
|
||||
* @param {String} [config.fontVariant] can be normal or small-caps. Default is normal
|
||||
* @param {String} [config.textDecoration] can be line-through, underline or empty string. Default is empty string.
|
||||
* @param {Number} [config.underlineOffset] offset for underline line. Default is calculated based on font size.
|
||||
* @param {String} config.text
|
||||
* @param {String} [config.align] can be left, center, right or justify
|
||||
* @param {String} [config.verticalAlign] can be top, middle or bottom
|
||||
* @param {Number} [config.padding]
|
||||
* @param {Number} [config.lineHeight] default is 1
|
||||
* @param {String} [config.wrap] can be "word", "char", or "none". Default is word
|
||||
* @param {Boolean} [config.ellipsis] can be true or false. Default is false. If true, text that does not fit is cut and ends with "…". That needs a fixed height or wrap="none"
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var text = new Konva.Text({
|
||||
* x: 10,
|
||||
* y: 15,
|
||||
* text: 'Simple Text',
|
||||
* fontSize: 30,
|
||||
* fontFamily: 'Calibri',
|
||||
* fill: 'green'
|
||||
* });
|
||||
*/
|
||||
export class Text extends Shape {
|
||||
constructor(config) {
|
||||
super(checkDefaultFill(config));
|
||||
this._partialTextX = 0;
|
||||
this._partialTextY = 0;
|
||||
// distance from the line box center to the baseline, see _setTextData
|
||||
this._baselineShift = 0;
|
||||
this._setTextData();
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
var _a, _b;
|
||||
const textArr = this.textArr, textArrLen = textArr.length;
|
||||
if (!this.text()) {
|
||||
return;
|
||||
}
|
||||
let padding = this.padding(), fontSize = this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, direction = this.direction(), align = this.align(), totalWidth = this.getWidth(), letterSpacing = this.letterSpacing(), charRenderFunc = this.charRenderFunc(), textDecoration = this.textDecoration(), shouldUnderline = textDecoration.indexOf('underline') !== -1, shouldLineThrough = textDecoration.indexOf('line-through') !== -1, n;
|
||||
let translateY = lineHeightPx / 2;
|
||||
let baseline = MIDDLE;
|
||||
if (!Konva.legacyTextRendering) {
|
||||
const metrics = this.measureSize('M'); // Use a sample character to get the ascent
|
||||
baseline = 'alphabetic';
|
||||
const ascent = (_a = metrics.fontBoundingBoxAscent) !== null && _a !== void 0 ? _a : metrics.actualBoundingBoxAscent;
|
||||
const descent = (_b = metrics.fontBoundingBoxDescent) !== null && _b !== void 0 ? _b : metrics.actualBoundingBoxDescent;
|
||||
translateY = (ascent - descent) / 2 + lineHeightPx / 2;
|
||||
}
|
||||
if (direction !== INHERIT) {
|
||||
context.setAttr('direction', direction);
|
||||
}
|
||||
else {
|
||||
// 'inherit' resolves to the direction the canvas already has, so that
|
||||
// an inherited rtl still takes the single native run below
|
||||
direction = context.direction;
|
||||
}
|
||||
context.setAttr('font', this._getContextFont());
|
||||
context.setAttr('textBaseline', baseline);
|
||||
context.setAttr('textAlign', LEFT);
|
||||
context.translate(padding, this._getTextTop());
|
||||
// Start the callback with the same fill that normal drawing would use.
|
||||
// Assigning that color again then needs no override, including black.
|
||||
if (charRenderFunc) {
|
||||
const style = context._getFillStyle(this);
|
||||
if (style !== undefined)
|
||||
context.fillStyle = style;
|
||||
if (this.hasStroke())
|
||||
context._applyStrokeStyle(this);
|
||||
}
|
||||
// context styles entering the loop, used to detect styles set by
|
||||
// charRenderFunc; every character is drawn inside save()/restore(),
|
||||
// so the entry state is the same for each one
|
||||
const fillStyleBefore = charRenderFunc ? context.fillStyle : undefined;
|
||||
const strokeStyleBefore = charRenderFunc ? context.strokeStyle : undefined;
|
||||
// global grapheme index passed to charRenderFunc, runs across lines
|
||||
let charIndex = 0;
|
||||
// draw text lines
|
||||
for (n = 0; n < textArrLen; n++) {
|
||||
let lineTranslateX = 0;
|
||||
let lineTranslateY = 0;
|
||||
const obj = textArr[n], text = obj.text, width = obj.width, lastLine = obj.lastInParagraph;
|
||||
// horizontal alignment
|
||||
context.save();
|
||||
if (align === RIGHT) {
|
||||
lineTranslateX += totalWidth - width - padding * 2;
|
||||
}
|
||||
else if (align === CENTER) {
|
||||
lineTranslateX += (totalWidth - width - padding * 2) / 2;
|
||||
}
|
||||
if (shouldUnderline) {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
const yOffset = this._getUnderlineOffset();
|
||||
const x = lineTranslateX;
|
||||
const y = translateY + lineTranslateY + yOffset;
|
||||
context.moveTo(x, y);
|
||||
const lineWidth = align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width;
|
||||
context.lineTo(x + Math.round(lineWidth), y);
|
||||
context.lineWidth = getDecorationLineWidth(fontSize);
|
||||
context.strokeStyle = context._getFillStyle(this);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
// store the starting x position for line-through which is drawn after text
|
||||
const lineThroughStartX = lineTranslateX;
|
||||
// Render graphemes separately for spacing, justification and callbacks.
|
||||
// Keep RTL text in one native run to preserve shaping.
|
||||
if (direction !== RTL &&
|
||||
(letterSpacing !== 0 || align === JUSTIFY || charRenderFunc)) {
|
||||
const spacesNumber = text.split(' ').length - 1;
|
||||
const array = stringToArray(text);
|
||||
for (let li = 0; li < array.length; li++) {
|
||||
const letter = array[li];
|
||||
// skip justify for the last line
|
||||
if (letter === ' ' && !lastLine && align === JUSTIFY) {
|
||||
lineTranslateX += (totalWidth - padding * 2 - width) / spacesNumber;
|
||||
}
|
||||
this._partialTextX = lineTranslateX;
|
||||
this._partialTextY = translateY + lineTranslateY;
|
||||
this._partialText = letter;
|
||||
// the font of the shape is already set on the context
|
||||
const letterWidth = context.measureText(letter).width;
|
||||
if (charRenderFunc) {
|
||||
context.save();
|
||||
charRenderFunc({
|
||||
char: letter,
|
||||
index: charIndex,
|
||||
x: lineTranslateX,
|
||||
y: translateY + lineTranslateY,
|
||||
lineIndex: n,
|
||||
column: li,
|
||||
isLastInLine: li === array.length - 1,
|
||||
width: letterWidth,
|
||||
context,
|
||||
});
|
||||
// a style the callback set on the context wins for this character;
|
||||
// fillStrokeShape() overwrites the context styles from the shape
|
||||
// attributes, so stash them for _fillFunc/_strokeFunc to re-apply
|
||||
const fillStyleAfter = context.fillStyle;
|
||||
if (fillStyleAfter !== fillStyleBefore) {
|
||||
this._partialFillStyle = fillStyleAfter;
|
||||
}
|
||||
const strokeStyleAfter = context.strokeStyle;
|
||||
if (strokeStyleAfter !== strokeStyleBefore) {
|
||||
this._partialStrokeStyle = strokeStyleAfter;
|
||||
}
|
||||
}
|
||||
context.fillStrokeShape(this);
|
||||
if (charRenderFunc) {
|
||||
this._partialFillStyle = undefined;
|
||||
this._partialStrokeStyle = undefined;
|
||||
context.restore();
|
||||
}
|
||||
lineTranslateX += letterWidth + letterSpacing;
|
||||
charIndex++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (letterSpacing !== 0) {
|
||||
context.setAttr('letterSpacing', `${letterSpacing}px`);
|
||||
}
|
||||
this._partialTextX = lineTranslateX;
|
||||
this._partialTextY = translateY + lineTranslateY;
|
||||
this._partialText = text;
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
// draw line-through above the text content
|
||||
if (shouldLineThrough) {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
const yOffset = !Konva.legacyTextRendering
|
||||
? -Math.round(fontSize / 4)
|
||||
: 0;
|
||||
const x = lineThroughStartX;
|
||||
context.moveTo(x, translateY + lineTranslateY + yOffset);
|
||||
const lineWidth = align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width;
|
||||
context.lineTo(x + Math.round(lineWidth), translateY + lineTranslateY + yOffset);
|
||||
context.lineWidth = getDecorationLineWidth(fontSize);
|
||||
context.strokeStyle = context._getFillStyle(this);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
context.restore();
|
||||
if (textArrLen > 1) {
|
||||
translateY += lineHeightPx;
|
||||
}
|
||||
}
|
||||
}
|
||||
_getTextTop() {
|
||||
const padding = this.padding(), free = this.getHeight() -
|
||||
this.textArr.length * this.lineHeight() * this.fontSize() -
|
||||
padding * 2, verticalAlign = this.verticalAlign();
|
||||
if (verticalAlign === MIDDLE)
|
||||
return padding + free / 2;
|
||||
if (verticalAlign === BOTTOM)
|
||||
return padding + free;
|
||||
return padding;
|
||||
}
|
||||
_getUnderlineOffset() {
|
||||
var _a;
|
||||
return ((_a = this.underlineOffset()) !== null && _a !== void 0 ? _a : Math.round(this.fontSize() / (!Konva.legacyTextRendering ? 4 : 2)));
|
||||
}
|
||||
getSelfRect() {
|
||||
const rect = super.getSelfRect();
|
||||
const lines = this.textArr.length;
|
||||
if (!lines || this.textDecoration().indexOf('underline') === -1) {
|
||||
return rect;
|
||||
}
|
||||
// the underline of the last line is drawn below the text block
|
||||
const fontSize = this.fontSize(), lineHeightPx = this.lineHeight() * fontSize;
|
||||
let bottom = this._getTextTop() + lines * lineHeightPx - lineHeightPx / 2;
|
||||
if (!Konva.legacyTextRendering) {
|
||||
bottom += this._baselineShift;
|
||||
}
|
||||
bottom += this._getUnderlineOffset() + getDecorationLineWidth(fontSize) / 2;
|
||||
rect.height = Math.max(rect.y + rect.height, bottom) - rect.y;
|
||||
return rect;
|
||||
}
|
||||
_getSelfRectForDrawing() {
|
||||
// Lines can be wider than the box, negative spacing moves the pen back,
|
||||
// and glyph ink passes the advance box (italics, accents): up to 0.6em
|
||||
// in common fonts, so pad by 1em.
|
||||
const rect = this.getSelfRect(), fontSize = this.fontSize(), padding = this.padding(), lineHeightPx = this.lineHeight() * fontSize, available = this.getWidth() - padding * 2, align = this.align(), blockHeight = this.textArr.length * lineHeightPx, top = this._getTextTop();
|
||||
let left = rect.x, right = rect.x + rect.width;
|
||||
for (const { text, width } of this.textArr) {
|
||||
const x = padding +
|
||||
(align === RIGHT
|
||||
? available - width
|
||||
: align === CENTER
|
||||
? (available - width) / 2
|
||||
: 0);
|
||||
const back = Math.min(0, this.letterSpacing()) * text.length;
|
||||
left = Math.min(left, x + back);
|
||||
right = Math.max(right, x + width - back);
|
||||
}
|
||||
const underline = this.textDecoration().includes('underline');
|
||||
const y = Math.min(rect.y, top + (underline ? Math.min(0, this._getUnderlineOffset()) : 0));
|
||||
return {
|
||||
x: left - fontSize,
|
||||
y: y - fontSize,
|
||||
width: right - left + fontSize * 2,
|
||||
height: Math.max(rect.y + rect.height, top + blockHeight) - y + fontSize * 2,
|
||||
};
|
||||
}
|
||||
_hitFunc(context) {
|
||||
const width = this.getWidth(), height = this.getHeight();
|
||||
context.beginPath();
|
||||
context.rect(0, 0, width, height);
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
setText(text) {
|
||||
const str = Util._isString(text)
|
||||
? text
|
||||
: text === null || text === undefined
|
||||
? ''
|
||||
: text + '';
|
||||
this._setAttr(TEXT, str);
|
||||
return this;
|
||||
}
|
||||
getWidth() {
|
||||
const isAuto = this.attrs.width === AUTO || this.attrs.width === undefined;
|
||||
return isAuto ? this.getTextWidth() + this.padding() * 2 : this.attrs.width;
|
||||
}
|
||||
getHeight() {
|
||||
const isAuto = this.attrs.height === AUTO || this.attrs.height === undefined;
|
||||
return isAuto
|
||||
? this.fontSize() * this.textArr.length * this.lineHeight() +
|
||||
this.padding() * 2
|
||||
: this.attrs.height;
|
||||
}
|
||||
/**
|
||||
* get pure text width without padding
|
||||
* @method
|
||||
* @name Konva.Text#getTextWidth
|
||||
* @returns {Number}
|
||||
*/
|
||||
getTextWidth() {
|
||||
return this.textWidth;
|
||||
}
|
||||
getTextHeight() {
|
||||
Util.warn('text.getTextHeight() method is deprecated. Use text.height() - for full height and text.fontSize() - for one line height.');
|
||||
return this.textHeight;
|
||||
}
|
||||
/**
|
||||
* measure string with the font of current text shape.
|
||||
* That method can't handle multiline text.
|
||||
* @method
|
||||
* @name Konva.Text#measureSize
|
||||
* @param {String} text text to measure
|
||||
* @returns {Object} { width , height } of measured text
|
||||
*/
|
||||
measureSize(text) {
|
||||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
||||
let _context = getDummyContext(), fontSize = this.fontSize(), metrics;
|
||||
_context.save();
|
||||
_context.font = this._getContextFont();
|
||||
metrics = _context.measureText(text);
|
||||
_context.restore();
|
||||
// Scale the fallback values based on the provided fontSize compared to the sample size (100 in your new case)
|
||||
const scaleFactor = fontSize / 100;
|
||||
// Note, fallback values are from chrome browser with 100px font size and font-family "Arial"
|
||||
return {
|
||||
actualBoundingBoxAscent: (_a = metrics.actualBoundingBoxAscent) !== null && _a !== void 0 ? _a : 71.58203125 * scaleFactor,
|
||||
actualBoundingBoxDescent: (_b = metrics.actualBoundingBoxDescent) !== null && _b !== void 0 ? _b : 0, // Remains zero as there is no descent in the provided metrics
|
||||
actualBoundingBoxLeft: (_c = metrics.actualBoundingBoxLeft) !== null && _c !== void 0 ? _c : -7.421875 * scaleFactor,
|
||||
actualBoundingBoxRight: (_d = metrics.actualBoundingBoxRight) !== null && _d !== void 0 ? _d : 75.732421875 * scaleFactor,
|
||||
alphabeticBaseline: (_e = metrics.alphabeticBaseline) !== null && _e !== void 0 ? _e : 0, // Remains zero as it's typically relative to the baseline itself
|
||||
emHeightAscent: (_f = metrics.emHeightAscent) !== null && _f !== void 0 ? _f : 100 * scaleFactor,
|
||||
emHeightDescent: (_g = metrics.emHeightDescent) !== null && _g !== void 0 ? _g : -20 * scaleFactor,
|
||||
fontBoundingBoxAscent: (_h = metrics.fontBoundingBoxAscent) !== null && _h !== void 0 ? _h : 91 * scaleFactor,
|
||||
fontBoundingBoxDescent: (_j = metrics.fontBoundingBoxDescent) !== null && _j !== void 0 ? _j : 21 * scaleFactor,
|
||||
hangingBaseline: (_k = metrics.hangingBaseline) !== null && _k !== void 0 ? _k : 72.80000305175781 * scaleFactor,
|
||||
ideographicBaseline: (_l = metrics.ideographicBaseline) !== null && _l !== void 0 ? _l : -21 * scaleFactor,
|
||||
width: metrics.width,
|
||||
height: fontSize, // Typically set to the font size
|
||||
};
|
||||
}
|
||||
_getContextFont() {
|
||||
return (this.fontStyle() +
|
||||
SPACE +
|
||||
this.fontVariant() +
|
||||
SPACE +
|
||||
(this.fontSize() + PX_SPACE) +
|
||||
// wrap font family into " so font families with spaces works ok
|
||||
normalizeFontFamily(this.fontFamily()));
|
||||
}
|
||||
_addTextLine(line) {
|
||||
const align = this.align();
|
||||
if (align === JUSTIFY) {
|
||||
line = line.trim();
|
||||
}
|
||||
const width = this._getTextWidth(line);
|
||||
return this.textArr.push({
|
||||
text: line,
|
||||
width: width,
|
||||
lastInParagraph: false,
|
||||
});
|
||||
}
|
||||
_getTextWidth(text, graphemes) {
|
||||
const letterSpacing = this.letterSpacing();
|
||||
// one spacing per grapheme, as the per-character rendering draws them
|
||||
const spacing = letterSpacing
|
||||
? letterSpacing * (graphemes !== null && graphemes !== void 0 ? graphemes : stringToArray(text).length)
|
||||
: 0;
|
||||
return getDummyContext().measureText(text).width + spacing;
|
||||
}
|
||||
_setTextData() {
|
||||
let lines = this.text().split('\n'), fontSize = +this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, width = this.attrs.width, height = this.attrs.height, fixedWidth = width !== AUTO && width !== undefined, fixedHeight = height !== AUTO && height !== undefined, padding = this.padding(), maxWidth = width - padding * 2, maxHeightPx = height - padding * 2, currentHeightPx = 0, wrap = this.wrap(),
|
||||
// align = this.align(),
|
||||
shouldWrap = wrap !== NONE, wrapAtWord = wrap !== CHAR && shouldWrap, shouldAddEllipsis = this.ellipsis();
|
||||
// measured here so getSelfRect, which runs per drag frame, needs no
|
||||
// measureText of its own
|
||||
const sample = this.measureSize('M');
|
||||
this._baselineShift =
|
||||
(sample.fontBoundingBoxAscent - sample.fontBoundingBoxDescent) / 2;
|
||||
this.textArr = [];
|
||||
const dummyContext = getDummyContext();
|
||||
dummyContext.font = this._getContextFont();
|
||||
// the per-character draw path (same condition as in _sceneFunc) advances
|
||||
// by unkerned glyph widths, so measure the line the same way
|
||||
dummyContext.fontKerning =
|
||||
this.direction() !== RTL &&
|
||||
(this.letterSpacing() !== 0 ||
|
||||
this.align() === JUSTIFY ||
|
||||
!!this.charRenderFunc())
|
||||
? 'none'
|
||||
: 'auto';
|
||||
const additionalWidth = shouldAddEllipsis
|
||||
? this._getTextWidth(ELLIPSIS)
|
||||
: 0;
|
||||
for (let i = 0, max = lines.length; i < max; ++i) {
|
||||
let line = lines[i];
|
||||
let lineWidth = this._getTextWidth(line);
|
||||
if (fixedWidth && lineWidth > maxWidth) {
|
||||
/*
|
||||
* if width is fixed and line does not fit entirely
|
||||
* break the line into multiple fitting lines
|
||||
*/
|
||||
const graphemes = stringToArray(line);
|
||||
const length = graphemes.length;
|
||||
let start = 0;
|
||||
const text = (end) => graphemes.slice(start, end).join('');
|
||||
const isBreak = (char) => char === SPACE || char === DASH;
|
||||
// graphemes per line, estimated from the average grapheme width
|
||||
const perLine = Math.max(1, Math.ceil((length * maxWidth) / lineWidth));
|
||||
while (start < length) {
|
||||
// only reserve the ellipsis width on a line that may be the last
|
||||
// visible one
|
||||
const extraWidth = shouldAddEllipsis &&
|
||||
fixedHeight &&
|
||||
currentHeightPx + lineHeightPx > maxHeightPx
|
||||
? additionalWidth
|
||||
: 0;
|
||||
const fits = (end) => {
|
||||
const width = this._getTextWidth(text(end), end - start);
|
||||
if (width + extraWidth > maxWidth) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/*
|
||||
* find the longest prefix that fits in the specified width:
|
||||
* grow a window from the estimate until it stops fitting, then
|
||||
* binary search inside it. Every probe measures about one line,
|
||||
* not the whole remaining text, so wrapping stays linear
|
||||
*/
|
||||
let low = start, high = Math.min(length, start + perLine);
|
||||
while (fits(high)) {
|
||||
low = high;
|
||||
if (high === length) {
|
||||
break;
|
||||
}
|
||||
high = Math.min(length, high + (high - start));
|
||||
}
|
||||
while (high - low > 1) {
|
||||
const mid = (low + high) >>> 1;
|
||||
if (fits(mid)) {
|
||||
low = mid;
|
||||
}
|
||||
else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
if (low === start) {
|
||||
// not even one character could fit in the element, abort
|
||||
break;
|
||||
}
|
||||
if (low === length) {
|
||||
// the rest of the paragraph fits on one line: kept untrimmed and
|
||||
// without the ellipsis check, like a paragraph that never wrapped
|
||||
this._addTextLine(text(length));
|
||||
currentHeightPx += lineHeightPx;
|
||||
if (fixedHeight &&
|
||||
currentHeightPx + lineHeightPx > maxHeightPx &&
|
||||
i < max - 1) {
|
||||
this._tryToAddEllipsisToLastLine();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (wrapAtWord && !isBreak(graphemes[low])) {
|
||||
// wrap at the last space or dash of the line instead
|
||||
let wrapIndex = low - 1;
|
||||
while (wrapIndex >= start && !isBreak(graphemes[wrapIndex])) {
|
||||
wrapIndex--;
|
||||
}
|
||||
if (wrapIndex >= start) {
|
||||
low = wrapIndex + 1;
|
||||
}
|
||||
}
|
||||
this._addTextLine(text(low).trimRight());
|
||||
currentHeightPx += lineHeightPx;
|
||||
if (this._shouldHandleEllipsis(currentHeightPx)) {
|
||||
this._tryToAddEllipsisToLastLine();
|
||||
/*
|
||||
* stop wrapping if wrapping is disabled or if adding
|
||||
* one more line would overflow the fixed height
|
||||
*/
|
||||
break;
|
||||
}
|
||||
// the next line starts after the whitespace of the break
|
||||
start = low;
|
||||
while (start < length && !graphemes[start].trim()) {
|
||||
start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// element width is automatically adjusted to max line width
|
||||
this._addTextLine(line);
|
||||
currentHeightPx += lineHeightPx;
|
||||
if (fixedHeight &&
|
||||
currentHeightPx + lineHeightPx > maxHeightPx &&
|
||||
i < max - 1) {
|
||||
this._tryToAddEllipsisToLastLine();
|
||||
}
|
||||
}
|
||||
// if element height is fixed, abort if adding one more line would overflow
|
||||
if (this.textArr[this.textArr.length - 1]) {
|
||||
this.textArr[this.textArr.length - 1].lastInParagraph = true;
|
||||
}
|
||||
if (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.textHeight = fontSize;
|
||||
this.textWidth = this.textArr.reduce((width, line) => Math.max(width, line.width), 0);
|
||||
}
|
||||
/**
|
||||
* whether to handle ellipsis, there are two cases:
|
||||
* 1. the current line is the last line
|
||||
* 2. wrap is NONE
|
||||
* @param {Number} currentHeightPx
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_shouldHandleEllipsis(currentHeightPx) {
|
||||
const fontSize = +this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, height = this.attrs.height, fixedHeight = height !== AUTO && height !== undefined, padding = this.padding(), maxHeightPx = height - padding * 2, wrap = this.wrap(), shouldWrap = wrap !== NONE;
|
||||
return (!shouldWrap ||
|
||||
(fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx));
|
||||
}
|
||||
_tryToAddEllipsisToLastLine() {
|
||||
const width = this.attrs.width, fixedWidth = width !== AUTO && width !== undefined, padding = this.padding(), maxWidth = width - padding * 2, shouldAddEllipsis = this.ellipsis();
|
||||
const lastLine = this.textArr[this.textArr.length - 1];
|
||||
if (!lastLine || !shouldAddEllipsis) {
|
||||
return;
|
||||
}
|
||||
let text = lastLine.text + ELLIPSIS;
|
||||
if (fixedWidth) {
|
||||
const graphemes = stringToArray(lastLine.text);
|
||||
while (graphemes.length && this._getTextWidth(text) > maxWidth) {
|
||||
graphemes.pop();
|
||||
text = graphemes.join('') + ELLIPSIS;
|
||||
}
|
||||
if (this._getTextWidth(text) > maxWidth)
|
||||
text = '';
|
||||
}
|
||||
lastLine.text = text;
|
||||
lastLine.width = this._getTextWidth(text);
|
||||
}
|
||||
// for text we can't disable stroke scaling
|
||||
// if we do, the result will be unexpected
|
||||
getStrokeScaleEnabled() {
|
||||
return true;
|
||||
}
|
||||
_getStrokePadding() {
|
||||
// _strokeFunc uses a miter limit of two for glyph outlines.
|
||||
return super._getStrokePadding(2);
|
||||
}
|
||||
_useBufferCanvas(forceFill, opacity = this.getAbsoluteOpacity()) {
|
||||
if (this.attrs.perfectDrawEnabled === false)
|
||||
return false;
|
||||
const hasLine = this.textDecoration().indexOf('underline') !== -1 ||
|
||||
this.textDecoration().indexOf('line-through') !== -1;
|
||||
const hasShadow = this.hasShadow();
|
||||
if (hasLine && hasShadow) {
|
||||
return true;
|
||||
}
|
||||
if (hasShadow && opacity !== 1 && hasShadowOpacityBug()) {
|
||||
return true;
|
||||
}
|
||||
return super._useBufferCanvas(forceFill, opacity);
|
||||
}
|
||||
}
|
||||
Text.prototype._fillFunc = _fillFunc;
|
||||
Text.prototype._strokeFunc = _strokeFunc;
|
||||
Text.prototype.className = TEXT_UPPER;
|
||||
// the decoration attrs are not in ATTR_CHANGE_LIST: they change the bounds
|
||||
// through getSelfRect, but need no relayout
|
||||
Text.prototype._attrsAffectingSize = ATTR_CHANGE_LIST.filter((attr) => attr !== 'width' && attr !== 'height').concat(['textDecoration', 'underlineOffset']);
|
||||
_registerNode(Text, true);
|
||||
// update text data for certain attr changes
|
||||
Text.prototype.on(ATTR_CHANGE_LIST.map((attr) => attr + CHANGE_KONVA).join(' '), function () {
|
||||
this._setTextData();
|
||||
});
|
||||
/**
|
||||
* get/set width of text area, which includes padding.
|
||||
* @name Konva.Text#width
|
||||
* @method
|
||||
* @param {Number} width
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get width
|
||||
* var width = text.width();
|
||||
*
|
||||
* // set width
|
||||
* text.width(20);
|
||||
*
|
||||
* // set to auto
|
||||
* text.width('auto');
|
||||
* text.width() // will return calculated width, and not "auto"
|
||||
*/
|
||||
Factory.overWriteSetter(Text, 'width', getNumberOrAutoValidator());
|
||||
/**
|
||||
* get/set the height of the text area, which takes into account multi-line text, line heights, and padding.
|
||||
* @name Konva.Text#height
|
||||
* @method
|
||||
* @param {Number} height
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get height
|
||||
* var height = text.height();
|
||||
*
|
||||
* // set height
|
||||
* text.height(20);
|
||||
*
|
||||
* // set to auto
|
||||
* text.height('auto');
|
||||
* text.height() // will return calculated height, and not "auto"
|
||||
*/
|
||||
Factory.overWriteSetter(Text, 'height', getNumberOrAutoValidator());
|
||||
/**
|
||||
* get/set direction
|
||||
* @name Konva.Text#direction
|
||||
* @method
|
||||
* @param {String} direction
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get direction
|
||||
* var direction = text.direction();
|
||||
*
|
||||
* // set direction
|
||||
* text.direction('rtl');
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'direction', INHERIT);
|
||||
/**
|
||||
* get/set font family
|
||||
* @name Konva.Text#fontFamily
|
||||
* @method
|
||||
* @param {String} fontFamily
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get font family
|
||||
* var fontFamily = text.fontFamily();
|
||||
*
|
||||
* // set font family
|
||||
* text.fontFamily('Arial');
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'fontFamily', 'Arial');
|
||||
/**
|
||||
* get/set font size in pixels
|
||||
* @name Konva.Text#fontSize
|
||||
* @method
|
||||
* @param {Number} fontSize
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get font size
|
||||
* var fontSize = text.fontSize();
|
||||
*
|
||||
* // set font size to 22px
|
||||
* text.fontSize(22);
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'fontSize', 12, getNumberValidator());
|
||||
/**
|
||||
* get/set font style. Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default.
|
||||
* @name Konva.Text#fontStyle
|
||||
* @method
|
||||
* @param {String} fontStyle
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get font style
|
||||
* var fontStyle = text.fontStyle();
|
||||
*
|
||||
* // set font style
|
||||
* text.fontStyle('bold');
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'fontStyle', NORMAL);
|
||||
/**
|
||||
* get/set font variant. Can be 'normal' or 'small-caps'. 'normal' is the default.
|
||||
* @name Konva.Text#fontVariant
|
||||
* @method
|
||||
* @param {String} fontVariant
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get font variant
|
||||
* var fontVariant = text.fontVariant();
|
||||
*
|
||||
* // set font variant
|
||||
* text.fontVariant('small-caps');
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'fontVariant', NORMAL);
|
||||
/**
|
||||
* get/set padding
|
||||
* @name Konva.Text#padding
|
||||
* @method
|
||||
* @param {Number} padding
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get padding
|
||||
* var padding = text.padding();
|
||||
*
|
||||
* // set padding to 10 pixels
|
||||
* text.padding(10);
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'padding', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set horizontal align of text. Can be 'left', 'center', 'right' or 'justify'
|
||||
* @name Konva.Text#align
|
||||
* @method
|
||||
* @param {String} align
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get text align
|
||||
* var align = text.align();
|
||||
*
|
||||
* // center text
|
||||
* text.align('center');
|
||||
*
|
||||
* // align text to right
|
||||
* text.align('right');
|
||||
*
|
||||
* // justify text
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'align', LEFT);
|
||||
/**
|
||||
* get/set vertical align of text. Can be 'top', 'middle', 'bottom'.
|
||||
* @name Konva.Text#verticalAlign
|
||||
* @method
|
||||
* @param {String} verticalAlign
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get text vertical align
|
||||
* var verticalAlign = text.verticalAlign();
|
||||
*
|
||||
* // center text
|
||||
* text.verticalAlign('middle');
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'verticalAlign', TOP);
|
||||
/**
|
||||
* get/set line height. The default is 1.
|
||||
* @name Konva.Text#lineHeight
|
||||
* @method
|
||||
* @param {Number} lineHeight
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get line height
|
||||
* var lineHeight = text.lineHeight();
|
||||
*
|
||||
* // set the line height
|
||||
* text.lineHeight(2);
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'lineHeight', 1, getNumberValidator());
|
||||
/**
|
||||
* get/set wrap. Can be "word", "char", or "none". Default is "word".
|
||||
* In "word" wrapping any word still can be wrapped if it can't be placed in the required width
|
||||
* without breaks.
|
||||
* @name Konva.Text#wrap
|
||||
* @method
|
||||
* @param {String} wrap
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get wrap
|
||||
* var wrap = text.wrap();
|
||||
*
|
||||
* // set wrap
|
||||
* text.wrap('word');
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'wrap', WORD);
|
||||
/**
|
||||
* get/set ellipsis. Can be true or false. Default is false. If ellipsis is true,
|
||||
* Konva cuts the text that does not fit and ends it with "…" (one character).
|
||||
* That needs a fixed height or wrap set to "none"
|
||||
* @name Konva.Text#ellipsis
|
||||
* @method
|
||||
* @param {Boolean} ellipsis
|
||||
* @returns {Boolean}
|
||||
* @example
|
||||
* // get ellipsis param, returns true or false
|
||||
* var ellipsis = text.ellipsis();
|
||||
*
|
||||
* // set ellipsis
|
||||
* text.ellipsis(true);
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'ellipsis', false, getBooleanValidator());
|
||||
/**
|
||||
* set letter spacing property. Default value is 0.
|
||||
* @name Konva.Text#letterSpacing
|
||||
* @method
|
||||
* @param {Number} letterSpacing
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'letterSpacing', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set text
|
||||
* @name Konva.Text#text
|
||||
* @method
|
||||
* @param {String} text
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get text
|
||||
* var text = text.text();
|
||||
*
|
||||
* // set text
|
||||
* text.text('Hello world!');
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'text', '', getStringValidator());
|
||||
/**
|
||||
* get/set text decoration of a text. Possible values are 'underline', 'line-through' or combination of these values separated by space
|
||||
* @name Konva.Text#textDecoration
|
||||
* @method
|
||||
* @param {String} textDecoration
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get text decoration
|
||||
* var textDecoration = text.textDecoration();
|
||||
*
|
||||
* // underline text
|
||||
* text.textDecoration('underline');
|
||||
*
|
||||
* // strike text
|
||||
* text.textDecoration('line-through');
|
||||
*
|
||||
* // underline and strike text
|
||||
* text.textDecoration('underline line-through');
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'textDecoration', '');
|
||||
/**
|
||||
* get/set text underline decoration offset. Offset for underline line. Default is calculated based on font size.
|
||||
* @name Konva.Text#underlineOffset
|
||||
* @method
|
||||
* @param {Number} underlineOffset
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get underline offset
|
||||
* var underlineOffset = text.underlineOffset();
|
||||
*
|
||||
* // set underline offset
|
||||
* text.underlineOffset(5);
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'underlineOffset', undefined, getNumberValidator());
|
||||
/**
|
||||
* get/set per-character render hook. The callback is invoked for each grapheme before drawing, except for RTL text.
|
||||
* It can mutate the provided context (e.g. translate, rotate, change styles) and should return void.
|
||||
* Note: per-character rendering may disable native kerning/ligatures.
|
||||
* @name Konva.Text#charRenderFunc
|
||||
* @method
|
||||
* @param {Function} charRenderFunc
|
||||
* @returns {Function}
|
||||
* @example
|
||||
* // apply small x-translation and a custom color to every second character
|
||||
* text.charRenderFunc(function(props) {
|
||||
* if (props.index % 2 === 1) {
|
||||
* props.context.translate(2, 0);
|
||||
* props.context.fillStyle = 'red';
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'charRenderFunc', undefined);
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import type { Context } from '../Context.ts';
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import { Text } from './Text.ts';
|
||||
import type { GetSet, PathSegment, Vector2d } from '../types.ts';
|
||||
export interface TextPathConfig extends ShapeConfig {
|
||||
text?: string;
|
||||
data?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontStyle?: string;
|
||||
fontVariant?: string;
|
||||
align?: string;
|
||||
letterSpacing?: number;
|
||||
textBaseline?: string;
|
||||
textDecoration?: string;
|
||||
direction?: string;
|
||||
kerningFunc?: (leftChar: string, rightChar: string) => number;
|
||||
}
|
||||
/**
|
||||
* Path constructor.
|
||||
* @author Jason Follas
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {String} [config.fontFamily] default is Arial
|
||||
* @param {Number} [config.fontSize] default is 12
|
||||
* @param {String} [config.fontStyle] Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default.
|
||||
* @param {String} [config.fontVariant] can be normal or small-caps. Default is normal
|
||||
* @param {String} [config.textBaseline] Can be 'top', 'bottom', 'middle', 'alphabetic', 'hanging'. Default is middle
|
||||
* @param {String} config.text
|
||||
* @param {String} config.data SVG data string
|
||||
* @param {Function} config.kerningFunc a getter for kerning values for the specified characters
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var kerningPairs = {
|
||||
* 'A': {
|
||||
* ' ': -0.05517578125,
|
||||
* 'T': -0.07421875,
|
||||
* 'V': -0.07421875
|
||||
* }
|
||||
* 'V': {
|
||||
* ',': -0.091796875,
|
||||
* ":": -0.037109375,
|
||||
* ";": -0.037109375,
|
||||
* "A": -0.07421875
|
||||
* }
|
||||
* }
|
||||
* var textpath = new Konva.TextPath({
|
||||
* x: 100,
|
||||
* y: 50,
|
||||
* fill: '#333',
|
||||
* fontSize: '24',
|
||||
* fontFamily: 'Arial',
|
||||
* text: 'All the world\'s a stage, and all the men and women merely players.',
|
||||
* data: 'M10,10 C0,0 10,150 100,100 S300,150 400,50',
|
||||
* kerningFunc(leftChar, rightChar) {
|
||||
* return kerningPairs.hasOwnProperty(leftChar) ? pairs[leftChar][rightChar] || 0 : 0
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
export declare class TextPath extends Shape<TextPathConfig> {
|
||||
dataArray: PathSegment[];
|
||||
glyphInfo: Array<{
|
||||
text: string;
|
||||
rotation: number;
|
||||
p0: Vector2d;
|
||||
p1: Vector2d;
|
||||
width: number;
|
||||
}>;
|
||||
partialText: string;
|
||||
pathLength: number;
|
||||
textWidth: number;
|
||||
textHeight: number;
|
||||
constructor(config?: TextPathConfig);
|
||||
_getTextPathLength(): number;
|
||||
_getPointAtLength(length: number, cursor?: {
|
||||
index: number;
|
||||
offset: number;
|
||||
}): {
|
||||
x: number;
|
||||
y: number;
|
||||
} | null;
|
||||
_readDataAttribute(): void;
|
||||
_sceneFunc(context: Context): void;
|
||||
_hitFunc(context: Context): void;
|
||||
/**
|
||||
* get text width in pixels
|
||||
* @method
|
||||
* @name Konva.TextPath#getTextWidth
|
||||
*/
|
||||
getTextWidth(): number;
|
||||
getTextHeight(): number;
|
||||
setText(text: string): Text;
|
||||
_getContextFont(): string;
|
||||
_getTextSize(text: string): {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
_setTextData(): null | undefined;
|
||||
getSelfRect(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
getStrokeScaleEnabled(): boolean;
|
||||
_getSelfRectForDrawing(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
fontFamily: GetSet<string, this>;
|
||||
fontSize: GetSet<number, this>;
|
||||
fontStyle: GetSet<string, this>;
|
||||
fontVariant: GetSet<string, this>;
|
||||
align: GetSet<string, this>;
|
||||
letterSpacing: GetSet<number, this>;
|
||||
text: GetSet<string, this>;
|
||||
data: GetSet<string, this>;
|
||||
kerningFunc: GetSet<(leftChar: string, rightChar: string) => number, this>;
|
||||
textBaseline: GetSet<string, this>;
|
||||
textDecoration: GetSet<string, this>;
|
||||
direction: GetSet<string, this>;
|
||||
}
|
||||
+550
@@ -0,0 +1,550 @@
|
||||
import { Util } from "../Util.js";
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { Path } from "./Path.js";
|
||||
import { Text, getDecorationLineWidth, getDummyContext, stringToArray, } from "./Text.js";
|
||||
import { getNumberValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
const EMPTY_STRING = '', NORMAL = 'normal';
|
||||
function _fillFunc(context) {
|
||||
context.fillText(this.partialText, 0, 0);
|
||||
}
|
||||
function _strokeFunc(context) {
|
||||
context.strokeText(this.partialText, 0, 0);
|
||||
}
|
||||
/**
|
||||
* Path constructor.
|
||||
* @author Jason Follas
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {String} [config.fontFamily] default is Arial
|
||||
* @param {Number} [config.fontSize] default is 12
|
||||
* @param {String} [config.fontStyle] Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default.
|
||||
* @param {String} [config.fontVariant] can be normal or small-caps. Default is normal
|
||||
* @param {String} [config.textBaseline] Can be 'top', 'bottom', 'middle', 'alphabetic', 'hanging'. Default is middle
|
||||
* @param {String} config.text
|
||||
* @param {String} config.data SVG data string
|
||||
* @param {Function} config.kerningFunc a getter for kerning values for the specified characters
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* var kerningPairs = {
|
||||
* 'A': {
|
||||
* ' ': -0.05517578125,
|
||||
* 'T': -0.07421875,
|
||||
* 'V': -0.07421875
|
||||
* }
|
||||
* 'V': {
|
||||
* ',': -0.091796875,
|
||||
* ":": -0.037109375,
|
||||
* ";": -0.037109375,
|
||||
* "A": -0.07421875
|
||||
* }
|
||||
* }
|
||||
* var textpath = new Konva.TextPath({
|
||||
* x: 100,
|
||||
* y: 50,
|
||||
* fill: '#333',
|
||||
* fontSize: '24',
|
||||
* fontFamily: 'Arial',
|
||||
* text: 'All the world\'s a stage, and all the men and women merely players.',
|
||||
* data: 'M10,10 C0,0 10,150 100,100 S300,150 400,50',
|
||||
* kerningFunc(leftChar, rightChar) {
|
||||
* return kerningPairs.hasOwnProperty(leftChar) ? pairs[leftChar][rightChar] || 0 : 0
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
export class TextPath extends Shape {
|
||||
constructor(config) {
|
||||
// call super constructor
|
||||
super(config);
|
||||
this.dataArray = [];
|
||||
this._readDataAttribute();
|
||||
this._setTextData();
|
||||
}
|
||||
_getTextPathLength() {
|
||||
return Path.getPathLength(this.dataArray);
|
||||
}
|
||||
_getPointAtLength(length, cursor) {
|
||||
// if path is not defined yet, do nothing
|
||||
if (!this.attrs.data) {
|
||||
return null;
|
||||
}
|
||||
const totalLength = this.pathLength;
|
||||
if (length > totalLength) {
|
||||
return null;
|
||||
}
|
||||
return Path.getPointAtLengthOfDataArray(length, this.dataArray, cursor);
|
||||
}
|
||||
_readDataAttribute() {
|
||||
this.dataArray = Path.parsePathData(this.attrs.data);
|
||||
this.pathLength = this._getTextPathLength();
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
context.setAttr('font', this._getContextFont());
|
||||
context.setAttr('textBaseline', this.textBaseline());
|
||||
context.setAttr('textAlign', 'left');
|
||||
context.save();
|
||||
const textDecoration = this.textDecoration();
|
||||
const fill = this.fill();
|
||||
const fontSize = this.fontSize();
|
||||
const glyphInfo = this.glyphInfo;
|
||||
const hasUnderline = textDecoration.indexOf('underline') !== -1;
|
||||
const hasLineThrough = textDecoration.indexOf('line-through') !== -1;
|
||||
if (hasUnderline) {
|
||||
context.beginPath();
|
||||
}
|
||||
for (let i = 0; i < glyphInfo.length; i++) {
|
||||
context.save();
|
||||
const p0 = glyphInfo[i].p0;
|
||||
context.translate(p0.x, p0.y);
|
||||
context.rotate(glyphInfo[i].rotation);
|
||||
this.partialText = glyphInfo[i].text;
|
||||
context.fillStrokeShape(this);
|
||||
if (hasUnderline) {
|
||||
if (i === 0) {
|
||||
context.moveTo(0, fontSize / 2 + 1);
|
||||
}
|
||||
context.lineTo(glyphInfo[i].width, fontSize / 2 + 1);
|
||||
}
|
||||
context.restore();
|
||||
//// To assist with debugging visually, uncomment following
|
||||
//
|
||||
// if (i % 2) context.strokeStyle = 'cyan';
|
||||
// else context.strokeStyle = 'green';
|
||||
// var p1 = glyphInfo[i].p1;
|
||||
// context.moveTo(p0.x, p0.y);
|
||||
// context.lineTo(p1.x, p1.y);
|
||||
// context.stroke();
|
||||
}
|
||||
if (hasUnderline) {
|
||||
context.strokeStyle = fill;
|
||||
context.lineWidth = getDecorationLineWidth(fontSize);
|
||||
context.stroke();
|
||||
}
|
||||
if (hasLineThrough) {
|
||||
context.beginPath();
|
||||
for (let i = 0; i < glyphInfo.length; i++) {
|
||||
context.save();
|
||||
const p0 = glyphInfo[i].p0;
|
||||
context.translate(p0.x, p0.y);
|
||||
context.rotate(glyphInfo[i].rotation);
|
||||
if (i === 0) {
|
||||
context.moveTo(0, 0);
|
||||
}
|
||||
context.lineTo(glyphInfo[i].width, 0);
|
||||
context.restore();
|
||||
}
|
||||
context.strokeStyle = fill;
|
||||
context.lineWidth = getDecorationLineWidth(fontSize);
|
||||
context.stroke();
|
||||
}
|
||||
context.restore();
|
||||
}
|
||||
_hitFunc(context) {
|
||||
context.beginPath();
|
||||
const glyphInfo = this.glyphInfo;
|
||||
if (glyphInfo.length >= 1) {
|
||||
const p0 = glyphInfo[0].p0;
|
||||
context.moveTo(p0.x, p0.y);
|
||||
}
|
||||
for (let i = 0; i < glyphInfo.length; i++) {
|
||||
const p1 = glyphInfo[i].p1;
|
||||
context.lineTo(p1.x, p1.y);
|
||||
}
|
||||
context.setAttr('lineWidth', this.fontSize());
|
||||
context.setAttr('strokeStyle', this.colorKey);
|
||||
context.stroke();
|
||||
}
|
||||
/**
|
||||
* get text width in pixels
|
||||
* @method
|
||||
* @name Konva.TextPath#getTextWidth
|
||||
*/
|
||||
getTextWidth() {
|
||||
return this.textWidth;
|
||||
}
|
||||
getTextHeight() {
|
||||
Util.warn('text.getTextHeight() method is deprecated. Use text.height() - for full height and text.fontSize() - for one line height.');
|
||||
return this.textHeight;
|
||||
}
|
||||
setText(text) {
|
||||
return Text.prototype.setText.call(this, text);
|
||||
}
|
||||
_getContextFont() {
|
||||
return Text.prototype._getContextFont.call(this);
|
||||
}
|
||||
_getTextSize(text) {
|
||||
const _context = getDummyContext();
|
||||
_context.save();
|
||||
_context.font = this._getContextFont();
|
||||
// Text leaves its own kerning mode on the shared dummy context
|
||||
_context.fontKerning = 'auto';
|
||||
const metrics = _context.measureText(text);
|
||||
_context.restore();
|
||||
return {
|
||||
width: metrics.width,
|
||||
height: parseInt(`${this.fontSize()}`, 10),
|
||||
};
|
||||
}
|
||||
_setTextData() {
|
||||
const charArr = stringToArray(this.text());
|
||||
// For RTL scripts (Hebrew, Arabic, …) the path is still walked left to
|
||||
// right by glyph index, so we lay glyphs out in reversed logical order.
|
||||
// A reader scanning the path right-to-left then sees the text in its
|
||||
// intended reading order. This does not perform full Unicode bidi or
|
||||
// Arabic cursive shaping — those need a real shaping engine — but it
|
||||
// makes pure RTL strings render in the right direction.
|
||||
if (this.direction() === 'rtl') {
|
||||
charArr.reverse();
|
||||
}
|
||||
const kerningFunc = this.kerningFunc();
|
||||
const chars = [];
|
||||
let width = 0;
|
||||
for (let i = 0; i < charArr.length; i++) {
|
||||
let kern = 0;
|
||||
if (kerningFunc && i > 0) {
|
||||
try {
|
||||
// kerningFunc is a user provided getter. Make sure it never breaks our logic
|
||||
kern = kerningFunc(charArr[i - 1], charArr[i]) * this.fontSize();
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
chars.push({
|
||||
char: charArr[i],
|
||||
width: this._getTextSize(charArr[i]).width,
|
||||
kern: kern,
|
||||
});
|
||||
width += chars[i].width + kern;
|
||||
}
|
||||
const { width: fullTextWidth, height } = this._getTextSize(this.attrs.text);
|
||||
this.textWidth = width;
|
||||
this.textHeight = height;
|
||||
this.glyphInfo = [];
|
||||
if (!this.attrs.data) {
|
||||
return null;
|
||||
}
|
||||
const letterSpacing = this.letterSpacing();
|
||||
const align = this.align();
|
||||
const numberOfSpaces = align === 'justify' ? this.text().split(' ').length - 1 : 0;
|
||||
// The sum of individual character widths can exceed the whole-string width
|
||||
// due to kerning (browsers place adjacent glyphs closer together than the
|
||||
// individual measurements suggest). This difference is the maximum amount
|
||||
// by which the accumulated glyph offset can legitimately overshoot the
|
||||
// path length when the path is sized to match the visually-measured text.
|
||||
const kerningAdjustment = Math.max(0, width - fullTextWidth);
|
||||
// defines the width of the text on a straight line
|
||||
const textWidth = Math.max(this.textWidth + (charArr.length - 1) * letterSpacing, 0);
|
||||
let offset = 0;
|
||||
if (align === 'center') {
|
||||
offset = Math.max(0, this.pathLength / 2 - textWidth / 2);
|
||||
}
|
||||
if (align === 'right') {
|
||||
offset = Math.max(0, this.pathLength - textWidth);
|
||||
}
|
||||
// Algorithm for calculating glyph positions:
|
||||
// 1. Get the begging point of the glyph on the path using the offsetToGlyph,
|
||||
// 2. Get the ending point of the glyph on the path using the offsetToGlyph plus glyph width,
|
||||
// 3. Calculate the rotation, width, and midpoint of the glyph using the start and end points,
|
||||
// 4. Add glyph width to the offsetToGlyph and repeat
|
||||
let offsetToGlyph = offset;
|
||||
// Keep the original boundary selection across gaps between subpaths.
|
||||
const cursor = this.dataArray.some((segment, index) => index > 0 && segment.command === 'M')
|
||||
? undefined
|
||||
: { index: 0, offset: 0 };
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
offsetToGlyph += chars[i].kern;
|
||||
const charStartPoint = this._getPointAtLength(offsetToGlyph, cursor);
|
||||
if (!charStartPoint)
|
||||
return;
|
||||
const char = chars[i].char;
|
||||
let glyphWidth = chars[i].width + (i < chars.length - 1 ? letterSpacing : 0);
|
||||
if (char === ' ' && align === 'justify') {
|
||||
glyphWidth += (this.pathLength - textWidth) / numberOfSpaces;
|
||||
}
|
||||
const charEndLength = offsetToGlyph + glyphWidth;
|
||||
// When the path length is sized to match the visually-measured (kerned)
|
||||
// text width, the accumulated per-glyph offset can exceed pathLength by
|
||||
// up to kerningAdjustment pixels. Clamp to pathLength in that case so
|
||||
// the last character is not silently dropped.
|
||||
const charEndPoint = this._getPointAtLength(charEndLength > this.pathLength &&
|
||||
charEndLength - this.pathLength <= kerningAdjustment
|
||||
? this.pathLength
|
||||
: charEndLength, cursor);
|
||||
if (!charEndPoint) {
|
||||
return;
|
||||
}
|
||||
const width = Path.getLineLength(charStartPoint.x, charStartPoint.y, charEndPoint.x, charEndPoint.y);
|
||||
const rotation = Math.atan2(charEndPoint.y - charStartPoint.y, charEndPoint.x - charStartPoint.x);
|
||||
this.glyphInfo.push({
|
||||
text: charArr[i],
|
||||
rotation: rotation,
|
||||
p0: charStartPoint,
|
||||
p1: charEndPoint,
|
||||
width: width,
|
||||
});
|
||||
offsetToGlyph += glyphWidth;
|
||||
}
|
||||
}
|
||||
getSelfRect() {
|
||||
if (!this.glyphInfo.length) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
const points = [];
|
||||
this.glyphInfo.forEach(function (info) {
|
||||
points.push(info.p0.x);
|
||||
points.push(info.p0.y);
|
||||
points.push(info.p1.x);
|
||||
points.push(info.p1.y);
|
||||
});
|
||||
const rect = Util._getPointsRect(points);
|
||||
const fontSize = this.fontSize();
|
||||
return {
|
||||
x: rect.x - fontSize / 2,
|
||||
y: rect.y - fontSize / 2,
|
||||
width: rect.width + fontSize,
|
||||
height: rect.height + fontSize,
|
||||
};
|
||||
}
|
||||
// Like Text: a glyph stroke is drawn with the text, so it always scales.
|
||||
getStrokeScaleEnabled() {
|
||||
return true;
|
||||
}
|
||||
_getSelfRectForDrawing() {
|
||||
// getSelfRect() pads glyph anchors by half the font size; ascenders,
|
||||
// accents and other baselines reach further from the path.
|
||||
const rect = this.getSelfRect(), pad = this.fontSize();
|
||||
return {
|
||||
x: rect.x - pad,
|
||||
y: rect.y - pad,
|
||||
width: rect.width + pad * 2,
|
||||
height: rect.height + pad * 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
TextPath.prototype._fillFunc = _fillFunc;
|
||||
TextPath.prototype._strokeFunc = _strokeFunc;
|
||||
TextPath.prototype._fillFuncHit = _fillFunc;
|
||||
TextPath.prototype._strokeFuncHit = _strokeFunc;
|
||||
TextPath.prototype.className = 'TextPath';
|
||||
TextPath.prototype._attrsAffectingSize = [
|
||||
'text',
|
||||
'fontSize',
|
||||
'data',
|
||||
'align',
|
||||
'letterSpacing',
|
||||
'kerningFunc',
|
||||
'fontFamily',
|
||||
'fontStyle',
|
||||
'fontVariant',
|
||||
'direction',
|
||||
];
|
||||
_registerNode(TextPath, true);
|
||||
TextPath.prototype.on('dataChange.konva', function () {
|
||||
this._readDataAttribute();
|
||||
this._setTextData();
|
||||
});
|
||||
// update text data for certain attr changes
|
||||
TextPath.prototype.on(TextPath.prototype._attrsAffectingSize
|
||||
.filter((attr) => attr !== 'data')
|
||||
.map((attr) => attr + 'Change.konva')
|
||||
.join(' '), function () {
|
||||
this._setTextData();
|
||||
});
|
||||
/**
|
||||
* get/set SVG path data string. This method
|
||||
* also automatically parses the data string
|
||||
* into a data array. Currently supported SVG data:
|
||||
* M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, A, a, Z, z
|
||||
* @name Konva.TextPath#data
|
||||
* @method
|
||||
* @param {String} data svg path string
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get data
|
||||
* var data = shape.data();
|
||||
*
|
||||
* // set data
|
||||
* shape.data('M200,100h100v50z');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'data');
|
||||
/**
|
||||
* get/set font family
|
||||
* @name Konva.TextPath#fontFamily
|
||||
* @method
|
||||
* @param {String} fontFamily
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get font family
|
||||
* var fontFamily = shape.fontFamily();
|
||||
*
|
||||
* // set font family
|
||||
* shape.fontFamily('Arial');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'fontFamily', 'Arial');
|
||||
/**
|
||||
* get/set font size in pixels
|
||||
* @name Konva.TextPath#fontSize
|
||||
* @method
|
||||
* @param {Number} fontSize
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get font size
|
||||
* var fontSize = shape.fontSize();
|
||||
*
|
||||
* // set font size to 22px
|
||||
* shape.fontSize(22);
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'fontSize', 12, getNumberValidator());
|
||||
/**
|
||||
* get/set font style. Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default.
|
||||
* @name Konva.TextPath#fontStyle
|
||||
* @method
|
||||
* @param {String} fontStyle
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get font style
|
||||
* var fontStyle = shape.fontStyle();
|
||||
*
|
||||
* // set font style
|
||||
* shape.fontStyle('bold');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'fontStyle', NORMAL);
|
||||
/**
|
||||
* get/set horizontal align of text. Can be 'left', 'center', 'right' or 'justify'
|
||||
* @name Konva.TextPath#align
|
||||
* @method
|
||||
* @param {String} align
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get text align
|
||||
* var align = text.align();
|
||||
*
|
||||
* // center text
|
||||
* text.align('center');
|
||||
*
|
||||
* // align text to right
|
||||
* text.align('right');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'align', 'left');
|
||||
/**
|
||||
* get/set letter spacing. The default is 0.
|
||||
* @name Konva.TextPath#letterSpacing
|
||||
* @method
|
||||
* @param {Number} letterSpacing
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get letter spacing value
|
||||
* var letterSpacing = shape.letterSpacing();
|
||||
*
|
||||
* // set the letter spacing value
|
||||
* shape.letterSpacing(2);
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'letterSpacing', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set text baseline. The default is 'middle'. Can be 'top', 'bottom', 'middle', 'alphabetic', 'hanging'
|
||||
* @name Konva.TextPath#textBaseline
|
||||
* @method
|
||||
* @param {String} textBaseline
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get current text baseline
|
||||
* var textBaseline = shape.textBaseline();
|
||||
*
|
||||
* // set new text baseline
|
||||
* shape.textBaseline('top');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'textBaseline', 'middle');
|
||||
/**
|
||||
* get/set font variant. Can be 'normal' or 'small-caps'. 'normal' is the default.
|
||||
* @name Konva.TextPath#fontVariant
|
||||
* @method
|
||||
* @param {String} fontVariant
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get font variant
|
||||
* var fontVariant = shape.fontVariant();
|
||||
*
|
||||
* // set font variant
|
||||
* shape.fontVariant('small-caps');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'fontVariant', NORMAL);
|
||||
/**
|
||||
* get/set text
|
||||
* @name Konva.TextPath#getText
|
||||
* @method
|
||||
* @param {String} text
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get text
|
||||
* var text = text.text();
|
||||
*
|
||||
* // set text
|
||||
* text.text('Hello world!');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'text', EMPTY_STRING);
|
||||
/**
|
||||
* get/set text decoration of a text. Can be '', 'underline', 'line-through', or a combination like 'underline line-through'.
|
||||
* @name Konva.TextPath#textDecoration
|
||||
* @method
|
||||
* @param {String} textDecoration
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get text decoration
|
||||
* var textDecoration = shape.textDecoration();
|
||||
*
|
||||
* // underline text
|
||||
* shape.textDecoration('underline');
|
||||
*
|
||||
* // line-through text
|
||||
* shape.textDecoration('line-through');
|
||||
*
|
||||
* // combine both
|
||||
* shape.textDecoration('underline line-through');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'textDecoration', '');
|
||||
/**
|
||||
* get/set kerning function.
|
||||
* @name Konva.TextPath#kerningFunc
|
||||
* @method
|
||||
* @param {String} kerningFunc
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get text decoration
|
||||
* var kerningFunc = text.kerningFunc();
|
||||
*
|
||||
* // center text
|
||||
* text.kerningFunc(function(leftChar, rightChar) {
|
||||
* return 1;
|
||||
* });
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'kerningFunc', undefined);
|
||||
/**
|
||||
* get/set text direction. Can be 'inherit', 'ltr' or 'rtl'. Default is 'inherit'.
|
||||
*
|
||||
* Setting `'rtl'` reverses the per-glyph layout order along the path, so
|
||||
* Hebrew/Arabic strings read correctly when scanned right-to-left. This is a
|
||||
* pragmatic fix limited by canvas TextPath rendering: each glyph is still
|
||||
* drawn independently, so Arabic cursive joining and full Unicode bidi
|
||||
* reordering of mixed-direction text are not supported. For best results,
|
||||
* pair `direction: 'rtl'` with `align: 'right'` so the text starts at the
|
||||
* end of the path.
|
||||
* @name Konva.TextPath#direction
|
||||
* @method
|
||||
* @param {String} direction
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get direction
|
||||
* var direction = textpath.direction();
|
||||
*
|
||||
* // set direction
|
||||
* textpath.direction('rtl');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'direction', 'inherit');
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
import { Transform } from '../Util.ts';
|
||||
import { Node } from '../Node.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import { Rect } from './Rect.ts';
|
||||
import { Group } from '../Group.ts';
|
||||
import { type ContainerConfig } from '../Container.ts';
|
||||
import type { GetSet, IRect, Vector2d } from '../types.ts';
|
||||
import type { HitCanvas, SceneCanvas } from '../Canvas.ts';
|
||||
export interface Box extends IRect {
|
||||
rotation: number;
|
||||
}
|
||||
export interface TransformerConfig extends ContainerConfig {
|
||||
nodes?: Array<Node>;
|
||||
resizeEnabled?: boolean;
|
||||
rotateEnabled?: boolean;
|
||||
rotateLineVisible?: boolean;
|
||||
rotationSnaps?: Array<number>;
|
||||
rotationSnapTolerance?: number;
|
||||
rotateAnchorOffset?: number;
|
||||
rotateAnchorAngle?: number;
|
||||
rotateAnchorCursor?: string;
|
||||
rotateMode?: 'handle' | 'corners' | 'both';
|
||||
borderEnabled?: boolean;
|
||||
borderStroke?: string;
|
||||
borderStrokeWidth?: number;
|
||||
borderDash?: Array<number>;
|
||||
anchorFill?: string;
|
||||
anchorStroke?: string;
|
||||
anchorStrokeWidth?: number;
|
||||
anchorSize?: number;
|
||||
anchorCornerRadius?: number;
|
||||
keepRatio?: boolean;
|
||||
shiftBehavior?: string;
|
||||
centeredScaling?: boolean;
|
||||
enabledAnchors?: Array<string>;
|
||||
flipEnabled?: boolean;
|
||||
ignoreStroke?: boolean;
|
||||
boundBoxFunc?: (oldBox: Box, newBox: Box) => Box;
|
||||
useSingleNodeRotation?: boolean;
|
||||
shouldOverdrawWholeArea?: boolean;
|
||||
anchorDragBoundFunc?: (oldPos: Vector2d, newPos: Vector2d, evt: any) => Vector2d;
|
||||
anchorStyleFunc?: (anchor: Rect) => void;
|
||||
}
|
||||
/**
|
||||
* Transformer constructor. Transformer is a special type of group that allow you transform Konva
|
||||
* primitives and shapes. Transforming tool is not changing `width` and `height` properties of nodes
|
||||
* when you resize them. Instead it changes `scaleX` and `scaleY` properties.
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @param {Object} config
|
||||
* @param {Boolean} [config.resizeEnabled] Default is true
|
||||
* @param {Boolean} [config.rotateEnabled] Default is true
|
||||
* @param {Boolean} [config.rotateLineVisible] Default is true
|
||||
* @param {Array} [config.rotationSnaps] Array of angles for rotation snaps. Default is []
|
||||
* @param {Number} [config.rotationSnapTolerance] Snapping tolerance. If closer than this it will snap. Default is 5
|
||||
* @param {Number} [config.rotateAnchorOffset] Default is 50
|
||||
* @param {Number} [config.rotateAnchorAngle] Angle of the rotate anchor around the box, in degrees. Default is 0 (top)
|
||||
* @param {String} [config.rotateAnchorCursor] Default is crosshair
|
||||
* @param {String} [config.rotateMode] How the user rotates: 'handle' (the rotate anchor), 'corners' (invisible zones just outside the corners) or 'both'. Default is 'handle'
|
||||
* @param {Number} [config.padding] Default is 0
|
||||
* @param {Boolean} [config.borderEnabled] Should we draw border? Default is true
|
||||
* @param {String} [config.borderStroke] Border stroke color
|
||||
* @param {Number} [config.borderStrokeWidth] Border stroke size
|
||||
* @param {Array} [config.borderDash] Array for border dash.
|
||||
* @param {String} [config.anchorFill] Anchor fill color
|
||||
* @param {String} [config.anchorStroke] Anchor stroke color
|
||||
* @param {Number} [config.anchorCornerRadius] Anchor corner radius
|
||||
* @param {Number} [config.anchorStrokeWidth] Anchor stroke size
|
||||
* @param {Number} [config.anchorSize] Default is 10
|
||||
* @param {Boolean} [config.keepRatio] Should we keep ratio when we are moving edges? Default is true
|
||||
* @param {String} [config.shiftBehavior] How does transformer react on shift key press when we are moving edges? 'none' also turns off the Shift rotation snap. Default is 'default'
|
||||
* @param {Boolean} [config.centeredScaling] Should we resize relative to node's center? Default is false
|
||||
* @param {Array} [config.enabledAnchors] Array of names of enabled handles
|
||||
* @param {Boolean} [config.flipEnabled] Can we flip/mirror shape on transform?. True by default
|
||||
* @param {Function} [config.boundBoxFunc] Bounding box function, see {@link Konva.Transformer#boundBoxFunc}
|
||||
* @param {Function} [config.anchorDragBoundFunc] Function bounding the position of a dragged anchor, see {@link Konva.Transformer#anchorDragBoundFunc}
|
||||
* @param {Function} [config.anchorStyleFunc] Function styling every anchor, see {@link Konva.Transformer#anchorStyleFunc}
|
||||
* @param {Boolean} [config.ignoreStroke] Should we ignore stroke size? Default is false
|
||||
* @param {Boolean} [config.useSingleNodeRotation] When just one node attached, should we use its rotation for transformer?
|
||||
* @param {Boolean} [config.shouldOverdrawWholeArea] Should we fill whole transformer area with fake transparent shape to enable dragging from empty spaces?
|
||||
* @example
|
||||
* var transformer = new Konva.Transformer({
|
||||
* nodes: [rectangle],
|
||||
* rotateAnchorOffset: 60,
|
||||
* enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right']
|
||||
* });
|
||||
* layer.add(transformer);
|
||||
*/
|
||||
export declare class Transformer extends Group {
|
||||
_nodes: Array<Node>;
|
||||
_nodeRectCache?: Map<Node, {
|
||||
rotation: number;
|
||||
ignoreStroke: boolean;
|
||||
version: number;
|
||||
width: number;
|
||||
height: number;
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}>;
|
||||
_movingAnchorName: string | null;
|
||||
_pointerId?: number;
|
||||
_anchors: Record<string, Rect>;
|
||||
_back: Shape;
|
||||
_transforming: boolean;
|
||||
_fitting: boolean;
|
||||
_transformWindow: Window | null;
|
||||
_anchorDragOffset: Vector2d;
|
||||
_rotateStart?: {
|
||||
pointerAngle: number;
|
||||
rotation: number;
|
||||
};
|
||||
sin: number;
|
||||
cos: number;
|
||||
_cursorChange: boolean;
|
||||
_elementsCreated: boolean;
|
||||
_lastNodeRect?: Readonly<Box>;
|
||||
_updatePending: boolean;
|
||||
static isTransforming: () => boolean;
|
||||
static _isLayerTransforming(layer: Node): boolean;
|
||||
constructor(config?: TransformerConfig);
|
||||
/**
|
||||
* alias to `tr.nodes([shape])`/ This method is deprecated and will be removed soon.
|
||||
* @method
|
||||
* @name Konva.Transformer#attachTo
|
||||
* @returns {Konva.Transformer}
|
||||
* @example
|
||||
* transformer.attachTo(shape);
|
||||
*/
|
||||
attachTo(node: Node): this;
|
||||
setNode(node: Node): this;
|
||||
getNode(): Node<import("../Node.ts").NodeConfig>;
|
||||
_getEventNamespace(): string;
|
||||
setNodes(nodes?: Array<Node>): this;
|
||||
_proxyDrag(node: Node): void;
|
||||
_detachNode(node: Node): void;
|
||||
_resetRotation(): void;
|
||||
_removeNode(node: Node): void;
|
||||
getNodes(): Node<import("../Node.ts").NodeConfig>[];
|
||||
/**
|
||||
* return the name of current active anchor
|
||||
* @method
|
||||
* @name Konva.Transformer#getActiveAnchor
|
||||
* @returns {String | Null}
|
||||
* @example
|
||||
* transformer.getActiveAnchor();
|
||||
*/
|
||||
getActiveAnchor(): string | null;
|
||||
/**
|
||||
* detach transformer from an attached node
|
||||
* @method
|
||||
* @name Konva.Transformer#detach
|
||||
* @returns {Konva.Transformer}
|
||||
* @example
|
||||
* transformer.detach();
|
||||
*/
|
||||
detach(): void;
|
||||
/**
|
||||
* bind events to the Transformer. You can use events: `transform`, `transformstart`, `transformend`, `dragstart`, `dragmove`, `dragend`
|
||||
* @method
|
||||
* @name Konva.Transformer#on
|
||||
* @param {String} evtStr e.g. 'transform'
|
||||
* @param {Function} handler The handler function. The first argument of that function is event object. Event object has `target` as main target of the event, `currentTarget` as current node listener and `evt` as native browser event.
|
||||
* @returns {Konva.Transformer}
|
||||
* @example
|
||||
* // add click listener
|
||||
* tr.on('transformstart', function() {
|
||||
* console.log('transform started');
|
||||
* });
|
||||
*/
|
||||
_resetTransformCache(changedNode?: Node): void;
|
||||
_onSubtreeChange(node: Node): void;
|
||||
_syncUpdate(): void;
|
||||
drawScene(can?: SceneCanvas, top?: Node): this;
|
||||
drawHit(can?: HitCanvas, top?: Node): this;
|
||||
_getNodeRect(): any;
|
||||
__getNodeRect(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
};
|
||||
getX(): any;
|
||||
getY(): any;
|
||||
getWidth(): any;
|
||||
getHeight(): any;
|
||||
_createElements(): void;
|
||||
_createAnchor(name: string, className?: string): Rect;
|
||||
_setAnchorCursor(name: string, rad?: number): void;
|
||||
_createBack(): void;
|
||||
_handleMouseDown(e: any): void;
|
||||
_handleMouseMove(e: any): void;
|
||||
_moveTransform(e: any): void;
|
||||
_handleMouseUp(e: any): void;
|
||||
getAbsoluteTransform(top?: Node | null): Transform;
|
||||
_removeEvents(e?: any): void;
|
||||
_fitNodesInto(newAttrs: any, evt?: any, anchorProjected?: boolean): void;
|
||||
_doFitNodesInto(newAttrs: any, evt?: any, anchorProjected?: boolean): void;
|
||||
_scheduleUpdate(): void;
|
||||
/**
|
||||
* force update of Konva.Transformer.
|
||||
* The transformer follows attribute changes of attached nodes and, for an attached group, any change of its descendants.
|
||||
* Use it after a change that sets no attribute, for example a `Line` points array mutated in place
|
||||
* or a custom `getSelfRect()` that depends on outside state.
|
||||
* @method
|
||||
* @name Konva.Transformer#forceUpdate
|
||||
*/
|
||||
forceUpdate(): void;
|
||||
update(): void;
|
||||
_update(): void;
|
||||
_updateElements(attrs: Box): void;
|
||||
/**
|
||||
* determine if transformer is in active transform
|
||||
* @method
|
||||
* @name Konva.Transformer#isTransforming
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isTransforming(): boolean;
|
||||
/**
|
||||
* Stop active transform action
|
||||
* @method
|
||||
* @name Konva.Transformer#stopTransform
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
stopTransform(): void;
|
||||
destroy(): this;
|
||||
add(...children: any[]): this;
|
||||
toObject(): {
|
||||
attrs: any;
|
||||
className: string;
|
||||
children?: Array<any>;
|
||||
};
|
||||
clone(obj?: any): this;
|
||||
getClientRect(config?: Parameters<Group['getClientRect']>[0]): IRect;
|
||||
nodes: GetSet<Node[], this>;
|
||||
enabledAnchors: GetSet<string[], this>;
|
||||
rotationSnaps: GetSet<number[], this>;
|
||||
anchorSize: GetSet<number, this>;
|
||||
resizeEnabled: GetSet<boolean, this>;
|
||||
rotateEnabled: GetSet<boolean, this>;
|
||||
rotateLineVisible: GetSet<boolean, this>;
|
||||
rotateAnchorOffset: GetSet<number, this>;
|
||||
rotateAnchorAngle: GetSet<number, this>;
|
||||
rotationSnapTolerance: GetSet<number, this>;
|
||||
rotateAnchorCursor: GetSet<string, this>;
|
||||
rotateMode: GetSet<'handle' | 'corners' | 'both', this>;
|
||||
padding: GetSet<number, this>;
|
||||
borderEnabled: GetSet<boolean, this>;
|
||||
borderStroke: GetSet<string, this>;
|
||||
borderStrokeWidth: GetSet<number, this>;
|
||||
borderDash: GetSet<number[], this>;
|
||||
anchorFill: GetSet<string, this>;
|
||||
anchorStroke: GetSet<string, this>;
|
||||
anchorCornerRadius: GetSet<number, this>;
|
||||
anchorStrokeWidth: GetSet<number, this>;
|
||||
keepRatio: GetSet<boolean, this>;
|
||||
shiftBehavior: GetSet<string, this>;
|
||||
centeredScaling: GetSet<boolean, this>;
|
||||
flipEnabled: GetSet<boolean, this>;
|
||||
ignoreStroke: GetSet<boolean, this>;
|
||||
boundBoxFunc: GetSet<(oldBox: Box, newBox: Box) => Box, this>;
|
||||
anchorDragBoundFunc: GetSet<(oldPos: Vector2d, newPos: Vector2d, evt: any) => Vector2d, this>;
|
||||
anchorStyleFunc: GetSet<null | ((Node: Rect) => void), this>;
|
||||
shouldOverdrawWholeArea: GetSet<boolean, this>;
|
||||
useSingleNodeRotation: GetSet<boolean, this>;
|
||||
}
|
||||
+1982
File diff suppressed because it is too large
Load Diff
+41
@@ -0,0 +1,41 @@
|
||||
import type { Context } from '../Context.ts';
|
||||
import type { ShapeConfig } from '../Shape.ts';
|
||||
import { Shape } from '../Shape.ts';
|
||||
import type { GetSet } from '../types.ts';
|
||||
export interface WedgeConfig extends ShapeConfig {
|
||||
angle: number;
|
||||
radius: number;
|
||||
clockwise?: boolean;
|
||||
}
|
||||
/**
|
||||
* Wedge constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} config.angle in degrees
|
||||
* @param {Number} config.radius
|
||||
* @param {Boolean} [config.clockwise]
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* // draw a wedge that's pointing downwards
|
||||
* var wedge = new Konva.Wedge({
|
||||
* radius: 40,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5,
|
||||
* angle: 60,
|
||||
* rotation: -120
|
||||
* });
|
||||
*/
|
||||
export declare class Wedge extends Shape<WedgeConfig> {
|
||||
_sceneFunc(context: Context): void;
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
setWidth(width: number): void;
|
||||
setHeight(height: number): void;
|
||||
radius: GetSet<number, this>;
|
||||
angle: GetSet<number, this>;
|
||||
clockwise: GetSet<boolean, this>;
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { Konva } from "../Global.js";
|
||||
import { getNumberValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
/**
|
||||
* Wedge constructor
|
||||
* @constructor
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} config.angle in degrees
|
||||
* @param {Number} config.radius
|
||||
* @param {Boolean} [config.clockwise]
|
||||
* @@shapeParams
|
||||
* @@nodeParams
|
||||
* @example
|
||||
* // draw a wedge that's pointing downwards
|
||||
* var wedge = new Konva.Wedge({
|
||||
* radius: 40,
|
||||
* fill: 'red',
|
||||
* stroke: 'black',
|
||||
* strokeWidth: 5,
|
||||
* angle: 60,
|
||||
* rotation: -120
|
||||
* });
|
||||
*/
|
||||
export class Wedge extends Shape {
|
||||
_sceneFunc(context) {
|
||||
context.beginPath();
|
||||
context.arc(0, 0, Math.abs(this.radius()), 0, Konva.getAngle(this.angle()), this.clockwise());
|
||||
context.lineTo(0, 0);
|
||||
context.closePath();
|
||||
context.fillStrokeShape(this);
|
||||
}
|
||||
getWidth() {
|
||||
return Math.abs(this.radius()) * 2;
|
||||
}
|
||||
getHeight() {
|
||||
return Math.abs(this.radius()) * 2;
|
||||
}
|
||||
setWidth(width) {
|
||||
this.radius(width / 2);
|
||||
}
|
||||
setHeight(height) {
|
||||
this.radius(height / 2);
|
||||
}
|
||||
}
|
||||
Wedge.prototype.className = 'Wedge';
|
||||
Wedge.prototype._centroid = true;
|
||||
Wedge.prototype._attrsAffectingSize = ['radius'];
|
||||
_registerNode(Wedge, true);
|
||||
/**
|
||||
* get/set radius
|
||||
* @name Konva.Wedge#radius
|
||||
* @method
|
||||
* @param {Number} radius
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get radius
|
||||
* var radius = wedge.radius();
|
||||
*
|
||||
* // set radius
|
||||
* wedge.radius(10);
|
||||
*/
|
||||
Factory.addGetterSetter(Wedge, 'radius', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set angle in degrees
|
||||
* @name Konva.Wedge#angle
|
||||
* @method
|
||||
* @param {Number} angle
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get angle
|
||||
* var angle = wedge.angle();
|
||||
*
|
||||
* // set angle
|
||||
* wedge.angle(20);
|
||||
*/
|
||||
Factory.addGetterSetter(Wedge, 'angle', 0, getNumberValidator());
|
||||
/**
|
||||
* get/set clockwise flag
|
||||
* @name Konva.Wedge#clockwise
|
||||
* @method
|
||||
* @param {Boolean} clockwise
|
||||
* @returns {Boolean}
|
||||
* @example
|
||||
* // get clockwise flag
|
||||
* var clockwise = wedge.clockwise();
|
||||
*
|
||||
* // draw wedge counter-clockwise
|
||||
* wedge.clockwise(false);
|
||||
*
|
||||
* // draw wedge clockwise
|
||||
* wedge.clockwise(true);
|
||||
*/
|
||||
Factory.addGetterSetter(Wedge, 'clockwise', false);
|
||||
Factory.backCompat(Wedge, {
|
||||
angleDeg: 'angle',
|
||||
getAngleDeg: 'getAngle',
|
||||
setAngleDeg: 'setAngle',
|
||||
});
|
||||
Reference in New Issue
Block a user