mirror of
https://github.com/konvajs/konva.git
synced 2025-08-20 04:51:04 +08:00
Compare commits
14 Commits
bca9ea0779
...
7dcaf96742
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7dcaf96742 | ||
![]() |
7b9ccd18ba | ||
![]() |
553245c074 | ||
![]() |
e33341d3f6 | ||
![]() |
8f22d97937 | ||
![]() |
d2ecf2064e | ||
![]() |
c398aedb0a | ||
![]() |
33e5ddf4ae | ||
![]() |
18956ae62b | ||
![]() |
eb70aaf7d2 | ||
![]() |
1adf506a93 | ||
![]() |
089766c7ae | ||
![]() |
5155a240a3 | ||
![]() |
65c06c6fbc |
@ -1,3 +0,0 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(...tseslint.configs.recommended);
|
@ -1,77 +0,0 @@
|
||||
import fs from 'fs';
|
||||
|
||||
// relative path here
|
||||
// but you will need just require('konva-node');
|
||||
import Konva from '../';
|
||||
|
||||
// Create stage. Container parameter is not required in NodeJS.
|
||||
var stage = new Konva.Stage({
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
|
||||
var layer = new Konva.Layer();
|
||||
stage.add(layer);
|
||||
|
||||
var rect = new Konva.Rect({
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 50,
|
||||
y: 50,
|
||||
fill: 'white',
|
||||
});
|
||||
var text = new Konva.Text({
|
||||
text: 'Generated inside node js',
|
||||
x: 20,
|
||||
y: 20,
|
||||
fill: 'black',
|
||||
});
|
||||
layer.add(rect).add(text);
|
||||
layer.draw();
|
||||
stage.setSize({
|
||||
width: 200,
|
||||
height: 200,
|
||||
});
|
||||
|
||||
// check tween works
|
||||
var tween = new Konva.Tween({
|
||||
node: rect,
|
||||
duration: 1,
|
||||
x: -50,
|
||||
});
|
||||
tween.play();
|
||||
|
||||
// After tween we want to convert stage to dataURL
|
||||
setTimeout(function () {
|
||||
stage.toDataURL({
|
||||
callback: function (data) {
|
||||
// Then add result to stage
|
||||
var img = new Konva.window.Image();
|
||||
img.onload = function () {
|
||||
var image = new Konva.Image({
|
||||
image: img,
|
||||
x: 10,
|
||||
y: 50,
|
||||
});
|
||||
layer.add(image);
|
||||
layer.draw();
|
||||
// save stage image as file
|
||||
stage.toDataURL({
|
||||
callback: function (data) {
|
||||
var base64Data = data.replace(/^data:image\/png;base64,/, '');
|
||||
fs.writeFile('./out.png', base64Data, 'base64', function (err) {
|
||||
err && console.log(err);
|
||||
console.log('See out.png');
|
||||
});
|
||||
// now try to create image from url
|
||||
Konva.Image.fromURL(data, () => {
|
||||
console.log('image loaded');
|
||||
// shoul'd throw
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
img.src = data;
|
||||
},
|
||||
});
|
||||
}, 1050);
|
@ -1,39 +0,0 @@
|
||||
var Konva = require('konva');
|
||||
var canvas = require('canvas');
|
||||
|
||||
// mock window
|
||||
Konva.window = {
|
||||
Image: canvas.Image,
|
||||
devicePixelRatio: 1,
|
||||
};
|
||||
// mock document
|
||||
Konva.document = {
|
||||
createElement: function () {},
|
||||
documentElement: {
|
||||
addEventListener: function () {},
|
||||
},
|
||||
};
|
||||
|
||||
// make some global injections
|
||||
global.requestAnimationFrame = (cb) => {
|
||||
setImmediate(cb);
|
||||
};
|
||||
|
||||
// create canvas in Node env
|
||||
Konva.Util.createCanvasElement = () => {
|
||||
const node = new canvas.Canvas();
|
||||
node.style = {};
|
||||
return node;
|
||||
};
|
||||
|
||||
// create image in Node env
|
||||
Konva.Util.createImageElement = () => {
|
||||
const node = new canvas.Image();
|
||||
node.style = {};
|
||||
return node;
|
||||
};
|
||||
|
||||
// _checkVisibility use dom element, in node we can skip it
|
||||
Konva.Stage.prototype._checkVisibility = () => {};
|
||||
|
||||
module.exports = Konva;
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "konva-node",
|
||||
"version": "0.11.2",
|
||||
"description": "Konva framework for NodeJS env",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"index.js"
|
||||
],
|
||||
"type": "module",
|
||||
"typings": "./node_modules/konva/konva.d.ts",
|
||||
"scripts": {},
|
||||
"keywords": [
|
||||
"canvas",
|
||||
"animations",
|
||||
"graphic",
|
||||
"html5"
|
||||
],
|
||||
"author": "Anton Lavrenov",
|
||||
"bugs": {
|
||||
"url": "https://github.com/konvajs/konva/issues"
|
||||
},
|
||||
"homepage": "http://konvajs.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/konvajs/konva.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"canvas": "^2.5.0",
|
||||
"konva": "^7"
|
||||
}
|
||||
}
|
56
package.json
56
package.json
@ -3,16 +3,34 @@
|
||||
"version": "9.3.22",
|
||||
"description": "HTML5 2d canvas library.",
|
||||
"author": "Anton Lavrenov",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"README.md",
|
||||
"konva.js",
|
||||
"konva.min.js",
|
||||
"lib",
|
||||
"cmj"
|
||||
"lib"
|
||||
],
|
||||
"main": "./lib/index-node.js",
|
||||
"browser": "./lib/index.js",
|
||||
"typings": "./lib/index-types.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./lib/*": {
|
||||
"default": "./lib/*.js"
|
||||
},
|
||||
"./lib/*.js": {
|
||||
"import": "./lib/*.js",
|
||||
"require": "./lib/*.js",
|
||||
"default": "./lib/*.js"
|
||||
},
|
||||
"./canvas-backend": {
|
||||
"default": "./lib/canvas-backend.js"
|
||||
},
|
||||
"./skia-backend": {
|
||||
"default": "./lib/skia-backend.js"
|
||||
}
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index-types.d.ts",
|
||||
"scripts": {
|
||||
"start": "npm run test:watch",
|
||||
"compile": "npm run clean && npm run tsc && cp ./src/index-types.d.ts ./lib/index-types.d.ts && npm run rollup",
|
||||
@ -22,10 +40,11 @@
|
||||
"test:build": "PARCEL_WORKER_BACKEND=process parcel build ./test/unit-tests.html --dist-dir ./test-build --target none --public-url ./ --no-source-maps",
|
||||
"test:browser": "npm run test:build && mocha-headless-chrome -f ./test-build/unit-tests.html -a disable-web-security -a no-sandbox -a disable-setuid-sandbox",
|
||||
"test:watch": "rm -rf ./.parcel-cache && PARCEL_WORKERS=0 parcel serve ./test/unit-tests.html ./test/manual-tests.html ./test/sandbox.html ./test/text-paths.html ./test/bunnies.html",
|
||||
"test:node:compiled": "rm -rf ./.test-temp && mkdir ./.test-temp && (tsc -p ./test/tsconfig.json --outDir ./.test-temp || true) && mocha './.test-temp/test/unit/**/*.js' -r ./test/node-global-setup.mjs --exit && rm -rf ./.test-temp && npm run test:import",
|
||||
"test:node": "npm run test:node:compiled",
|
||||
"test:node:canvas": "rm -rf ./.test-temp && mkdir ./.test-temp && (tsc -p ./test/tsconfig.json --outDir ./.test-temp || true) && node ./rename-imports-test.mjs && mocha './.test-temp/test/unit/**/*.js' -r ./test/node-canvas-global-setup.mjs --exit && rm -rf ./.test-temp && npm run test:import",
|
||||
"test:node:skia": "rm -rf ./.test-temp && mkdir ./.test-temp && (tsc -p ./test/tsconfig.json --outDir ./.test-temp || true) && node ./rename-imports-test.mjs && mocha './.test-temp/test/unit/**/*.js' -r ./test/node-skia-global-setup.mjs --exit && rm -rf ./.test-temp && npm run test:import",
|
||||
"test:node": "npm run test:node:canvas && npm run test:node:skia",
|
||||
"tsc": "tsc --removeComments",
|
||||
"rollup": "rollup -c --bundleConfigAsCjs",
|
||||
"rollup": "rollup -c",
|
||||
"clean": "rm -rf ./lib && rm -rf ./types && rm -rf ./cmj && rm -rf ./test-build",
|
||||
"watch": "rollup -c -w",
|
||||
"size": "size-limit"
|
||||
@ -61,18 +80,18 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"@parcel/transformer-image": "2.13.2",
|
||||
"@size-limit/preset-big-lib": "^11.1.6",
|
||||
"@parcel/transformer-image": "2.15.4",
|
||||
"@size-limit/preset-big-lib": "^11.2.0",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"canvas": "^3.1.0",
|
||||
"chai": "5.1.2",
|
||||
"canvas": "^3.1.2",
|
||||
"chai": "5.2.1",
|
||||
"filehound": "^1.17.6",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp": "^5.0.1",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-connect": "^5.7.0",
|
||||
"gulp-exec": "^5.0.0",
|
||||
"gulp-jsdoc3": "^3.0.0",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-rename": "^2.1.0",
|
||||
"gulp-replace": "^1.1.4",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
@ -82,12 +101,13 @@
|
||||
"mocha-headless-chrome": "^4.0.0",
|
||||
"parcel": "2.13.3",
|
||||
"process": "^0.11.10",
|
||||
"rollup": "^4.31.0",
|
||||
"rollup": "^4.46.2",
|
||||
"rollup-plugin-typescript2": "^0.36.0",
|
||||
"size-limit": "^11.1.6",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"size-limit": "^11.2.0",
|
||||
"skia-canvas": "^2.0.2",
|
||||
"ts-mocha": "^11.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"keywords": [
|
||||
"canvas",
|
||||
|
36
rename-imports-test.mjs
Normal file
36
rename-imports-test.mjs
Normal file
@ -0,0 +1,36 @@
|
||||
import FileHound from 'filehound';
|
||||
import fs from 'fs';
|
||||
|
||||
const files = FileHound.create().paths('./.test-temp').ext(['js', 'ts']).find();
|
||||
|
||||
files.then((filePaths) => {
|
||||
filePaths.forEach((filepath) => {
|
||||
fs.readFile(filepath, 'utf8', (err, text) => {
|
||||
if (!text.match(/import .* from/g)) {
|
||||
return;
|
||||
}
|
||||
text = text.replace(/(import .* from\s+['"])(.*)(?=['"])/g, '$1$2.js');
|
||||
if (text.match(/export .* from/g)) {
|
||||
text = text.replace(/(export .* from\s+['"])(.*)(?=['"])/g, '$1$2.js');
|
||||
}
|
||||
|
||||
if (err) throw err;
|
||||
|
||||
// stupid replacement back
|
||||
text = text.replace("from 'canvas.js';", "from 'canvas';");
|
||||
text = text.replace("from 'chai.js';", "from 'chai';");
|
||||
text = text.replace("from 'skia-canvas.js';", "from 'skia-canvas';");
|
||||
|
||||
// Handle import("./x/y/z") syntax.
|
||||
text = text.replace(/(import\s*\(\s*['"])(.*)(?=['"])/g, '$1$2.js');
|
||||
|
||||
fs.writeFile(filepath, text, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Removed CommonJS export rewriting to keep ESM output intact
|
@ -34,14 +34,4 @@ files.then((filePaths) => {
|
||||
});
|
||||
});
|
||||
|
||||
const indexFiles = ['lib/index.js', 'lib/index-node.js', 'lib/Core.js'];
|
||||
indexFiles.forEach((filepath) => {
|
||||
fs.readFile(filepath, 'utf8', (err, text) => {
|
||||
text = text.replace('exports.default =', 'module.exports =');
|
||||
fs.writeFile(filepath, text, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// Removed CommonJS export rewriting to keep ESM output intact
|
||||
|
@ -11,16 +11,12 @@ export default {
|
||||
sourcemap: false,
|
||||
freeze: false,
|
||||
},
|
||||
// { file: pkg.module, format: 'es', sourcemap: true }
|
||||
],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [],
|
||||
watch: {
|
||||
include: 'src/**',
|
||||
},
|
||||
plugins: [
|
||||
// Allow json resolution
|
||||
// json(),
|
||||
// Compile TypeScript files
|
||||
typescript({
|
||||
useTsconfigDeclarationDir: true,
|
||||
@ -32,14 +28,5 @@ export default {
|
||||
},
|
||||
},
|
||||
}),
|
||||
// // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
|
||||
// commonjs(),
|
||||
// // Allow node_modules resolution, so you can use 'external' to control
|
||||
// // which external modules to include in the bundle
|
||||
// // https://github.com/rollup/rollup-plugin-node-resolve#usage
|
||||
// resolve(),
|
||||
|
||||
// Resolve source maps to the original source
|
||||
// sourceMaps()
|
||||
],
|
||||
};
|
||||
|
@ -184,6 +184,11 @@ export const Konva = {
|
||||
// insert Konva into global namespace (window)
|
||||
// it is required for npm packages
|
||||
_injectGlobal(Konva) {
|
||||
if (typeof glob.Konva !== 'undefined') {
|
||||
console.error(
|
||||
'Severa Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.'
|
||||
);
|
||||
}
|
||||
glob.Konva = Konva;
|
||||
},
|
||||
};
|
||||
|
@ -913,7 +913,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
|
||||
* @returns {Array}
|
||||
* @example
|
||||
* shape.getAncestors().forEach(function(node) {
|
||||
* console.log(node.getId());
|
||||
* console.log(node.id());
|
||||
* })
|
||||
*/
|
||||
getAncestors() {
|
||||
|
103
src/Util.ts
103
src/Util.ts
@ -2,6 +2,26 @@ import { Konva } from './Global';
|
||||
import { Context } from './Context';
|
||||
import { IRect, RGB, Vector2d } from './types';
|
||||
|
||||
const NODE_ERROR = `Konva.js unsupported environment.
|
||||
|
||||
Looks like you are trying to use Konva.js in Node.js environment. because "document" object is undefined.
|
||||
|
||||
To use Konva.js in Node.js environment, you need to use the "canvas-backend" or "skia-backend" module.
|
||||
|
||||
bash: npm install canvas
|
||||
js: import "konva/canvas-backend";
|
||||
|
||||
or
|
||||
|
||||
bash: npm install skia-canvas
|
||||
js: import "konva/skia-backend";
|
||||
`;
|
||||
const ensureBrowser = () => {
|
||||
if (typeof document === 'undefined') {
|
||||
throw new Error(NODE_ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Last updated November 2011
|
||||
* By Simon Sarris
|
||||
@ -424,7 +444,7 @@ const OBJECT_ARRAY = '[object Array]',
|
||||
yellowgreen: [154, 205, 5],
|
||||
},
|
||||
RGB_REGEX = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/;
|
||||
let animQueue: Array<Function> = [];
|
||||
let animQueue: Array<Function> = [];
|
||||
|
||||
const req =
|
||||
(typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame) ||
|
||||
@ -505,6 +525,7 @@ export const Util = {
|
||||
}
|
||||
},
|
||||
createCanvasElement() {
|
||||
ensureBrowser();
|
||||
const canvas = document.createElement('canvas');
|
||||
// on some environments canvas.style is readonly
|
||||
try {
|
||||
@ -513,6 +534,7 @@ export const Util = {
|
||||
return canvas;
|
||||
},
|
||||
createImageElement() {
|
||||
ensureBrowser();
|
||||
return document.createElement('img');
|
||||
},
|
||||
_isInDocument(el: any) {
|
||||
@ -995,6 +1017,11 @@ export const Util = {
|
||||
height: number,
|
||||
cornerRadius: number | number[]
|
||||
) {
|
||||
// if negative dimensions, abs width/height and move rectangle
|
||||
let xOrigin = width < 0 ? width : 0;
|
||||
let yOrigin = height < 0 ? height : 0;
|
||||
width = Math.abs(width);
|
||||
height = Math.abs(height);
|
||||
let topLeft = 0;
|
||||
let topRight = 0;
|
||||
let bottomLeft = 0;
|
||||
@ -1011,35 +1038,85 @@ export const Util = {
|
||||
bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2);
|
||||
bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2);
|
||||
}
|
||||
context.moveTo(topLeft, 0);
|
||||
context.lineTo(width - topRight, 0);
|
||||
context.moveTo(xOrigin + topLeft, yOrigin);
|
||||
context.lineTo(xOrigin + width - topRight, yOrigin);
|
||||
context.arc(
|
||||
width - topRight,
|
||||
topRight,
|
||||
xOrigin + width - topRight,
|
||||
yOrigin + topRight,
|
||||
topRight,
|
||||
(Math.PI * 3) / 2,
|
||||
0,
|
||||
false
|
||||
);
|
||||
context.lineTo(width, height - bottomRight);
|
||||
context.lineTo(xOrigin + width, yOrigin + height - bottomRight);
|
||||
context.arc(
|
||||
width - bottomRight,
|
||||
height - bottomRight,
|
||||
xOrigin + width - bottomRight,
|
||||
yOrigin + height - bottomRight,
|
||||
bottomRight,
|
||||
0,
|
||||
Math.PI / 2,
|
||||
false
|
||||
);
|
||||
context.lineTo(bottomLeft, height);
|
||||
context.lineTo(xOrigin + bottomLeft, yOrigin + height);
|
||||
context.arc(
|
||||
bottomLeft,
|
||||
height - bottomLeft,
|
||||
xOrigin + bottomLeft,
|
||||
yOrigin + height - bottomLeft,
|
||||
bottomLeft,
|
||||
Math.PI / 2,
|
||||
Math.PI,
|
||||
false
|
||||
);
|
||||
context.lineTo(0, topLeft);
|
||||
context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false);
|
||||
context.lineTo(xOrigin, yOrigin + topLeft);
|
||||
context.arc(
|
||||
xOrigin + topLeft,
|
||||
yOrigin + topLeft,
|
||||
topLeft,
|
||||
Math.PI,
|
||||
(Math.PI * 3) / 2,
|
||||
false
|
||||
);
|
||||
},
|
||||
drawRoundedPolygonPath(
|
||||
context: Context,
|
||||
points: Vector2d[],
|
||||
sides: number,
|
||||
radius: number,
|
||||
cornerRadius: number | number[]
|
||||
) {
|
||||
radius = Math.abs(radius);
|
||||
for (let i = 0; i < sides; i++) {
|
||||
const prev = points[(i - 1 + sides) % sides];
|
||||
const curr = points[i];
|
||||
const next = points[(i + 1) % sides];
|
||||
const vec1 = {x: curr.x - prev.x, y: curr.y - prev.y};
|
||||
const vec2 = {x: next.x - curr.x, y: next.y - curr.y};
|
||||
const len1 = Math.hypot(vec1.x, vec1.y);
|
||||
const len2 = Math.hypot(vec2.x, vec2.y);
|
||||
let currCornerRadius;
|
||||
if (typeof cornerRadius === 'number') {
|
||||
currCornerRadius = cornerRadius;
|
||||
} else {
|
||||
currCornerRadius = i < cornerRadius.length ? cornerRadius[i] : 0;
|
||||
}
|
||||
const maxCornerRadius = radius * Math.cos(Math.PI / sides);
|
||||
// cornerRadius creates perfect circle at 1/2 radius
|
||||
currCornerRadius = maxCornerRadius * Math.min(1, (currCornerRadius / radius) * 2);
|
||||
const normalVec1 = {x: vec1.x / len1, y: vec1.y / len1};
|
||||
const normalVec2 = {x: vec2.x / len2, y: vec2.y / len2};
|
||||
const p1 = {
|
||||
x: curr.x - normalVec1.x * currCornerRadius,
|
||||
y: curr.y - normalVec1.y * currCornerRadius,
|
||||
};
|
||||
const p2 = {
|
||||
x: curr.x + normalVec2.x * currCornerRadius,
|
||||
y: curr.y + normalVec2.y * currCornerRadius,
|
||||
};
|
||||
if (i === 0) {
|
||||
context.moveTo(p1.x, p1.y);
|
||||
} else {
|
||||
context.lineTo(p1.x, p1.y);
|
||||
}
|
||||
context.arcTo(curr.x, curr.y, p2.x, p2.y, currCornerRadius);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ import { Ring } from './shapes/Ring';
|
||||
import { Sprite } from './shapes/Sprite';
|
||||
import { Star } from './shapes/Star';
|
||||
import { Text } from './shapes/Text';
|
||||
import { MultiStyledText } from './shapes/MultiStyledText';
|
||||
import { TextPath } from './shapes/TextPath';
|
||||
import { Transformer } from './shapes/Transformer';
|
||||
import { Wedge } from './shapes/Wedge';
|
||||
@ -59,6 +60,7 @@ export const Konva = Core.Util._assign(Core, {
|
||||
Star,
|
||||
Text,
|
||||
TextPath,
|
||||
MultiStyledText,
|
||||
Transformer,
|
||||
Wedge,
|
||||
/**
|
||||
|
26
src/canvas-backend.ts
Normal file
26
src/canvas-backend.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Konva } from './_CoreInternals';
|
||||
import * as Canvas from 'canvas';
|
||||
|
||||
const canvas = Canvas['default'] || Canvas;
|
||||
|
||||
global.DOMMatrix = canvas.DOMMatrix;
|
||||
|
||||
Konva.Util['createCanvasElement'] = () => {
|
||||
const node = canvas.createCanvas(300, 300) as any;
|
||||
if (!node['style']) {
|
||||
node['style'] = {};
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
// create image in Node env
|
||||
Konva.Util.createImageElement = () => {
|
||||
const node = new canvas.Image() as any;
|
||||
return node;
|
||||
};
|
||||
|
||||
// this line is not part of the public API
|
||||
// but will be used in tests
|
||||
Konva.Util['Canvas'] = Canvas;
|
||||
|
||||
export default Konva;
|
671
src/shapes/MultiStyledText.ts
Normal file
671
src/shapes/MultiStyledText.ts
Normal file
@ -0,0 +1,671 @@
|
||||
import { Context } from '../Context'
|
||||
import { Factory } from '../Factory'
|
||||
import { _registerNode } from '../Global'
|
||||
import { Shape, ShapeConfig } from '../Shape'
|
||||
import { GetSet } from '../types'
|
||||
import { Util } from '../Util'
|
||||
import { getNumberOrAutoValidator, getNumberValidator, getBooleanValidator, getStringValidator } from '../Validators'
|
||||
|
||||
let dummyContext: Context & CanvasRenderingContext2D
|
||||
function getDummyContext() {
|
||||
if (dummyContext) {
|
||||
return dummyContext
|
||||
}
|
||||
dummyContext = Util.createCanvasElement().getContext('2d') as any
|
||||
return dummyContext
|
||||
}
|
||||
|
||||
function normalizeFontFamily(fontFamily: string) {
|
||||
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(', ');
|
||||
}
|
||||
|
||||
export interface TextStyle {
|
||||
start: number // start position of the style
|
||||
end?: number // end position of the style, if undefined it means until the end
|
||||
fontFamily: string
|
||||
fontSize: number
|
||||
fontStyle: 'normal' | 'italic' | 'bold' | 'italic bold' | 'bold italic'
|
||||
fontVariant: 'normal' | 'small-caps'
|
||||
textDecoration: '' | 'underline' | 'line-through' | 'underline line-through'
|
||||
fill: string
|
||||
stroke: string
|
||||
}
|
||||
|
||||
type TextPart = {
|
||||
text: string
|
||||
width: number
|
||||
style: Omit<TextStyle, 'start' | 'end'>
|
||||
}
|
||||
|
||||
export interface MultiStyledTextConfig extends ShapeConfig {
|
||||
text?: string;
|
||||
textStyles?: TextStyle[]
|
||||
align?: string;
|
||||
verticalAlign?: string;
|
||||
padding?: number;
|
||||
lineHeight?: number;
|
||||
letterSpacing?: number;
|
||||
wrap?: string;
|
||||
ellipsis?: boolean;
|
||||
}
|
||||
|
||||
export class MultiStyledText extends Shape<MultiStyledTextConfig> {
|
||||
public className = 'MultiStyledText'
|
||||
|
||||
public align!: GetSet<'left' | 'center' | 'right' | 'justify', this>
|
||||
public letterSpacing!: GetSet<number, this>
|
||||
public verticalAlign!: GetSet<'top' | 'middle' | 'bottom', this>
|
||||
public padding!: GetSet<number, this>
|
||||
public lineHeight!: GetSet<number, this>
|
||||
public text!: GetSet<string, this>
|
||||
public textStyles!: GetSet<TextStyle[], this>
|
||||
public wrap!: GetSet<'word' | 'char' | 'none', this>
|
||||
public ellipsis!: GetSet<boolean, this>
|
||||
|
||||
private textLines: {
|
||||
width: number
|
||||
totalHeight: number
|
||||
parts: TextPart[]
|
||||
}[] = []
|
||||
private linesWidth!: number
|
||||
private linesHeight!: number
|
||||
|
||||
// used when drawing
|
||||
private drawState!: {
|
||||
x: number
|
||||
y: number
|
||||
text: string
|
||||
}
|
||||
|
||||
constructor(config?: MultiStyledTextConfig) {
|
||||
super(config)
|
||||
// update text data for certain attr changes
|
||||
for (const attr of [
|
||||
'padding', 'wrap', 'lineHeight', 'letterSpacing', 'textStyles', 'width', 'height', 'text'
|
||||
]) {
|
||||
this.on(`${attr}Change.konva`, this.computeTextParts)
|
||||
}
|
||||
this.computeTextParts()
|
||||
}
|
||||
|
||||
private formatFont (part: Pick<TextPart, 'style'>) {
|
||||
return `${part.style.fontStyle} ${part.style.fontVariant} ${part.style.fontSize}px ${normalizeFontFamily(part.style.fontFamily)}`
|
||||
}
|
||||
|
||||
private measurePart (part: Omit<TextPart, 'width'>) {
|
||||
const context = getDummyContext()
|
||||
context.save()
|
||||
context.font = this.formatFont(part)
|
||||
const width = context.measureText(part.text).width
|
||||
context.restore()
|
||||
return width
|
||||
}
|
||||
|
||||
private computeTextParts () {
|
||||
this.textLines = []
|
||||
const lines = this.text().split('\n')
|
||||
const maxWidth = this.attrs.width
|
||||
const maxHeight = this.attrs.height
|
||||
const hasFixedWidth = maxWidth !== 'auto' && maxWidth !== undefined
|
||||
const hasFixedHeight = maxHeight !== 'auto' && maxHeight !== undefined
|
||||
|
||||
const shouldWrap = this.wrap() !== 'none'
|
||||
const wrapAtWord = this.wrap() !== 'char' && shouldWrap
|
||||
const shouldAddEllipsis = this.ellipsis()
|
||||
const styles = this.textStyles()
|
||||
const ellipsis = '…'
|
||||
const additionalWidth = shouldAddEllipsis ? this.measurePart({ text: ellipsis, style: styles[styles.length - 1] }) : 0;
|
||||
|
||||
const stylesByChar = Array.from(this.text()).map((char, index) => {
|
||||
return {
|
||||
char,
|
||||
style: styles.find((style) => index >= style.start && (typeof style.end === 'undefined' || style.end >= index))!
|
||||
}
|
||||
})
|
||||
const findParts = (start: number, end: number) => {
|
||||
// find matching characters
|
||||
const chars = stylesByChar.slice(start, end)
|
||||
// group them by style
|
||||
const parts: TextPart[] = []
|
||||
for (const char of chars) {
|
||||
const similarGroupIndex = parts.findIndex((part) => part.style === char.style)
|
||||
if (similarGroupIndex === -1) {
|
||||
parts.push({ text: char.char, width: 0, style: char.style })
|
||||
continue
|
||||
}
|
||||
parts[similarGroupIndex].text += char.char
|
||||
}
|
||||
return parts
|
||||
}
|
||||
const measureSubstring = (start: number, end: number) => {
|
||||
return measureParts(findParts(start, end))
|
||||
}
|
||||
const measureParts = (parts: TextPart[]) => {
|
||||
return parts.reduce((size, part) => {
|
||||
part.width = this.measurePart(part)
|
||||
return size + part.width
|
||||
}, 0)
|
||||
}
|
||||
const measureHeightParts = (parts: TextPart[]) => {
|
||||
return Math.max(...parts.map((part) => {
|
||||
return part.style.fontSize * this.lineHeight()
|
||||
}))
|
||||
}
|
||||
const addLine = (width: number, height: number, parts: TextPart[]) => {
|
||||
// if element height is fixed, abort if adding one more line would overflow
|
||||
// so we don't add this line, the loop will be broken anyway
|
||||
if (hasFixedHeight && (currentHeight + height) > maxHeight) {
|
||||
return
|
||||
}
|
||||
this.textLines.push({
|
||||
width,
|
||||
parts: parts.map((part) => {
|
||||
// compute size if not already computed during part creation
|
||||
part.width = part.width === 0 ? this.measurePart(part) : part.width
|
||||
return part
|
||||
}),
|
||||
totalHeight: height
|
||||
})
|
||||
}
|
||||
|
||||
let currentHeight = 0
|
||||
let charCount = 0
|
||||
for (let line of lines) {
|
||||
let lineWidth = measureSubstring(charCount, charCount + line.length)
|
||||
let lineHeight: number
|
||||
|
||||
if (hasFixedWidth && lineWidth > maxWidth) {
|
||||
/*
|
||||
* if width is fixed and line does not fit entirely
|
||||
* break the line into multiple fitting lines
|
||||
*/
|
||||
let cursor = 0
|
||||
while (line.length > 0) {
|
||||
/*
|
||||
* use binary search to find the longest substring that
|
||||
* that would fit in the specified width
|
||||
*/
|
||||
var low = 0,
|
||||
high = line.length,
|
||||
match = '',
|
||||
matchWidth = 0
|
||||
while (low < high) {
|
||||
var mid = (low + high) >>> 1,
|
||||
substr = line.slice(0, mid + 1),
|
||||
substrWidth = measureSubstring(charCount + cursor, charCount + cursor + mid + 1) + additionalWidth
|
||||
if (substrWidth <= maxWidth) {
|
||||
low = mid + 1
|
||||
match = substr
|
||||
matchWidth = substrWidth
|
||||
} else {
|
||||
high = mid
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 'low' is now the index of the substring end
|
||||
* 'match' is the substring
|
||||
* 'matchWidth' is the substring width in px
|
||||
*/
|
||||
if (match) {
|
||||
// a fitting substring was found
|
||||
if (wrapAtWord) {
|
||||
// try to find a space or dash where wrapping could be done
|
||||
let wrapIndex: number
|
||||
var nextChar = line[match.length]
|
||||
var nextIsSpaceOrDash = nextChar === ' ' || nextChar === '-'
|
||||
if (nextIsSpaceOrDash && matchWidth <= maxWidth) {
|
||||
wrapIndex = match.length
|
||||
} else {
|
||||
wrapIndex = Math.max(match.lastIndexOf(' '), match.lastIndexOf('-')) + 1
|
||||
}
|
||||
if (wrapIndex > 0) {
|
||||
// re-cut the substring found at the space/dash position
|
||||
low = wrapIndex
|
||||
match = match.slice(0, low)
|
||||
matchWidth = measureSubstring(charCount + cursor, charCount + cursor + low)
|
||||
}
|
||||
}
|
||||
// match = match.trimRight()
|
||||
const parts = findParts(charCount + cursor, charCount + cursor + low)
|
||||
lineHeight = measureHeightParts(parts)
|
||||
addLine(measureParts(parts), lineHeight, parts)
|
||||
currentHeight += lineHeight
|
||||
if (
|
||||
!shouldWrap ||
|
||||
(hasFixedHeight && currentHeight + lineHeight > maxHeight)
|
||||
) {
|
||||
const lastLine = this.textLines[this.textLines.length - 1]
|
||||
if (lastLine) {
|
||||
if (shouldAddEllipsis) {
|
||||
const lastPart = lastLine.parts[lastLine.parts.length - 1]
|
||||
const lastPartWidthWithEllipsis = this.measurePart({ ...lastPart, text: `${lastPart.text}${ellipsis}` })
|
||||
const haveSpace = lastPartWidthWithEllipsis < maxWidth
|
||||
if (!haveSpace) {
|
||||
lastPart.text = lastPart.text.slice(0, lastPart.text.length - 3)
|
||||
}
|
||||
lastLine.parts.splice(lastLine.parts.length - 1, 1)
|
||||
lastLine.parts.push({
|
||||
...lastPart,
|
||||
width: lastPartWidthWithEllipsis,
|
||||
text: `${lastPart.text}${ellipsis}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* stop wrapping if wrapping is disabled or if adding
|
||||
* one more line would overflow the fixed height
|
||||
*/
|
||||
break
|
||||
}
|
||||
line = line.slice(low)
|
||||
cursor += low
|
||||
// line = line.trimLeft()
|
||||
if (line.length > 0) {
|
||||
// Check if the remaining text would fit on one line
|
||||
const parts = findParts(charCount + cursor, charCount + cursor + line.length)
|
||||
lineWidth = measureParts(parts)
|
||||
if (lineWidth <= maxWidth) {
|
||||
// if it does, add the line and break out of the loop
|
||||
const height = measureHeightParts(parts)
|
||||
addLine(lineWidth, height, parts)
|
||||
currentHeight += height
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// not even one character could fit in the element, abort
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const parts = findParts(charCount, charCount + line.length)
|
||||
lineHeight = measureHeightParts(parts)
|
||||
addLine(lineWidth, lineHeight, parts)
|
||||
}
|
||||
|
||||
// if element height is fixed, abort if adding one more line would overflow
|
||||
// so we stop here to avoid processing useless lines
|
||||
if (hasFixedHeight && (currentHeight + lineHeight!) > maxHeight) {
|
||||
break
|
||||
}
|
||||
|
||||
charCount += line.length
|
||||
currentHeight += lineHeight!
|
||||
}
|
||||
|
||||
this.linesHeight = this.textLines.reduce((size, line) => size + line.totalHeight, 0)
|
||||
this.linesWidth = Math.max(...this.textLines.map((line) => line.width, 0))
|
||||
}
|
||||
|
||||
public getHeight (): number {
|
||||
const isAuto = this.attrs.height === 'auto' || this.attrs.height === undefined
|
||||
if (!isAuto) {
|
||||
return this.attrs.height
|
||||
}
|
||||
return this.linesHeight + this.padding() * 2
|
||||
}
|
||||
|
||||
public getWidth (): number {
|
||||
const isAuto = this.attrs.width === 'auto' || this.attrs.width === undefined
|
||||
if (!isAuto) {
|
||||
return this.attrs.width
|
||||
}
|
||||
return this.linesWidth + this.padding() * 2
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This method is called when the shape should render
|
||||
* on canvas
|
||||
*/
|
||||
protected _sceneFunc(context: Context & CanvasRenderingContext2D) {
|
||||
if (this.text().length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const totalWidth = this.getWidth()
|
||||
const totalHeight = this.getHeight()
|
||||
|
||||
context.setAttr('textBaseline', 'middle')
|
||||
context.setAttr('textAlign', 'left')
|
||||
|
||||
// handle vertical alignment
|
||||
const padding = this.padding()
|
||||
let alignY = 0
|
||||
if (this.verticalAlign() === 'middle') {
|
||||
alignY = (totalHeight - this.linesHeight - padding * 2) / 2;
|
||||
} else if (this.verticalAlign() === 'bottom') {
|
||||
alignY = totalHeight - this.linesHeight - padding * 2;
|
||||
}
|
||||
context.translate(padding, alignY + padding)
|
||||
|
||||
let y = this.textLines[0].totalHeight / 2
|
||||
let lineIndex = 0
|
||||
for (const line of this.textLines) {
|
||||
const isLastLine = lineIndex === this.textLines.length - 1
|
||||
let lineX = 0
|
||||
let lineY = 0
|
||||
context.save()
|
||||
|
||||
// horizontal alignment
|
||||
if (this.align() === 'right') {
|
||||
lineX += totalWidth - line.width - padding * 2
|
||||
} else if (this.align() === 'center') {
|
||||
lineY += (totalWidth - line.width - padding * 2) / 2
|
||||
}
|
||||
|
||||
for (const part of line.parts) {
|
||||
|
||||
// style
|
||||
if (part.style.textDecoration.includes('underline')) {
|
||||
context.save();
|
||||
context.beginPath()
|
||||
|
||||
context.moveTo(
|
||||
lineX,
|
||||
y + lineY + Math.round(part.style.fontSize / 2)
|
||||
)
|
||||
const spacesNumber = part.text.split(' ').length - 1
|
||||
const oneWord = spacesNumber === 0
|
||||
const lineWidth =
|
||||
this.align() === 'justify' && isLastLine && !oneWord
|
||||
? totalWidth - padding * 2
|
||||
: part.width
|
||||
context.lineTo(
|
||||
lineX + Math.round(lineWidth),
|
||||
y + lineY + Math.round(part.style.fontSize / 2)
|
||||
)
|
||||
|
||||
// I have no idea what is real ratio
|
||||
// just /15 looks good enough
|
||||
context.lineWidth = part.style.fontSize / 15
|
||||
context.strokeStyle = part.style.fill
|
||||
context.stroke()
|
||||
context.restore()
|
||||
}
|
||||
if (part.style.textDecoration.includes('line-through')) {
|
||||
context.save()
|
||||
context.beginPath()
|
||||
context.moveTo(lineX, y + lineY)
|
||||
const spacesNumber = part.text.split(' ').length - 1
|
||||
const oneWord = spacesNumber === 0
|
||||
const lineWidth =
|
||||
this.align() === 'justify' && isLastLine && !oneWord
|
||||
? totalWidth - padding * 2
|
||||
: part.width
|
||||
context.lineTo(
|
||||
lineX + Math.round(lineWidth),
|
||||
y + lineY
|
||||
)
|
||||
context.lineWidth = part.style.fontSize / 15
|
||||
context.strokeStyle = part.style.fill
|
||||
context.stroke()
|
||||
context.restore()
|
||||
}
|
||||
|
||||
this.fill(part.style.fill)
|
||||
this.stroke(part.style.stroke)
|
||||
context.setAttr('font', this.formatFont(part))
|
||||
|
||||
// text
|
||||
if (this.letterSpacing() !== 0 || this.align() === 'justify') {
|
||||
const spacesNumber = part.text.split(' ').length - 1
|
||||
var array = Array.from(part.text)
|
||||
for (let li = 0; li < array.length; li++) {
|
||||
const letter = array[li]
|
||||
// skip justify for the last line
|
||||
if (letter === ' ' && lineIndex !== this.textLines.length - 1 && this.align() === 'justify') {
|
||||
lineX += (totalWidth - padding * 2 - line.width) / spacesNumber;
|
||||
}
|
||||
this.drawState = {
|
||||
x: lineX,
|
||||
y: y + lineY,
|
||||
text: letter
|
||||
}
|
||||
context.fillStrokeShape(this)
|
||||
lineX += this.measurePart({ ...part, text: letter }) + this.letterSpacing()
|
||||
}
|
||||
} else {
|
||||
this.drawState = {
|
||||
x: lineX,
|
||||
y: y + lineY,
|
||||
text: part.text
|
||||
}
|
||||
context.fillStrokeShape(this)
|
||||
lineX += part.width + this.letterSpacing()
|
||||
}
|
||||
}
|
||||
|
||||
context.restore()
|
||||
if (typeof this.textLines[lineIndex + 1] !== 'undefined') {
|
||||
y += this.textLines[lineIndex + 1].totalHeight
|
||||
}
|
||||
++lineIndex
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This method is called by context.fillStrokeShape(this)
|
||||
* to fill the shape
|
||||
*/
|
||||
public _fillFunc = (context: Context) => {
|
||||
context.fillText(this.drawState.text, this.drawState.x, this.drawState.y)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This method is called by context.fillStrokeShape(this)
|
||||
* to stroke the shape
|
||||
*/
|
||||
public _strokeFunc = (context: Context) => {
|
||||
context.strokeText(this.drawState.text, this.drawState.x, this.drawState.y, undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This method should render on canvas a rect with
|
||||
* the width and the height of the text shape
|
||||
*/
|
||||
protected _hitFunc(context: Context & CanvasRenderingContext2D) {
|
||||
context.beginPath()
|
||||
context.rect(0, 0, this.getWidth(), this.getHeight())
|
||||
context.closePath()
|
||||
context.fillStrokeShape(this)
|
||||
}
|
||||
|
||||
// for text we can't disable stroke scaling
|
||||
// if we do, the result will be unexpected
|
||||
public getStrokeScaleEnabled() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
_registerNode(MultiStyledText)
|
||||
|
||||
/**
|
||||
* 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(MultiStyledText, '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(MultiStyledText, 'height', getNumberOrAutoValidator())
|
||||
|
||||
/**
|
||||
* 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(MultiStyledText, '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');
|
||||
*/
|
||||
Factory.addGetterSetter(MultiStyledText, '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(MultiStyledText, '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(MultiStyledText, '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(MultiStyledText, 'wrap', 'word')
|
||||
|
||||
/**
|
||||
* get/set ellipsis. Can be true or false. Default is false. If ellipses is true,
|
||||
* Konva will add "..." at the end of the text if it doesn't have enough space to write characters.
|
||||
* That is possible only when you limit both width and height of the text
|
||||
* @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(MultiStyledText, 'ellipsis', false, getBooleanValidator())
|
||||
|
||||
/**
|
||||
* set letter spacing property. Default value is 0.
|
||||
* @name Konva.Text#letterSpacing
|
||||
* @method
|
||||
* @param {Number} letterSpacing
|
||||
*/
|
||||
Factory.addGetterSetter(MultiStyledText, '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(MultiStyledText, 'text', '', getStringValidator())
|
||||
|
||||
/**
|
||||
* get/set textStyles
|
||||
* @name Konva.Text#textStyles
|
||||
* @method
|
||||
* @param {TextStyle[]} textStyles
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // set styles
|
||||
* text.textStyles([{ start: 0, fontFamily: 'Roboto' }]);
|
||||
*/
|
||||
const defaultStyle: TextStyle = {
|
||||
start: 0,
|
||||
fill: 'black',
|
||||
stroke: 'black',
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 12,
|
||||
fontStyle: 'normal',
|
||||
fontVariant: 'normal',
|
||||
textDecoration: ''
|
||||
}
|
||||
Factory.addGetterSetter(MultiStyledText, 'textStyles', [defaultStyle])
|
@ -1,13 +1,15 @@
|
||||
import { Factory } from '../Factory';
|
||||
import { Shape, ShapeConfig } from '../Shape';
|
||||
import { GetSet, Vector2d } from '../types';
|
||||
import { getNumberValidator } from '../Validators';
|
||||
import { getNumberOrArrayOfNumbersValidator, getNumberValidator } from '../Validators';
|
||||
import { _registerNode } from '../Global';
|
||||
import { Context } from '../Context';
|
||||
import { Util } from '../Util';
|
||||
|
||||
export interface RegularPolygonConfig extends ShapeConfig {
|
||||
sides: number;
|
||||
radius: number;
|
||||
cornerRadius?: number | number[];
|
||||
}
|
||||
/**
|
||||
* RegularPolygon constructor. Examples include triangles, squares, pentagons, hexagons, etc.
|
||||
@ -15,6 +17,7 @@ export interface RegularPolygonConfig extends ShapeConfig {
|
||||
* @memberof Konva
|
||||
* @augments Konva.Shape
|
||||
* @param {Object} config
|
||||
* @param {Number} [config.cornerRadius]
|
||||
* @param {Number} config.sides
|
||||
* @param {Number} config.radius
|
||||
* @@shapeParams
|
||||
@ -32,13 +35,20 @@ export interface RegularPolygonConfig extends ShapeConfig {
|
||||
*/
|
||||
export class RegularPolygon extends Shape<RegularPolygonConfig> {
|
||||
_sceneFunc(context: Context) {
|
||||
const points = this._getPoints();
|
||||
const points = this._getPoints(),
|
||||
radius = this.radius(),
|
||||
sides = this.sides(),
|
||||
cornerRadius = this.cornerRadius();
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(points[0].x, points[0].y);
|
||||
|
||||
for (let n = 1; n < points.length; n++) {
|
||||
context.lineTo(points[n].x, points[n].y);
|
||||
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();
|
||||
@ -91,6 +101,7 @@ export class RegularPolygon extends Shape<RegularPolygonConfig> {
|
||||
|
||||
radius: GetSet<number, this>;
|
||||
sides: GetSet<number, this>;
|
||||
cornerRadius: GetSet<number | number[], this>;
|
||||
}
|
||||
|
||||
RegularPolygon.prototype.className = 'RegularPolygon';
|
||||
@ -127,3 +138,26 @@ Factory.addGetterSetter(RegularPolygon, 'radius', 0, getNumberValidator());
|
||||
* 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)
|
||||
);
|
||||
|
@ -13,6 +13,18 @@ import { _registerNode } from '../Global';
|
||||
|
||||
import { GetSet } from '../types';
|
||||
|
||||
export interface CharRenderProps {
|
||||
char: string;
|
||||
index: number;
|
||||
x: number;
|
||||
y: number;
|
||||
lineIndex: number;
|
||||
column: number;
|
||||
isLastInLine: boolean;
|
||||
width: number;
|
||||
context: Context;
|
||||
}
|
||||
|
||||
export function stringToArray(string: string): string[] {
|
||||
// Use Unicode-aware splitting
|
||||
return [...string].reduce((acc, char, index, array) => {
|
||||
@ -223,6 +235,7 @@ export class Text extends Shape<TextConfig> {
|
||||
align = this.align(),
|
||||
totalWidth = this.getWidth(),
|
||||
letterSpacing = this.letterSpacing(),
|
||||
charRenderFunc = this.charRenderFunc(),
|
||||
fill = this.fill(),
|
||||
textDecoration = this.textDecoration(),
|
||||
shouldUnderline = textDecoration.indexOf('underline') !== -1,
|
||||
@ -318,10 +331,14 @@ export class Text extends Shape<TextConfig> {
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
// As `letterSpacing` isn't supported on Safari, we use this polyfill.
|
||||
// The exception is for RTL text, which we rely on native as it cannot
|
||||
// be supported otherwise.
|
||||
if (direction !== RTL && (letterSpacing !== 0 || align === JUSTIFY)) {
|
||||
if (
|
||||
direction !== RTL &&
|
||||
(letterSpacing !== 0 || align === JUSTIFY || charRenderFunc)
|
||||
) {
|
||||
// var words = text.split(' ');
|
||||
const spacesNumber = text.split(' ').length - 1;
|
||||
const array = stringToArray(text);
|
||||
@ -338,7 +355,31 @@ export class Text extends Shape<TextConfig> {
|
||||
this._partialTextX = lineTranslateX;
|
||||
this._partialTextY = translateY + lineTranslateY;
|
||||
this._partialText = letter;
|
||||
|
||||
if (charRenderFunc) {
|
||||
context.save();
|
||||
const previousLines = textArr.slice(0, n);
|
||||
const previousGraphemes = previousLines.reduce(
|
||||
(acc, line) => acc + stringToArray(line.text).length,
|
||||
0
|
||||
);
|
||||
const charIndex = li + previousGraphemes;
|
||||
charRenderFunc({
|
||||
char: letter,
|
||||
index: charIndex,
|
||||
x: lineTranslateX,
|
||||
y: translateY + lineTranslateY,
|
||||
lineIndex: n,
|
||||
column: li,
|
||||
isLastInLine: lastLine,
|
||||
width: this.measureSize(letter).width,
|
||||
context,
|
||||
});
|
||||
}
|
||||
context.fillStrokeShape(this);
|
||||
if (charRenderFunc) {
|
||||
context.restore();
|
||||
}
|
||||
lineTranslateX += this.measureSize(letter).width + letterSpacing;
|
||||
}
|
||||
} else {
|
||||
@ -713,6 +754,7 @@ export class Text extends Shape<TextConfig> {
|
||||
text: GetSet<string, this>;
|
||||
wrap: GetSet<string, this>;
|
||||
ellipsis: GetSet<boolean, this>;
|
||||
charRenderFunc: GetSet<null | ((props: CharRenderProps) => void), this>;
|
||||
}
|
||||
|
||||
Text.prototype._fillFunc = _fillFunc;
|
||||
@ -995,3 +1037,21 @@ Factory.addGetterSetter(Text, 'text', '', getStringValidator());
|
||||
*/
|
||||
|
||||
Factory.addGetterSetter(Text, 'textDecoration', '');
|
||||
|
||||
/**
|
||||
* get/set per-character render hook. The callback is invoked for each grapheme before drawing.
|
||||
* 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 {(props: {char: string, index: number, x: number, y: number, lineIndex: number, column: number, isLastInLine: boolean, width: number, context: Konva.Context}) => void} charRenderFunc
|
||||
* @returns {(props: CharRenderProps) => void}
|
||||
* @example
|
||||
* // apply small x-translation to every second character
|
||||
* text.charRenderFunc(function(props) {
|
||||
* if (props.index % 2 === 1) {
|
||||
* props.context.translate(2, 0);
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
Factory.addGetterSetter(Text, 'charRenderFunc', undefined);
|
||||
|
36
src/skia-backend.ts
Normal file
36
src/skia-backend.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Konva } from './_CoreInternals';
|
||||
import { Canvas, DOMMatrix, Image } from 'skia-canvas';
|
||||
|
||||
global.DOMMatrix = DOMMatrix as any;
|
||||
|
||||
// @ts-ignore
|
||||
Canvas.prototype.toDataURL = Canvas.prototype.toDataURLSync;
|
||||
|
||||
Konva.Util['createCanvasElement'] = () => {
|
||||
const node = new Canvas(300, 300) as any;
|
||||
if (!node['style']) {
|
||||
node['style'] = {};
|
||||
}
|
||||
node.toString = () => '[object HTMLCanvasElement]';
|
||||
const ctx = node.getContext('2d');
|
||||
// Override the getter to return the canvas node directly
|
||||
// because in skia-canvas canvas is using weak ref to the canvas node
|
||||
// and somehow on many tests it fails to get the canvas node
|
||||
Object.defineProperty(ctx, 'canvas', {
|
||||
get: () => node,
|
||||
});
|
||||
return node;
|
||||
};
|
||||
|
||||
// create image in Node env
|
||||
Konva.Util.createImageElement = () => {
|
||||
const node = new Image() as any;
|
||||
node.toString = () => '[object HTMLImageElement]';
|
||||
return node;
|
||||
};
|
||||
|
||||
// this line is not part of the public API
|
||||
// but will be used in tests
|
||||
Konva.Util['isSkia'] = true;
|
||||
|
||||
export default Konva;
|
@ -1,5 +1,6 @@
|
||||
// try to import only core
|
||||
const Konva = require('../');
|
||||
const Konva = require('../').default;
|
||||
require('../lib/canvas-backend');
|
||||
|
||||
// just do a simple action
|
||||
const stage = new Konva.Stage();
|
||||
|
@ -4,15 +4,15 @@ function equal(val1, val2, message) {
|
||||
}
|
||||
}
|
||||
|
||||
// try to import only core
|
||||
// try to import only core from built lib
|
||||
import Konva from '../lib/Core.js';
|
||||
import '../lib/canvas-backend.js';
|
||||
import { Rect } from '../lib/shapes/Rect.js';
|
||||
import '../lib/index-node.js';
|
||||
|
||||
equal(Rect !== undefined, true, 'Rect is defined');
|
||||
|
||||
equal(Konva.Rect, Rect, 'Rect is injected');
|
||||
|
||||
// // just do a simple action
|
||||
// just do a simple action
|
||||
const stage = new Konva.Stage();
|
||||
stage.toDataURL();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { addStage, Konva, loadImage } from '../unit/test-utils';
|
||||
import { addStage, Konva, loadImage, showHit } from '../unit/test-utils';
|
||||
|
||||
describe('Manual', function () {
|
||||
// ======================================================
|
||||
|
21
test/node-canvas-global-setup.mjs
Normal file
21
test/node-canvas-global-setup.mjs
Normal file
@ -0,0 +1,21 @@
|
||||
export async function mochaGlobalSetup() {
|
||||
// Load node-canvas polyfills on the compiled test output
|
||||
// Path from this file (test/) to compiled file (.test-temp/src/...)
|
||||
try {
|
||||
await import(
|
||||
new URL('../.test-temp/src/canvas-backend.js', import.meta.url)
|
||||
);
|
||||
} catch (e) {
|
||||
// If not compiled yet or path missing, keep going; tests that need it will fail clearly
|
||||
}
|
||||
|
||||
globalThis.Path2D ??= class Path2D {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return `Path2D`;
|
||||
}
|
||||
};
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
export function mochaGlobalSetup() {
|
||||
globalThis.Path2D ??= class Path2D {
|
||||
constructor(path) {
|
||||
this.path = path
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return `Path2D`;
|
||||
}
|
||||
}
|
||||
}
|
13
test/node-skia-global-setup.mjs
Normal file
13
test/node-skia-global-setup.mjs
Normal file
@ -0,0 +1,13 @@
|
||||
export async function mochaGlobalSetup() {
|
||||
await import(new URL('../.test-temp/src/skia-backend.js', import.meta.url));
|
||||
|
||||
globalThis.Path2D ??= class Path2D {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return `Path2D`;
|
||||
}
|
||||
};
|
||||
}
|
@ -48,37 +48,40 @@
|
||||
var layer = new Konva.Layer();
|
||||
stage.add(layer);
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
// use external library to parse and draw gif animation
|
||||
function onDrawFrame(ctx, frame) {
|
||||
// update canvas size
|
||||
canvas.width = frame.width;
|
||||
canvas.height = frame.height;
|
||||
// update canvas that we are using for Konva.Image
|
||||
ctx.drawImage(frame.buffer, 0, 0);
|
||||
// redraw the layer
|
||||
layer.draw();
|
||||
}
|
||||
const text = new Konva.Text({
|
||||
text: 'Hello, how are you doing today? Would you like to start using konva.js',
|
||||
width: 400,
|
||||
fontSize: 50,
|
||||
x: 100,
|
||||
y: 100,
|
||||
draggable: true,
|
||||
});
|
||||
layer.add(text);
|
||||
|
||||
gifler('https://konvajs.org/assets/yoda.gif').frames(canvas, onDrawFrame);
|
||||
|
||||
function testKonvaImage() {
|
||||
setInterval(() => {
|
||||
const image = new Konva.Image({
|
||||
image: canvas,
|
||||
x: Math.random() * width,
|
||||
y: Math.random() * height,
|
||||
});
|
||||
layer.add(image);
|
||||
|
||||
setTimeout(() => {
|
||||
image.image(canvas);
|
||||
image.destroy();
|
||||
}, 500);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
testKonvaImage();
|
||||
const anim = new Konva.Animation((frame) => {
|
||||
text.charRenderFunc(({ char, index, context }) => {
|
||||
const animationDuration = 4000;
|
||||
const animationTime = frame.time % animationDuration;
|
||||
const length = text.text().length;
|
||||
const durationPerChar = animationDuration / length;
|
||||
const localStartTime = index * durationPerChar;
|
||||
const localTime = animationTime - localStartTime;
|
||||
const inAnimation = localTime > 0;
|
||||
if (!inAnimation) {
|
||||
context.setAttr('globalAlpha', 0);
|
||||
return;
|
||||
}
|
||||
const afterAnimation = localTime > durationPerChar;
|
||||
if (afterAnimation) {
|
||||
return;
|
||||
}
|
||||
const animationAlpha = Math.abs(localTime / durationPerChar);
|
||||
const oldOpacity = context.globalAlpha;
|
||||
context.setAttr('globalAlpha', oldOpacity * animationAlpha * 0.5);
|
||||
context.translate(0, -15 + (localTime / durationPerChar) * 15);
|
||||
});
|
||||
}, layer);
|
||||
anim.start();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -4,7 +4,7 @@
|
||||
"noEmitOnError": false,
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2015", "dom"],
|
||||
"module": "CommonJS",
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
"allowJs": true,
|
||||
|
@ -86,8 +86,6 @@ describe('Group', function () {
|
||||
|
||||
const trace = layer.getContext().getTrace();
|
||||
|
||||
console.log(trace);
|
||||
|
||||
assert.equal(
|
||||
trace,
|
||||
'clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);beginPath();rect(0,0,0,0);clip();transform(1,0,0,1,0,0);restore();'
|
||||
|
@ -327,7 +327,7 @@ describe('Image', function () {
|
||||
layer.add(image);
|
||||
layer.draw();
|
||||
assert.equal(image instanceof Konva.Image, true);
|
||||
var nativeImg = image.image();
|
||||
var nativeImg = image.image() as HTMLImageElement;
|
||||
assert.equal(nativeImg instanceof Image, true);
|
||||
assert.equal(nativeImg.src.indexOf(src) !== -1, true);
|
||||
assert.equal(nativeImg.complete, true);
|
||||
|
@ -366,7 +366,7 @@ describe('Label', function () {
|
||||
layer.add(label);
|
||||
|
||||
assert.equal(counter, 4);
|
||||
tag.pointerDirection('bottom');
|
||||
tag.pointerDirection('down');
|
||||
assert.equal(counter, 5);
|
||||
tag.pointerWidth(30);
|
||||
assert.equal(counter, 6);
|
||||
|
@ -171,9 +171,7 @@ describe('Layer', function () {
|
||||
|
||||
circle.colorKey = '#000000';
|
||||
|
||||
circle.on('mouseover', function () {
|
||||
console.log('mouseover');
|
||||
});
|
||||
circle.on('mouseover', function () {});
|
||||
|
||||
layer.add(circle);
|
||||
stage.add(layer);
|
||||
|
@ -2068,7 +2068,7 @@ describe('MouseEvents', function () {
|
||||
type: 'mouseenter',
|
||||
};
|
||||
|
||||
stage._pointerenter(evt);
|
||||
stage._pointerenter(evt as PointerEvent);
|
||||
|
||||
assert.equal(mouseenterCount, 1, 'mouseenterCount should be 1');
|
||||
});
|
||||
@ -2268,7 +2268,7 @@ describe('MouseEvents', function () {
|
||||
type: 'mouseenter',
|
||||
};
|
||||
|
||||
stage._pointerenter(evt);
|
||||
stage._pointerenter(evt as PointerEvent);
|
||||
|
||||
simulateMouseMove(stage, {
|
||||
x: 10,
|
||||
@ -2386,9 +2386,7 @@ describe('MouseEvents', function () {
|
||||
var layer = new Konva.Layer();
|
||||
stage.add(layer);
|
||||
|
||||
stage.on('mousedown mousemove mouseup click', function (e) {
|
||||
console.log('state', e.type);
|
||||
});
|
||||
stage.on('mousedown mousemove mouseup click', function (e) {});
|
||||
|
||||
var rect = new Konva.Rect({
|
||||
width: 50,
|
||||
@ -2402,7 +2400,6 @@ describe('MouseEvents', function () {
|
||||
|
||||
var clicks = 0;
|
||||
rect.on('click', function () {
|
||||
console.log('click');
|
||||
clicks += 1;
|
||||
if (clicks === 2) {
|
||||
debugger;
|
||||
|
@ -831,7 +831,6 @@ describe('Caching', function () {
|
||||
group.cache();
|
||||
|
||||
const canvas = group._cache.get('canvas').scene;
|
||||
console.log(canvas.width / 2);
|
||||
assert.equal(canvas.width, 106 * canvas.pixelRatio);
|
||||
});
|
||||
|
||||
|
@ -924,11 +924,11 @@ describe('Node', function () {
|
||||
});
|
||||
|
||||
circle1.on('mousemove', function () {
|
||||
console.log('mousemove circle1');
|
||||
// console.log('mousemove circle1');
|
||||
});
|
||||
|
||||
circle2.on('mousemove', function () {
|
||||
console.log('mousemove circle2');
|
||||
// console.log('mousemove circle2');
|
||||
});
|
||||
|
||||
layer1.add(circle1);
|
||||
@ -1101,11 +1101,11 @@ describe('Node', function () {
|
||||
});
|
||||
|
||||
circle1.on('mousemove', function () {
|
||||
console.log('mousemove circle1');
|
||||
// console.log('mousemove circle1');
|
||||
});
|
||||
|
||||
circle2.on('mousemove', function () {
|
||||
console.log('mousemove circle2');
|
||||
// console.log('mousemove circle2');
|
||||
});
|
||||
|
||||
group.add(circle2);
|
||||
@ -1887,12 +1887,6 @@ describe('Node', function () {
|
||||
|
||||
circle.on('click', function () {
|
||||
clicks.push('circle');
|
||||
|
||||
/*
|
||||
var evt = window.event;
|
||||
var rightClick = evt.which ? evt.which == 3 : evt.button == 2;
|
||||
console.log(rightClick);
|
||||
*/
|
||||
});
|
||||
var foo;
|
||||
circle.on('customEvent', function (evt) {
|
||||
@ -2558,11 +2552,18 @@ describe('Node', function () {
|
||||
});
|
||||
|
||||
it('make sure we can create non existing node type', function () {
|
||||
const oldWarn = console.warn;
|
||||
let called = false;
|
||||
console.warn = function () {
|
||||
called = true;
|
||||
};
|
||||
var json =
|
||||
'{"attrs":{},"className":"Layer","children":[{"attrs":{},"className":"Group","children":[{"attrs":{"x":289,"y":100,"radius":70,"fill":"green","stroke":"black","strokeWidth":4,"name":"myCircle","draggable":true},"className":"WeirdShape"}]}]}';
|
||||
var layer = Konva.Node.create(json);
|
||||
|
||||
assert.deepEqual(layer.find('Shape').length, 1);
|
||||
console.warn = oldWarn;
|
||||
assert.equal(called, true);
|
||||
});
|
||||
|
||||
// ======================================================
|
||||
@ -3484,6 +3485,7 @@ describe('Node', function () {
|
||||
assert.equal(rect.findAncestor('#group'), group);
|
||||
assert.equal(rect.findAncestor('Group'), group);
|
||||
|
||||
// @ts-expect-error - test for no selector
|
||||
assert.equal(rect.findAncestor(), null, 'return null if no selector');
|
||||
});
|
||||
|
||||
|
@ -1186,7 +1186,6 @@ describe('Path', function () {
|
||||
SVGPath.setAttribute('d', data);
|
||||
for (var i = 0.001; i < path.getLength(); i += 1) {
|
||||
var p = path.getPointAtLength(i);
|
||||
console.log(p);
|
||||
var circle = new Konva.Circle({
|
||||
x: p.x + path.x(),
|
||||
y: p.y + path.y(),
|
||||
@ -1196,7 +1195,6 @@ describe('Path', function () {
|
||||
});
|
||||
layer.add(circle);
|
||||
const position = SVGPath.getPointAtLength(i);
|
||||
console.log(position);
|
||||
assert(Math.abs(p.x - position.x) <= 1);
|
||||
assert(Math.abs(p.y - position.y) <= 1);
|
||||
}
|
||||
|
@ -234,4 +234,28 @@ describe('Rect', function () {
|
||||
'clearRect(0,0,578,200);save();transform(1,0,0,1,50,50);beginPath();moveTo(0,0);lineTo(90,0);arc(90,10,10,4.712,0,false);lineTo(100,80);arc(80,80,20,0,1.571,false);lineTo(30,100);arc(30,70,30,1.571,3.142,false);lineTo(0,0);arc(0,0,0,3.142,4.712,false);closePath();fillStyle=black;fill();restore();clearRect(0,0,578,200);save();transform(1,0,0,1,50,50);beginPath();moveTo(0,0);lineTo(90,0);arc(90,10,10,4.712,0,false);lineTo(100,80);arc(80,80,20,0,1.571,false);lineTo(30,100);arc(30,70,30,1.571,3.142,false);lineTo(0,0);arc(0,0,0,3.142,4.712,false);closePath();fillStyle=black;fill();restore();'
|
||||
);
|
||||
});
|
||||
|
||||
// ======================================================
|
||||
it('corner radius with negative width and height', function () {
|
||||
var stage = addStage();
|
||||
var layer = new Konva.Layer();
|
||||
var rect = new Konva.Rect({
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: -100,
|
||||
height: -100,
|
||||
fill: 'black',
|
||||
cornerRadius: [0, 10, 20, 30],
|
||||
});
|
||||
|
||||
layer.add(rect);
|
||||
stage.add(layer);
|
||||
layer.draw();
|
||||
|
||||
var trace = layer.getContext().getTrace();
|
||||
assert.equal(
|
||||
trace,
|
||||
'clearRect(0,0,578,200);save();transform(1,0,0,1,100,100);beginPath();moveTo(-100,-100);lineTo(-10,-100);arc(-10,-90,10,4.712,0,false);lineTo(0,-20);arc(-20,-20,20,0,1.571,false);lineTo(-70,0);arc(-70,-30,30,1.571,3.142,false);lineTo(-100,-100);arc(-100,-100,0,3.142,4.712,false);closePath();fillStyle=black;fill();restore();clearRect(0,0,578,200);save();transform(1,0,0,1,100,100);beginPath();moveTo(-100,-100);lineTo(-10,-100);arc(-10,-90,10,4.712,0,false);lineTo(0,-20);arc(-20,-20,20,0,1.571,false);lineTo(-70,0);arc(-70,-30,30,1.571,3.142,false);lineTo(-100,-100);arc(-100,-100,0,3.142,4.712,false);closePath();fillStyle=black;fill();restore();'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
Konva,
|
||||
cloneAndCompareLayer,
|
||||
assertAlmostEqual,
|
||||
createCanvas,
|
||||
compareLayerAndCanvas,
|
||||
} from './test-utils';
|
||||
|
||||
describe('RegularPolygon', function () {
|
||||
@ -206,4 +208,59 @@ describe('RegularPolygon', function () {
|
||||
assertAlmostEqual(box.width, 91.60254037844388);
|
||||
assertAlmostEqual(box.height, 80.00000000000003);
|
||||
});
|
||||
|
||||
// ======================================================
|
||||
it('limit corner radius', function () {
|
||||
var stage = addStage();
|
||||
var layer = new Konva.Layer();
|
||||
var sides = 5;
|
||||
var radius = 50;
|
||||
|
||||
var poly = new Konva.RegularPolygon({
|
||||
x: 100,
|
||||
y: 100,
|
||||
sides: sides,
|
||||
radius: radius,
|
||||
fill: 'black',
|
||||
cornerRadius: 25,
|
||||
});
|
||||
var resultCircleRadius = radius * Math.cos(Math.PI / sides);
|
||||
|
||||
layer.add(poly);
|
||||
stage.add(layer);
|
||||
|
||||
// corner radius creates perfect circle at 1/2 radius
|
||||
var canvas = createCanvas();
|
||||
var context = canvas.getContext('2d');
|
||||
context.beginPath();
|
||||
context.arc(100, 100, resultCircleRadius, 0, Math.PI * 2);
|
||||
context.fillStyle = 'black';
|
||||
context.fill();
|
||||
compareLayerAndCanvas(layer, canvas, 200);
|
||||
});
|
||||
|
||||
// ======================================================
|
||||
it('negative polygon radius with cornerRadius', function () {
|
||||
var stage = addStage();
|
||||
var layer = new Konva.Layer();
|
||||
|
||||
var poly = new Konva.RegularPolygon({
|
||||
x: 100,
|
||||
y: 100,
|
||||
sides: 5,
|
||||
radius: -100,
|
||||
fill: 'black',
|
||||
cornerRadius: 20,
|
||||
});
|
||||
|
||||
layer.add(poly);
|
||||
stage.add(layer);
|
||||
layer.draw();
|
||||
|
||||
var trace = layer.getContext().getTrace();
|
||||
assert.equal(
|
||||
trace,
|
||||
'clearRect(0,0,578,200);save();transform(1,0,0,1,100,100);beginPath();moveTo(26.18,80.979);arcTo(0,100,-26.18,80.979,32.361);lineTo(-68.925,49.923);arcTo(-95.106,30.902,-85.106,0.125,32.361);lineTo(-68.779,-50.125);arcTo(-58.779,-80.902,-26.418,-80.902,32.361);lineTo(26.418,-80.902);arcTo(58.779,-80.902,68.779,-50.125,32.361);lineTo(85.106,0.125);arcTo(95.106,30.902,68.925,49.923,32.361);closePath();fillStyle=black;fill();restore();clearRect(0,0,578,200);save();transform(1,0,0,1,100,100);beginPath();moveTo(26.18,80.979);arcTo(0,100,-26.18,80.979,32.361);lineTo(-68.925,49.923);arcTo(-95.106,30.902,-85.106,0.125,32.361);lineTo(-68.779,-50.125);arcTo(-58.779,-80.902,-26.418,-80.902,32.361);lineTo(26.418,-80.902);arcTo(58.779,-80.902,68.779,-50.125,32.361);lineTo(85.106,0.125);arcTo(95.106,30.902,68.925,49.923,32.361);closePath();fillStyle=black;fill();restore();'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1160,6 +1160,8 @@ describe('Shape', function () {
|
||||
// no we should hit the rect
|
||||
assert.equal(stage.getIntersection({ x: 5, y: 5 }), rect);
|
||||
|
||||
const oldWarn = console.warn;
|
||||
console.warn = function () {};
|
||||
rect.strokeHitEnabled(false);
|
||||
|
||||
assert.equal(rect.hitStrokeWidth(), 0);
|
||||
@ -1171,15 +1173,7 @@ describe('Shape', function () {
|
||||
rect.hitStrokeWidth(0);
|
||||
|
||||
assert.equal(rect.strokeHitEnabled(), false);
|
||||
|
||||
// var trace = layer
|
||||
// .getHitCanvas()
|
||||
// .getContext()
|
||||
// .getTrace(true);
|
||||
// assert.equal(
|
||||
// trace,
|
||||
// 'clearRect();save();transform();beginPath();rect();closePath();save();fillStyle;fill();restore();restore();'
|
||||
// );
|
||||
console.warn = oldWarn;
|
||||
});
|
||||
|
||||
it('enable hitStrokeWidth even if we have no stroke on scene', function () {
|
||||
|
@ -398,7 +398,7 @@ describe('Stage', function () {
|
||||
'17) getAllIntersections should return one shape'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 266, y: 114 })[0].getId(),
|
||||
stage.getAllIntersections({ x: 266, y: 114 })[0].id(),
|
||||
'greenCircle',
|
||||
'19) first intersection should be greenCircle'
|
||||
);
|
||||
@ -409,7 +409,7 @@ describe('Stage', function () {
|
||||
'18) getAllIntersections should return one shape'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 414, y: 115 })[0].getId(),
|
||||
stage.getAllIntersections({ x: 414, y: 115 })[0].id(),
|
||||
'redCircle',
|
||||
'20) first intersection should be redCircle'
|
||||
);
|
||||
@ -420,12 +420,12 @@ describe('Stage', function () {
|
||||
'1) getAllIntersections should return two shapes'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].getId(),
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].id(),
|
||||
'redCircle',
|
||||
'2) first intersection should be redCircle'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[1].getId(),
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[1].id(),
|
||||
'greenCircle',
|
||||
'3) second intersection should be greenCircle'
|
||||
);
|
||||
@ -440,7 +440,7 @@ describe('Stage', function () {
|
||||
'4) getAllIntersections should return one shape'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].getId(),
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].id(),
|
||||
'redCircle',
|
||||
'5) first intersection should be redCircle'
|
||||
);
|
||||
@ -455,12 +455,12 @@ describe('Stage', function () {
|
||||
'6) getAllIntersections should return two shapes'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].getId(),
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].id(),
|
||||
'redCircle',
|
||||
'7) first intersection should be redCircle'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[1].getId(),
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[1].id(),
|
||||
'greenCircle',
|
||||
'8) second intersection should be greenCircle'
|
||||
);
|
||||
@ -475,7 +475,7 @@ describe('Stage', function () {
|
||||
'9) getAllIntersections should return one shape'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].getId(),
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].id(),
|
||||
'greenCircle',
|
||||
'10) first intersection should be greenCircle'
|
||||
);
|
||||
@ -490,12 +490,12 @@ describe('Stage', function () {
|
||||
'11) getAllIntersections should return two shapes'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].getId(),
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[0].id(),
|
||||
'redCircle',
|
||||
'12) first intersection should be redCircle'
|
||||
);
|
||||
assert.equal(
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[1].getId(),
|
||||
stage.getAllIntersections({ x: 350, y: 118 })[1].id(),
|
||||
'greenCircle',
|
||||
'13) second intersection should be greenCircle'
|
||||
);
|
||||
@ -507,12 +507,12 @@ describe('Stage', function () {
|
||||
'14) getAllIntersections should return two shapes'
|
||||
);
|
||||
assert.equal(
|
||||
layer.getAllIntersections({ x: 350, y: 118 })[0].getId(),
|
||||
layer.getAllIntersections({ x: 350, y: 118 })[0].id(),
|
||||
'redCircle',
|
||||
'15) first intersection should be redCircle'
|
||||
);
|
||||
assert.equal(
|
||||
layer.getAllIntersections({ x: 350, y: 118 })[1].getId(),
|
||||
layer.getAllIntersections({ x: 350, y: 118 })[1].id(),
|
||||
'greenCircle',
|
||||
'16) second intersection should be greenCircle'
|
||||
);
|
||||
@ -551,6 +551,11 @@ describe('Stage', function () {
|
||||
// ======================================================
|
||||
it('Should not throw on clip for stage', function () {
|
||||
// no asserts, because we check throw
|
||||
const oldWarn = console.warn;
|
||||
let called = false;
|
||||
console.warn = function () {
|
||||
called = true;
|
||||
};
|
||||
var stage = addStage({
|
||||
clipFunc: function () {},
|
||||
});
|
||||
@ -566,6 +571,8 @@ describe('Stage', function () {
|
||||
|
||||
layer.add(text);
|
||||
stage.add(layer);
|
||||
console.warn = oldWarn;
|
||||
assert.equal(called, true);
|
||||
});
|
||||
|
||||
// ======================================================
|
||||
@ -1424,7 +1431,10 @@ describe('Stage', function () {
|
||||
mimeType: 'image/jpeg',
|
||||
quality: 0.5,
|
||||
});
|
||||
assert.isTrue(blob instanceof Blob && blob.type === 'image/jpeg', "can't change type of blob");
|
||||
assert.isTrue(
|
||||
blob instanceof Blob && blob.type === 'image/jpeg',
|
||||
"can't change type of blob"
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
assert.fail('error creating blob');
|
||||
|
@ -215,7 +215,7 @@ describe('Text', function () {
|
||||
var canvas = createCanvas();
|
||||
var context = canvas.getContext('2d');
|
||||
context.textBaseline = 'middle';
|
||||
context.letterSpacing = '10px';
|
||||
(context as any).letterSpacing = '10px';
|
||||
context.font = 'normal normal 50px Arial';
|
||||
context.fillStyle = 'darkgrey';
|
||||
context.fillText('आपकी दौड़ के लिए परफेक्ट जूते!', 10, 10 + 25);
|
||||
@ -1625,14 +1625,14 @@ describe('Text', function () {
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
var grd = ctx.createPattern(imageObj, 'repeat');
|
||||
grd.setTransform({
|
||||
(grd as any).setTransform({
|
||||
a: 1,
|
||||
b: 0,
|
||||
c: 0,
|
||||
d: 1,
|
||||
e: -50,
|
||||
f: 0,
|
||||
});
|
||||
} as any);
|
||||
ctx.fillStyle = grd;
|
||||
|
||||
ctx.fillText(text.text(), 0, 15);
|
||||
@ -1668,19 +1668,13 @@ describe('Text', function () {
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
var grd = ctx.createPattern(imageObj, 'repeat');
|
||||
const matrix =
|
||||
typeof DOMMatrix === 'undefined'
|
||||
? {
|
||||
a: 0.5, // Horizontal scaling. A value of 1 results in no scaling.
|
||||
b: 0, // Vertical skewing.
|
||||
c: 0, // Horizontal skewing.
|
||||
d: 0.5,
|
||||
e: 0, // Horizontal translation (moving).
|
||||
f: 0, // Vertical translation (moving).
|
||||
}
|
||||
: new DOMMatrix([0.5, 0, 0, 0.5, 0, 0]);
|
||||
// node-canvas expects its own DOMMatrix type; cast to any for cross-env test
|
||||
const matrix: any =
|
||||
typeof (global as any).DOMMatrix === 'undefined'
|
||||
? { a: 0.5, b: 0, c: 0, d: 0.5, e: 0, f: 0 }
|
||||
: new (global as any).DOMMatrix([0.5, 0, 0, 0.5, 0, 0]);
|
||||
|
||||
grd.setTransform(matrix);
|
||||
(grd as any).setTransform(matrix);
|
||||
|
||||
ctx.fillStyle = grd;
|
||||
|
||||
@ -1786,9 +1780,42 @@ describe('Text', function () {
|
||||
layer.draw();
|
||||
Konva._fixTextRendering = false;
|
||||
|
||||
const trace =
|
||||
'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);font=normal normal 100px Arial;textBaseline=alphabetic;textAlign=left;translate(0,0);save();fillStyle=black;fillText(hello,0,85);restore();restore();';
|
||||
if (Konva.Util['isSkia']) {
|
||||
const trace =
|
||||
'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);font=normal normal 100px Arial;textBaseline=alphabetic;textAlign=left;translate(0,0);save();fillStyle=black;fillText(hello,0,84.668);restore();restore();';
|
||||
assert.equal(layer.getContext().getTrace(), trace);
|
||||
} else {
|
||||
const trace =
|
||||
'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);font=normal normal 100px Arial;textBaseline=alphabetic;textAlign=left;translate(0,0);save();fillStyle=black;fillText(hello,0,85);restore();restore();';
|
||||
assert.equal(layer.getContext().getTrace(), trace);
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(layer.getContext().getTrace(), trace);
|
||||
it('charRenderFunc draws per character and can mutate context', function () {
|
||||
var stage = addStage();
|
||||
var layer = new Konva.Layer();
|
||||
stage.add(layer);
|
||||
|
||||
var text = new Konva.Text({
|
||||
x: 10,
|
||||
y: 10,
|
||||
text: 'AB',
|
||||
fontSize: 20,
|
||||
charRenderFunc: function (props) {
|
||||
if (props.index === 1) {
|
||||
// shift only the second character
|
||||
props.context.translate(0, 10);
|
||||
}
|
||||
},
|
||||
});
|
||||
layer.add(text);
|
||||
layer.draw();
|
||||
|
||||
var trace = layer.getContext().getTrace();
|
||||
|
||||
assert.equal(
|
||||
trace,
|
||||
'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,10,10);font=normal normal 20px Arial;textBaseline=middle;textAlign=left;translate(0,0);save();save();fillStyle=black;fillText(A,0,10);restore();save();translate(0,10);fillStyle=black;fillText(B,13.34,10);restore();restore();restore();'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -877,7 +877,7 @@ describe('TextPath', function () {
|
||||
|
||||
var rect = textpath.getClientRect();
|
||||
assert.equal(Math.round(rect.width), 299);
|
||||
assert.equal(Math.round(rect.height), 171);
|
||||
assert.equal(Math.abs(Math.round(rect.height) - 171) < 2, true);
|
||||
});
|
||||
|
||||
it.skip('check vertical text path', function () {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { assert } from 'chai';
|
||||
import { Transformer } from '../../src/shapes/Transformer';
|
||||
import type { Rect } from '../../src/shapes/Rect';
|
||||
|
||||
import {
|
||||
addStage,
|
||||
@ -4842,8 +4843,15 @@ describe('Transformer', function () {
|
||||
const tr = new Konva.Transformer();
|
||||
layer.add(tr);
|
||||
|
||||
const oldError = console.error;
|
||||
let called = false;
|
||||
console.error = function () {
|
||||
called = true;
|
||||
};
|
||||
tr.nodes([layer]);
|
||||
assert.equal(tr.nodes().length, 0);
|
||||
console.error = oldError;
|
||||
assert.equal(called, true);
|
||||
});
|
||||
|
||||
it('anchorStyleFunc', function () {
|
||||
@ -4866,7 +4874,7 @@ describe('Transformer', function () {
|
||||
});
|
||||
layer.add(tr);
|
||||
// manual check of correct position of node
|
||||
var handler = tr.findOne<Konva.Rect>('.bottom-right');
|
||||
var handler = tr.findOne<Rect>('.bottom-right');
|
||||
assert.equal(handler.fill(), 'white');
|
||||
|
||||
tr.anchorStyleFunc((anchor) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { createCanvas, Canvas } from 'canvas';
|
||||
import KonvaModule from '../../src/index';
|
||||
export const Konva = KonvaModule;
|
||||
|
||||
var TYPE_ARRAY = /\[object Array\]/i,
|
||||
TYPE_CANVAS = /\[object (Canvas|HTMLCanvasElement)\]/i,
|
||||
@ -6,15 +7,24 @@ var TYPE_ARRAY = /\[object Array\]/i,
|
||||
TYPE_CONTEXT = /\[object CanvasRenderingContext2D\]/i,
|
||||
TYPE_IMAGE = /\[object (Image|HTMLImageElement)\]/i,
|
||||
TYPE_IMAGE_DATA = /\[object ImageData\]/i,
|
||||
UNDEFINED = 'undefined',
|
||||
canvas = getCanvas(),
|
||||
context = canvas.getContext('2d');
|
||||
UNDEFINED = 'undefined';
|
||||
|
||||
// Creation
|
||||
function getCanvas(width?, height?) {
|
||||
return createCanvas(width, height);
|
||||
return Konva.Util.createCanvasElement();
|
||||
}
|
||||
|
||||
let singleCanvas;
|
||||
function getSingleCanvas() {
|
||||
if (!singleCanvas) {
|
||||
singleCanvas = getCanvas();
|
||||
}
|
||||
return singleCanvas;
|
||||
}
|
||||
|
||||
function getImageData(width, height) {
|
||||
const canvas = getSingleCanvas();
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.clearRect(0, 0, width, height);
|
||||
@ -26,7 +36,7 @@ function isImage(object) {
|
||||
return isType(object, TYPE_IMAGE);
|
||||
}
|
||||
function isCanvas(object) {
|
||||
return isType(object, TYPE_CANVAS) || object instanceof Canvas;
|
||||
return isType(object, TYPE_CANVAS);
|
||||
}
|
||||
function isContext(object) {
|
||||
return isType(object, TYPE_CONTEXT);
|
||||
@ -49,10 +59,7 @@ function isImageType(object) {
|
||||
);
|
||||
}
|
||||
function isType(object, type) {
|
||||
return (
|
||||
typeof object === 'object' &&
|
||||
!!Object.prototype.toString.apply(object).match(type)
|
||||
);
|
||||
return typeof object === 'object' && !!object.toString().match(type);
|
||||
}
|
||||
|
||||
// Type Conversion
|
||||
@ -61,6 +68,8 @@ function copyImageData(imageData) {
|
||||
width = imageData.width,
|
||||
data = imageData.data;
|
||||
|
||||
const canvas = getSingleCanvas();
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const newImageData = context.getImageData(0, 0, width, height);
|
||||
@ -89,6 +98,8 @@ function toImageData(object) {
|
||||
function toImageDataFromImage(image) {
|
||||
const height = image.height,
|
||||
width = image.width;
|
||||
const canvas = getSingleCanvas();
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.clearRect(0, 0, width, height);
|
||||
@ -189,8 +200,7 @@ function diffUnequal(a, b, options) {
|
||||
bData = b.data,
|
||||
cData = c.data,
|
||||
align = options && options.align;
|
||||
var rowOffset,
|
||||
columnOffset;
|
||||
var rowOffset, columnOffset;
|
||||
|
||||
for (let i = cData.length - 1; i > 0; i = i - 4) {
|
||||
cData[i] = 255;
|
||||
@ -239,10 +249,10 @@ function diffUnequal(a, b, options) {
|
||||
function checkType(...args) {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (!isImageType(args[i])) {
|
||||
throw {
|
||||
name: 'ImageTypeError',
|
||||
message: 'Submitted object was not an image.',
|
||||
};
|
||||
// throw {
|
||||
// name: 'ImageTypeError',
|
||||
// message: 'Submitted object was not an image.',
|
||||
// };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { assert } from 'chai';
|
||||
import KonvaModule from '../../src/index';
|
||||
import '../../src/index-node';
|
||||
|
||||
export const Konva = KonvaModule;
|
||||
|
||||
import * as canvas from 'canvas';
|
||||
// import * as canvas from 'canvas';
|
||||
|
||||
Konva.enableTrace = true;
|
||||
Konva.showWarnings = true;
|
||||
@ -84,12 +83,14 @@ export function loadImage(url, callback) {
|
||||
url = (document.getElementById(url) as HTMLImageElement).src;
|
||||
}
|
||||
|
||||
return canvas
|
||||
.loadImage(url)
|
||||
.then(callback)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
const image = Konva.Util.createImageElement();
|
||||
image.onload = () => {
|
||||
callback(image);
|
||||
};
|
||||
image.onerror = (e) => {
|
||||
console.error('Error loading image', url, e);
|
||||
};
|
||||
image.src = url;
|
||||
}
|
||||
|
||||
export function getPixelRatio() {
|
||||
@ -171,7 +172,7 @@ export function compareLayers(layer1: Layer, layer2: Layer, tol?, secondTol?) {
|
||||
}
|
||||
|
||||
export function createCanvas() {
|
||||
var node = canvas.createCanvas(300, 300);
|
||||
var node = Konva.Util.createCanvasElement();
|
||||
node.width = 578 * Konva.pixelRatio;
|
||||
node.height = 200 * Konva.pixelRatio;
|
||||
node.getContext('2d').scale(Konva.pixelRatio, Konva.pixelRatio);
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"module": "CommonJS",
|
||||
"module": "ESNext",
|
||||
"target": "ES2018",
|
||||
// "sourceMap": true,
|
||||
"noEmitOnError": true,
|
||||
|
Loading…
Reference in New Issue
Block a user