Merge branch 'konvajs:master' into master

This commit is contained in:
iaosee 2025-04-01 15:51:04 +08:00 committed by GitHub
commit e793c8270a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 111 additions and 78 deletions

View File

@ -3,6 +3,11 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## 9.3.21 (not released)
- Fixed memory leaks on Tween destroy
- Fixed incorrect export of stage/layer when internal nodes used buffer canvas for rendering
## 9.3.20 (2025-03-20) ## 9.3.20 (2025-03-20)
- Fix text rendering when ellipses are used - Fix text rendering when ellipses are used

View File

@ -120,8 +120,6 @@ export abstract class Container<
* layer.add(shape1, shape2, shape3); * layer.add(shape1, shape2, shape3);
* // empty arrays are accepted, though each individual child must be defined * // empty arrays are accepted, though each individual child must be defined
* layer.add(...shapes); * layer.add(...shapes);
* // remember to redraw layer if you changed something
* layer.draw();
*/ */
add(...children: ChildType[]) { add(...children: ChildType[]) {
if (children.length === 0) { if (children.length === 0) {

View File

@ -385,7 +385,7 @@ export class Layer extends Container<Group | Shape> {
// empty pixel // empty pixel
return {}; return {};
} }
drawScene(can?: SceneCanvas, top?: Node) { drawScene(can?: SceneCanvas, top?: Node, bufferCanvas?: SceneCanvas) {
const layer = this.getLayer(), const layer = this.getLayer(),
canvas = can || (layer && layer.getCanvas()); canvas = can || (layer && layer.getCanvas());
@ -397,7 +397,7 @@ export class Layer extends Container<Group | Shape> {
canvas.getContext().clear(); canvas.getContext().clear();
} }
Container.prototype.drawScene.call(this, canvas, top); Container.prototype.drawScene.call(this, canvas, top, bufferCanvas);
this._fire(DRAW, { this._fire(DRAW, {
node: this, node: this,

View File

@ -693,39 +693,32 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
evtStr: K, evtStr: K,
handler: KonvaEventListener<this, NodeEventMap[K]> handler: KonvaEventListener<this, NodeEventMap[K]>
) { ) {
this._cache && this._cache.delete(ALL_LISTENERS); if (this._cache) {
this._cache.delete(ALL_LISTENERS);
}
if (arguments.length === 3) { if (arguments.length === 3) {
return this._delegate.apply(this, arguments as any); return this._delegate.apply(this, arguments as any);
} }
let events = (evtStr as string).split(SPACE), const events = (evtStr as string).split(SPACE);
len = events.length,
n,
event,
parts,
baseEvent,
name;
/* /*
* loop through types and attach event listeners to * loop through types and attach event listeners to
* each one. eg. 'click mouseover.namespace mouseout' * each one. eg. 'click mouseover.namespace mouseout'
* will create three event bindings * will create three event bindings
*/ */
for (n = 0; n < len; n++) { for (let n = 0; n < events.length; n++) {
event = events[n]; const event = events[n];
parts = event.split('.'); const parts = event.split('.');
baseEvent = parts[0]; const baseEvent = parts[0];
name = parts[1] || ''; const name = parts[1] || '';
// create events array if it doesn't exist // create events array if it doesn't exist
if (!this.eventListeners[baseEvent]) { if (!this.eventListeners[baseEvent]) {
this.eventListeners[baseEvent] = []; this.eventListeners[baseEvent] = [];
} }
this.eventListeners[baseEvent].push({ this.eventListeners[baseEvent].push({ name , handler });
name: name,
handler: handler,
});
} }
return this; return this;
@ -2208,7 +2201,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* node.addName('selected'); * node.addName('selected');
* node.name(); // return 'red selected' * node.name(); // return 'red selected'
*/ */
addName(name) { addName(name: string) {
if (!this.hasName(name)) { if (!this.hasName(name)) {
const oldName = this.name(); const oldName = this.name();
const newName = oldName ? oldName + ' ' + name : name; const newName = oldName ? oldName + ' ' + name : name;
@ -2380,7 +2373,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
const topListeners = this._getProtoListeners(eventType); const topListeners = this._getProtoListeners(eventType);
if (topListeners) { if (topListeners) {
for (var i = 0; i < topListeners.length; i++) { for (let i = 0; i < topListeners.length; i++) {
topListeners[i].handler.call(this, evt); topListeners[i].handler.call(this, evt);
} }
} }
@ -2389,7 +2382,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
// because events can be added/removed while firing // because events can be added/removed while firing
const selfListeners = this.eventListeners[eventType]; const selfListeners = this.eventListeners[eventType];
if (selfListeners) { if (selfListeners) {
for (var i = 0; i < selfListeners.length; i++) { for (let i = 0; i < selfListeners.length; i++) {
selfListeners[i].handler.call(this, evt); selfListeners[i].handler.call(this, evt);
} }
} }

View File

@ -594,13 +594,12 @@ export class Shape<
// 3 - when node is cached and we need to draw it into layer // 3 - when node is cached and we need to draw it into layer
const layer = this.getLayer(); const layer = this.getLayer();
let canvas = can || layer!.getCanvas(), const canvas = can || layer!.getCanvas(),
context = canvas.getContext() as SceneContext, context = canvas.getContext() as SceneContext,
cachedCanvas = this._getCanvasCache(), cachedCanvas = this._getCanvasCache(),
drawFunc = this.getSceneFunc(), drawFunc = this.getSceneFunc(),
hasShadow = this.hasShadow(), hasShadow = this.hasShadow();
stage, let stage, bufferContext;
bufferContext;
const skipBuffer = canvas.isCache; const skipBuffer = canvas.isCache;
const cachingSelf = top === this; const cachingSelf = top === this;
@ -633,7 +632,7 @@ export class Shape<
bufferContext.save(); bufferContext.save();
bufferContext._applyLineJoin(this); bufferContext._applyLineJoin(this);
// layer might be undefined if we are using cache before adding to layer // layer might be undefined if we are using cache before adding to layer
var o = this.getAbsoluteTransform(top).getMatrix(); const o = this.getAbsoluteTransform(top).getMatrix();
bufferContext.transform(o[0], o[1], o[2], o[3], o[4], o[5]); bufferContext.transform(o[0], o[1], o[2], o[3], o[4], o[5]);
drawFunc.call(this, bufferContext, this); drawFunc.call(this, bufferContext, this);
@ -651,7 +650,7 @@ export class Shape<
context._applyLineJoin(this); context._applyLineJoin(this);
if (!cachingSelf) { if (!cachingSelf) {
var o = this.getAbsoluteTransform(top).getMatrix(); const o = this.getAbsoluteTransform(top).getMatrix();
context.transform(o[0], o[1], o[2], o[3], o[4], o[5]); context.transform(o[0], o[1], o[2], o[3], o[4], o[5]);
context._applyOpacity(this); context._applyOpacity(this);
context._applyGlobalCompositeOperation(this); context._applyGlobalCompositeOperation(this);

View File

@ -214,11 +214,11 @@ export class Stage extends Container<Layer> {
*/ */
setContainer(container) { setContainer(container) {
if (typeof container === STRING) { if (typeof container === STRING) {
let id;
if (container.charAt(0) === '.') { if (container.charAt(0) === '.') {
const className = container.slice(1); const className = container.slice(1);
container = document.getElementsByClassName(className)[0]; container = document.getElementsByClassName(className)[0];
} else { } else {
var id;
if (container.charAt(0) !== '#') { if (container.charAt(0) !== '#') {
id = container; id = container;
} else { } else {

View File

@ -200,8 +200,7 @@ export class Tween {
nodeId = node._id, nodeId = node._id,
easing = config.easing || Easings.Linear, easing = config.easing || Easings.Linear,
yoyo = !!config.yoyo; yoyo = !!config.yoyo;
let duration, let duration, key;
key;
if (typeof config.duration === 'undefined') { if (typeof config.duration === 'undefined') {
duration = 0.3; duration = 0.3;
@ -268,11 +267,7 @@ export class Tween {
_addAttr(key, end) { _addAttr(key, end) {
const node = this.node, const node = this.node,
nodeId = node._id; nodeId = node._id;
let diff, let diff, len, trueEnd, trueStart, endRGBA;
len,
trueEnd,
trueStart,
endRGBA;
// remove conflict from tween map if it exists // remove conflict from tween map if it exists
const tweenId = Tween.tweens[nodeId][key]; const tweenId = Tween.tweens[nodeId][key];
@ -352,14 +347,7 @@ export class Tween {
_tweenFunc(i) { _tweenFunc(i) {
const node = this.node, const node = this.node,
attrs = Tween.attrs[node._id][this._id]; attrs = Tween.attrs[node._id][this._id];
let key, let key, attr, start, diff, newVal, n, len, end;
attr,
start,
diff,
newVal,
n,
len,
end;
for (key in attrs) { for (key in attrs) {
attr = attrs[key]; attr = attrs[key];
@ -528,11 +516,26 @@ export class Tween {
this.pause(); this.pause();
// Clean up animation
if (this.anim) {
this.anim.stop();
}
// Clean up tween entries
for (const key in attrs) { for (const key in attrs) {
delete Tween.tweens[nodeId][key]; delete Tween.tweens[nodeId][key];
} }
// Clean up attrs entry
delete Tween.attrs[nodeId][thisId]; delete Tween.attrs[nodeId][thisId];
// Clean up parent objects if empty
if (Object.keys(Tween.tweens[nodeId]).length === 0) {
delete Tween.tweens[nodeId];
}
if (Object.keys(Tween.attrs[nodeId]).length === 0) {
delete Tween.attrs[nodeId];
}
} }
} }

View File

@ -163,7 +163,6 @@ export class Image extends Shape<ImageConfig> {
* Konva.Image.fromURL(imageURL, function(image){ * Konva.Image.fromURL(imageURL, function(image){
* // image is Konva.Image instance * // image is Konva.Image instance
* layer.add(image); * layer.add(image);
* layer.draw();
* }); * });
*/ */
static fromURL( static fromURL(

View File

@ -102,26 +102,24 @@ export class Line<
} }
_sceneFunc(context: Context) { _sceneFunc(context: Context) {
let points = this.points(), const points = this.points(),
length = points.length, length = points.length,
tension = this.tension(), tension = this.tension(),
closed = this.closed(), closed = this.closed(),
bezier = this.bezier(), bezier = this.bezier();
tp,
len,
n;
if (!length) { if (!length) {
return; return;
} }
let n = 0;
context.beginPath(); context.beginPath();
context.moveTo(points[0], points[1]); context.moveTo(points[0], points[1]);
// tension // tension
if (tension !== 0 && length > 4) { if (tension !== 0 && length > 4) {
tp = this.getTensionPoints(); const tp = this.getTensionPoints();
len = tp.length; const len = tp.length;
n = closed ? 0 : 4; n = closed ? 0 : 4;
if (!closed) { if (!closed) {

View File

@ -171,7 +171,7 @@ function checkDefaultFill(config?: TextConfig) {
* @param {String} [config.fontVariant] can be normal or small-caps. Default is normal * @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 {String} [config.textDecoration] can be line-through, underline or empty string. Default is empty string.
* @param {String} config.text * @param {String} config.text
* @param {String} [config.align] can be left, center, or right * @param {String} [config.align] can be left, center, right or justify
* @param {String} [config.verticalAlign] can be top, middle or bottom * @param {String} [config.verticalAlign] can be top, middle or bottom
* @param {Number} [config.padding] * @param {Number} [config.padding]
* @param {Number} [config.lineHeight] default is 1 * @param {Number} [config.lineHeight] default is 1
@ -885,6 +885,8 @@ Factory.addGetterSetter(Text, 'padding', 0, getNumberValidator());
* *
* // align text to right * // align text to right
* text.align('right'); * text.align('right');
*
* // justify text
*/ */
Factory.addGetterSetter(Text, 'align', LEFT); Factory.addGetterSetter(Text, 'align', LEFT);

View File

@ -11,6 +11,7 @@
<script type="module"> <script type="module">
// CORE // CORE
import './unit/Animation-test.ts'; import './unit/Animation-test.ts';
import './unit/Tween-test.ts';
import './unit/Canvas-test.ts'; import './unit/Canvas-test.ts';
import './unit/Container-test.ts'; import './unit/Container-test.ts';
import './unit/Context-test.ts'; import './unit/Context-test.ts';

View File

@ -1550,6 +1550,53 @@ describe('Shape', function () {
compareCanvases(canvas2, canvas1, 240, 110); compareCanvases(canvas2, canvas1, 240, 110);
}); });
it('export stage when buffer canvas is for line', async function () {
var stage = addStage();
var layer = new Konva.Layer();
stage.add(layer);
const group = new Konva.Group({
id: 'group01',
draggable: false,
opacity: 0.99,
});
layer.add(group);
const arrow = new Konva.Arrow({
x: 0,
y: 0,
points: [50, 25, 200, 25, 200, 225, 400, 225],
stroke: 'purple',
fill: 'purple',
strokeWidth: 4,
pointerAtEnding: true,
bezier: true,
});
group.add(arrow);
const bounds = layer.getClientRect({ relativeTo: stage });
const pos = stage.getPosition();
const canvas1 = layer.toCanvas({
pixelRatio: 1,
x: bounds.x + pos.x,
y: bounds.y + pos.y,
width: bounds.width,
height: bounds.height,
});
group.opacity(1);
const canvas2 = layer.toCanvas({
pixelRatio: 1,
x: bounds.x + pos.x,
y: bounds.y + pos.y,
width: bounds.width,
height: bounds.height,
});
compareCanvases(canvas1, canvas2, 240, 110);
});
// ====================================================== // ======================================================
it('optional disable shadow for stroke', function () { it('optional disable shadow for stroke', function () {
var stage = addStage(); var stage = addStage();

View File

@ -98,8 +98,8 @@ describe('Tween', function () {
tween.destroy(); tween.destroy();
assert.equal(Konva.Tween.tweens[circle._id].x, undefined); assert.equal(Konva.Tween.tweens[circle._id], undefined);
assert.equal(Konva.Tween.attrs[circle._id][tween._id], undefined); assert.equal(Konva.Tween.attrs[circle._id], undefined);
}); });
// ====================================================== // ======================================================
@ -238,7 +238,7 @@ describe('Tween', function () {
duration: 0.1, duration: 0.1,
onFinish: function () { onFinish: function () {
assert.equal(circle.x(), stage.width() / 2); assert.equal(circle.x(), stage.width() / 2);
assert.equal(Object.keys(Konva.Tween.attrs[circle._id]).length, 0); assert.equal(Konva.Tween.attrs[circle._id], undefined);
done(); done();
}, },
}); });
@ -305,16 +305,10 @@ describe('Tween', function () {
points: [100, 100, 200, 100, 200, 200, 100, 200], points: [100, 100, 200, 100, 200, 200, 100, 200],
duration: 0.1, duration: 0.1,
onFinish: function () { onFinish: function () {
assert.deepEqual(line.points(), [ assert.deepEqual(
100, line.points(),
100, [100, 100, 200, 100, 200, 200, 100, 200]
200, );
100,
200,
200,
100,
200,
]);
done(); done();
}, },
}); });
@ -364,16 +358,10 @@ describe('Tween', function () {
tween.reverse(); tween.reverse();
}, },
onReset: function () { onReset: function () {
assert.deepEqual(line.points(), [ assert.deepEqual(
100, line.points(),
100, [100, 100, 200, 100, 200, 200, 100, 200]
200, );
100,
200,
200,
100,
200,
]);
done(); done();
}, },
}); });