z-order support for konva

This commit is contained in:
SkyeBlep 2023-01-10 22:36:08 -08:00
parent 6e5aff393f
commit c0efb7d1a1
6 changed files with 310 additions and 0 deletions

View File

@ -0,0 +1,141 @@
import { Util } from './Util';
import { Container, ContainerConfig } from './Container';
import { _registerNode } from './Global';
import { Node } from './Node';
import { Shape } from './Shape';
import { Group, GroupConfig } from './Group';
import { HitCanvas, SceneCanvas } from './Canvas';
export interface AbsoluteRenderOrderGroupConfig extends GroupConfig { }
/**
* AbsoluteRenderOrderGroup constructor. AbsoluteRenderOrderGroup is a special kind of Group that renders all of its
* children and subchildren recursively, in the order of the z-order parameter.
*
* In order to maintain masking behavior, cached groups are respected and treated as a single object at the group's
* designated z-order.
* @constructor
* @memberof Konva
* @augments Konva.Container
* @param {Object} config
* @@nodeParams
* @@containerParams
* @example
* var group = new Konva.Group();
*/
export class AbsoluteRenderOrderGroup extends Group {
_validateAdd(child: Node) {
var type = child.getType();
if (type !== 'Group' && type !== 'Shape') {
Util.throw('You may only add groups and shapes to groups.');
}
}
_drawChildren(drawMethod, canvas, top) {
var context = canvas && canvas.getContext(),
clipWidth = this.clipWidth(),
clipHeight = this.clipHeight(),
clipFunc = this.clipFunc(),
hasClip = (clipWidth && clipHeight) || clipFunc;
const selfCache = top === this;
if (hasClip) {
context.save();
var transform = this.getAbsoluteTransform(top);
var m = transform.getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
context.beginPath();
if (clipFunc) {
clipFunc.call(this, context, this);
} else {
var clipX = this.clipX();
var clipY = this.clipY();
context.rect(clipX, clipY, clipWidth, clipHeight);
}
context.clip();
m = transform.copy().invert().getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
var hasComposition =
!selfCache &&
this.globalCompositeOperation() !== 'source-over' &&
drawMethod === 'drawScene';
if (hasComposition) {
context.save();
context._applyGlobalCompositeOperation(this);
}
// AbsoluteRenderOrderGroup differs from the standard container by ordering its children itself, instead of
// letting children call each.
let unorderedChildren = new Map<number, Node[]>();
// Add all children recursively to orderedChildren
this.addChildrenRecursivelyToMap(this, unorderedChildren);
// Sort children by zOrder
// ( https://stackoverflow.com/questions/31158902/is-it-possible-to-sort-a-es6-map-object )
let orderedChildren = new Map<number, Node[]>([...unorderedChildren].sort(
(a, b) =>
{
if (a[0] > b[0])
return 1;
else if (a[0] == b[0])
return 0;
else
return -1;
}));
// Draw children in zOrder
for (const [zOrder, nodeArray] of orderedChildren)
{
//console.log("Drawing " + zOrder);
for (const node of nodeArray)
{
//console.log(node)
node[drawMethod](canvas, top);
//console.log(node.toString())
}
}
if (hasComposition) {
context.restore();
}
if (hasClip) {
context.restore();
}
}
private addChildrenRecursivelyToMap(node:Node, orderedChildren:Map<number, Array<Node>>):void
{
let rootNode:AbsoluteRenderOrderGroup = this;
if (node == rootNode || // the AbsoluteRenderOrderGroup itself will always render using z-order logic, even if cached
(node instanceof Group && !node.isCached())) { // However, cached subgroups are considered to be just a regular object (this protects masking)
(node as Group).children?.forEach(function (child) {
rootNode.addChildrenRecursivelyToMap(child, orderedChildren);
});
} else {
// Is a leaf / don't descend farther -- this can be added to children map
let zOrder:number = node.zOrder();
if (!orderedChildren.has(zOrder))
{
orderedChildren.set(zOrder, new Array<Node>());
}
orderedChildren.get(zOrder).push(node); // I'd much prefer the [] syntax for clarity, but seems TS/JS doesn't seem to support it, ugh.
}
}
}
//AbsoluteRenderOrderGroup.prototype.nodeType = 'AbsoluteRenderOrderGroup';
// Node type appears to be either Shape or Group, so it doesn't seem like this should be set if we mirror how Shapes work?
AbsoluteRenderOrderGroup.prototype.className = 'AbsoluteRenderOrderGroup';
_registerNode(AbsoluteRenderOrderGroup);

View File

@ -72,6 +72,7 @@ export interface NodeConfig {
preventDefault?: boolean; preventDefault?: boolean;
globalCompositeOperation?: globalCompositeOperationType; globalCompositeOperation?: globalCompositeOperationType;
filters?: Array<Filter>; filters?: Array<Filter>;
zOrder?: number;
} }
// CONSTANTS // CONSTANTS
@ -2618,6 +2619,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
rotation: GetSet<number, this>; rotation: GetSet<number, this>;
zIndex: GetSet<number, this>; zIndex: GetSet<number, this>;
zOrder: GetSet<number, this>;
scale: GetSet<Vector2d | undefined, this>; scale: GetSet<Vector2d | undefined, this>;
scaleX: GetSet<number, this>; scaleX: GetSet<number, this>;
@ -3278,6 +3280,25 @@ addGetterSetter(Node, 'dragBoundFunc');
*/ */
addGetterSetter(Node, 'draggable', false, getBooleanValidator()); addGetterSetter(Node, 'draggable', false, getBooleanValidator());
/**
* get/set z-order position. Note that z-order is not the same as z-index. Z-order is used with the
* AbsoluteRenderOrderContainer to recursively render child objects in an absolute z-order. This field will otherwise
* be ignored. Alternatively, you can use the z-index features to move an object within a particular group, relative to
* the other nodes in the group.
* @name Konva.Node#zOrder
* @method
* @param {Number} zOrder
* @returns {Object}
* @example
* // get z-order
* var z = node.zOrder();
*
* // set z-order
* node.zOrder(5);
*/
addGetterSetter(Node, 'zOrder', 0, getNumberValidator());
Factory.backCompat(Node, { Factory.backCompat(Node, {
rotateDeg: 'rotate', rotateDeg: 'rotate',
setRotationDeg: 'setRotation', setRotationDeg: 'setRotation',

View File

@ -11,6 +11,7 @@ import { Layer } from './Layer';
import { FastLayer } from './FastLayer'; import { FastLayer } from './FastLayer';
import { Group } from './Group'; import { Group } from './Group';
import { AbsoluteRenderOrderGroup } from './AbsoluteRenderOrderGroup';
import { DD } from './DragAndDrop'; import { DD } from './DragAndDrop';
@ -32,6 +33,7 @@ export const Konva = Util._assign(Global, {
Layer, Layer,
FastLayer, FastLayer,
Group, Group,
AbsoluteRenderOrderGroup,
DD, DD,
Shape, Shape,
shapes, shapes,

View File

@ -85,6 +85,10 @@ declare namespace Konva {
export type Group = import('./Group').Group; export type Group = import('./Group').Group;
export type GroupConfig = import('./Group').GroupConfig; export type GroupConfig = import('./Group').GroupConfig;
export const AbsoluteRenderOrderGroup: typeof import('./AbsoluteRenderOrderGroup').AbsoluteRenderOrderGroup;
export type AbsoluteRenderOrderGroup = import('./AbsoluteRenderOrderGroup').AbsoluteRenderOrderGroup;
export type AbsoluteRenderOrderGroupConfig = import('./AbsoluteRenderOrderGroup').AbsoluteRenderOrderGroupConfig;
export const DD: typeof import('./DragAndDrop').DD; export const DD: typeof import('./DragAndDrop').DD;
export const Shape: typeof import('./Shape').Shape; export const Shape: typeof import('./Shape').Shape;

View File

@ -17,6 +17,7 @@
import './unit/DragAndDrop-test.ts'; import './unit/DragAndDrop-test.ts';
import './unit/Global-test.ts'; import './unit/Global-test.ts';
import './unit/Group-test.ts'; import './unit/Group-test.ts';
import './unit/AbsoluteRenderOrderGroup-test.ts';
import './unit/Layer-test.ts'; import './unit/Layer-test.ts';
import './unit/Util-test.ts'; import './unit/Util-test.ts';
import './unit/Stage-test.ts'; import './unit/Stage-test.ts';

View File

@ -0,0 +1,141 @@
import { assert } from 'chai';
import { addStage, isNode, Konva } from './test-utils';
describe('AbsoluteRenderOrderGroup', function () {
// ======================================================
it('check render order -- simple, no subgroups', function () {
var stage = addStage();
const layer = new Konva.Layer();
stage.add(layer);
// This will test that AbsoluteRenderOrderGroup renders based on z-order, not z-index
const absoluteRenderOrderGroupTest = new Konva.AbsoluteRenderOrderGroup({
x: 0,
y: 0
});
layer.add(absoluteRenderOrderGroupTest);
const redRect = new Konva.Rect({
x: 0,
y: 0,
width: 100,
height: 100,
fill: 'red',
zOrder: 10 // on top
});
absoluteRenderOrderGroupTest.add(redRect);
const blueRect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'blue',
zOrder: 0 // on bottom
});
absoluteRenderOrderGroupTest.add(blueRect);
// Set z-order to be deliberately different from z-index
redRect.moveToBottom();
layer.draw();
// Check pixel color -- should be Red if AbsoluteRenderOrderGroup is respecting the ordering
// (More info: https://stackoverflow.com/questions/667045/get-a-pixel-from-html-canvas )
let context = layer.canvas.getContext();
let imageData = context.getImageData(55, 55, 1, 1); // this is an intersecting pixel location between red & blue
let red = imageData.data[0];
assert.equal(red, 255, "Did not find red pixel, ordering is possibly incorrect. Red amount found was: " + red);
});
it('Test AbsoluteRenderOrderGroup correctly interleaves the ordering of subgroups', function () {
var stage = addStage();
const layer = new Konva.Layer();
stage.add(layer);
const absoluteRenderOrderGroupTest = new Konva.AbsoluteRenderOrderGroup({
x: 0,
y: 0
});
layer.add(absoluteRenderOrderGroupTest);
const group1 = new Konva.Group({
x: 0,
y: 0
});
const group2 = new Konva.Group({
x: 25,
y: 25,
});
absoluteRenderOrderGroupTest.add(group1);
absoluteRenderOrderGroupTest.add(group2);
// Add shapes that interleave between the groups
// It should render as brightest red -> middle reds -> black
const rect1 = new Konva.Rect({
x: 0,
y: 0,
width: 100,
height: 100,
fill: '#FF0000',
zOrder: 10 // on top
});
group1.add(rect1);
const rect2 = new Konva.Rect({
x: 0,
y: 0,
width: 100,
height: 100,
fill: '#AA0000',
zOrder: 7
});
group2.add(rect2);
const rect3 = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: '#770000',
zOrder: 5
});
group1.add(rect3);
const rect4 = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'black',
zOrder: 0 // on bottom
});
group2.add(rect4);
layer.draw();
// Check pixel color -- should be Red if AbsoluteRenderOrderGroup is respecting the ordering
// (More info: https://stackoverflow.com/questions/667045/get-a-pixel-from-html-canvas )
let context = layer.canvas.getContext();
let widthCapture = 200;
let heightCapture = 200;
let imageData = context.getImageData(0, 0, widthCapture, heightCapture);
let red1 = imageData.data[(80+widthCapture*80)*4];
let red2 = imageData.data[(115+widthCapture*115)*4];
let red3 = imageData.data[(140+widthCapture*140)*4];
let black = imageData.data[(160+widthCapture*160)*4];
assert.equal(red1, 255, "Did not find correct amount of red in bright red pixel for test 1, ordering is possibly incorrect. Red amount found was: " + red1);
assert.equal(red2, 170, "Did not find correct amount of red in medium bright red pixel for test 1, ordering is possibly incorrect. Red amount found was: " + red2);
assert.equal(red3, 119, "Did not find correct amount of red in medium dark red pixel for test 1, ordering is possibly incorrect. Red amount found was: " + red3);
assert.equal(black, 0, "Did not find correct amount of red in black pixel for test 1, ordering is possibly incorrect. Red amount found was: " + black);
});
});