first commit

This commit is contained in:
2026-10-17 12:09:36 -04:00
commit 56046a63fc
1149 changed files with 658055 additions and 0 deletions
+67
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.&nbsp; 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.&nbsp; 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
View File
@@ -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.&nbsp; 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.&nbsp; 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
View File
@@ -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.&nbsp; Lines are defined by an array of points and
* a tension
* @constructor
* @memberof Konva
* @augments Konva.Shape
* @param {Object} config
* @param {Array} config.points Flat array of points coordinates. You should define them as [x1, y1, x2, y2, x3, y3].
* @param {Number} [config.tension] Higher values will result in a more curvy line. A value of 0 will result in no interpolation.
* The default is 0
* @param {Boolean} [config.closed] defines whether or not the line shape is closed, creating a polygon or blob
* @param {Boolean} [config.bezier] if no tension is provided but bezier=true, we draw the line as a bezier using the passed points
* @@shapeParams
* @@nodeParams
* @example
* var line = new Konva.Line({
* x: 100,
* y: 50,
* points: [73, 70, 340, 23, 450, 60, 500, 20],
* stroke: 'red',
* tension: 1
* });
*/
export 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
View File
@@ -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.&nbsp; Lines are defined by an array of points and
* a tension
* @constructor
* @memberof Konva
* @augments Konva.Shape
* @param {Object} config
* @param {Array} config.points Flat array of points coordinates. You should define them as [x1, y1, x2, y2, x3, y3].
* @param {Number} [config.tension] Higher values will result in a more curvy line. A value of 0 will result in no interpolation.
* The default is 0
* @param {Boolean} [config.closed] defines whether or not the line shape is closed, creating a polygon or blob
* @param {Boolean} [config.bezier] if no tension is provided but bezier=true, we draw the line as a bezier using the passed points
* @@shapeParams
* @@nodeParams
* @example
* var line = new Konva.Line({
* x: 100,
* y: 50,
* points: [73, 70, 340, 23, 450, 60, 500, 20],
* stroke: 'red',
* tension: 1
* });
*/
export class Line extends Shape {
constructor(config) {
super(config);
this.on('pointsChange.konva tensionChange.konva closedChange.konva bezierChange.konva', function () {
this._clearCache('tensionPoints');
});
}
_hasTension() {
return this.tension() !== 0 && this.points().length > 4;
}
/**
* Report every curve segment of a line with a tension, in draw order. Both
* the scene function and the bounding rect read the shape through this, so
* that they can not disagree about which curve the line is.
*
* The handlers take plain numbers, because this runs on every frame.
*/
_eachTensionSegment(onQuadratic, onCubic) {
const points = this.points(), length = points.length, closed = this.closed(), tp = this.getTensionPoints(), len = tp.length;
let x0 = points[0], y0 = points[1],
// a closed line has a curve on both sides of the first point, so it
// starts on a full cubic. An open one is capped by a quadratic instead
n = closed ? 0 : 4;
if (!closed) {
onQuadratic(x0, y0, tp[0], tp[1], tp[2], tp[3]);
x0 = tp[2];
y0 = tp[3];
}
while (n < len - 2) {
const cp1x = tp[n++], cp1y = tp[n++], cp2x = tp[n++], cp2y = tp[n++], x = tp[n++], y = tp[n++];
onCubic(x0, y0, cp1x, cp1y, cp2x, cp2y, x, y);
x0 = x;
y0 = y;
}
if (!closed) {
onQuadratic(x0, y0, tp[len - 2], tp[len - 1], points[length - 2], points[length - 1]);
}
}
_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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -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
View File
@@ -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',
});