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.
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)
- Fix text rendering when ellipses are used

View File

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

View File

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

View File

@ -693,39 +693,32 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
evtStr: 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) {
return this._delegate.apply(this, arguments as any);
}
let events = (evtStr as string).split(SPACE),
len = events.length,
n,
event,
parts,
baseEvent,
name;
const events = (evtStr as string).split(SPACE);
/*
* loop through types and attach event listeners to
* each one. eg. 'click mouseover.namespace mouseout'
* will create three event bindings
*/
for (n = 0; n < len; n++) {
event = events[n];
parts = event.split('.');
baseEvent = parts[0];
name = parts[1] || '';
for (let n = 0; n < events.length; n++) {
const event = events[n];
const parts = event.split('.');
const baseEvent = parts[0];
const name = parts[1] || '';
// create events array if it doesn't exist
if (!this.eventListeners[baseEvent]) {
this.eventListeners[baseEvent] = [];
}
this.eventListeners[baseEvent].push({
name: name,
handler: handler,
});
this.eventListeners[baseEvent].push({ name , handler });
}
return this;
@ -2208,7 +2201,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* node.addName('selected');
* node.name(); // return 'red selected'
*/
addName(name) {
addName(name: string) {
if (!this.hasName(name)) {
const oldName = this.name();
const newName = oldName ? oldName + ' ' + name : name;
@ -2380,7 +2373,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
const topListeners = this._getProtoListeners(eventType);
if (topListeners) {
for (var i = 0; i < topListeners.length; i++) {
for (let i = 0; i < topListeners.length; i++) {
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
const selfListeners = this.eventListeners[eventType];
if (selfListeners) {
for (var i = 0; i < selfListeners.length; i++) {
for (let i = 0; i < selfListeners.length; i++) {
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
const layer = this.getLayer();
let canvas = can || layer!.getCanvas(),
const canvas = can || layer!.getCanvas(),
context = canvas.getContext() as SceneContext,
cachedCanvas = this._getCanvasCache(),
drawFunc = this.getSceneFunc(),
hasShadow = this.hasShadow(),
stage,
bufferContext;
hasShadow = this.hasShadow();
let stage, bufferContext;
const skipBuffer = canvas.isCache;
const cachingSelf = top === this;
@ -633,7 +632,7 @@ export class Shape<
bufferContext.save();
bufferContext._applyLineJoin(this);
// 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]);
drawFunc.call(this, bufferContext, this);
@ -651,7 +650,7 @@ export class Shape<
context._applyLineJoin(this);
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._applyOpacity(this);
context._applyGlobalCompositeOperation(this);

View File

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

View File

@ -200,8 +200,7 @@ export class Tween {
nodeId = node._id,
easing = config.easing || Easings.Linear,
yoyo = !!config.yoyo;
let duration,
key;
let duration, key;
if (typeof config.duration === 'undefined') {
duration = 0.3;
@ -268,11 +267,7 @@ export class Tween {
_addAttr(key, end) {
const node = this.node,
nodeId = node._id;
let diff,
len,
trueEnd,
trueStart,
endRGBA;
let diff, len, trueEnd, trueStart, endRGBA;
// remove conflict from tween map if it exists
const tweenId = Tween.tweens[nodeId][key];
@ -352,14 +347,7 @@ export class Tween {
_tweenFunc(i) {
const node = this.node,
attrs = Tween.attrs[node._id][this._id];
let key,
attr,
start,
diff,
newVal,
n,
len,
end;
let key, attr, start, diff, newVal, n, len, end;
for (key in attrs) {
attr = attrs[key];
@ -528,11 +516,26 @@ export class Tween {
this.pause();
// Clean up animation
if (this.anim) {
this.anim.stop();
}
// Clean up tween entries
for (const key in attrs) {
delete Tween.tweens[nodeId][key];
}
// Clean up attrs entry
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){
* // image is Konva.Image instance
* layer.add(image);
* layer.draw();
* });
*/
static fromURL(

View File

@ -102,26 +102,24 @@ export class Line<
}
_sceneFunc(context: Context) {
let points = this.points(),
const points = this.points(),
length = points.length,
tension = this.tension(),
closed = this.closed(),
bezier = this.bezier(),
tp,
len,
n;
bezier = this.bezier();
if (!length) {
return;
}
let n = 0;
context.beginPath();
context.moveTo(points[0], points[1]);
// tension
if (tension !== 0 && length > 4) {
tp = this.getTensionPoints();
len = tp.length;
const tp = this.getTensionPoints();
const len = tp.length;
n = closed ? 0 : 4;
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.textDecoration] can be line-through, underline or empty string. Default is empty string.
* @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 {Number} [config.padding]
* @param {Number} [config.lineHeight] default is 1
@ -885,6 +885,8 @@ Factory.addGetterSetter(Text, 'padding', 0, getNumberValidator());
*
* // align text to right
* text.align('right');
*
* // justify text
*/
Factory.addGetterSetter(Text, 'align', LEFT);

View File

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

View File

@ -1550,6 +1550,53 @@ describe('Shape', function () {
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 () {
var stage = addStage();

View File

@ -98,8 +98,8 @@ describe('Tween', function () {
tween.destroy();
assert.equal(Konva.Tween.tweens[circle._id].x, undefined);
assert.equal(Konva.Tween.attrs[circle._id][tween._id], undefined);
assert.equal(Konva.Tween.tweens[circle._id], undefined);
assert.equal(Konva.Tween.attrs[circle._id], undefined);
});
// ======================================================
@ -238,7 +238,7 @@ describe('Tween', function () {
duration: 0.1,
onFinish: function () {
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();
},
});
@ -305,16 +305,10 @@ describe('Tween', function () {
points: [100, 100, 200, 100, 200, 200, 100, 200],
duration: 0.1,
onFinish: function () {
assert.deepEqual(line.points(), [
100,
100,
200,
100,
200,
200,
100,
200,
]);
assert.deepEqual(
line.points(),
[100, 100, 200, 100, 200, 200, 100, 200]
);
done();
},
});
@ -364,16 +358,10 @@ describe('Tween', function () {
tween.reverse();
},
onReset: function () {
assert.deepEqual(line.points(), [
100,
100,
200,
100,
200,
200,
100,
200,
]);
assert.deepEqual(
line.points(),
[100, 100, 200, 100, 200, 200, 100, 200]
);
done();
},
});