mirror of
https://github.com/konvajs/konva.git
synced 2025-06-28 06:31:15 +08:00
z-order support for konva
This commit is contained in:
parent
6e5aff393f
commit
c0efb7d1a1
141
src/AbsoluteRenderOrderGroup.ts
Normal file
141
src/AbsoluteRenderOrderGroup.ts
Normal 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);
|
21
src/Node.ts
21
src/Node.ts
@ -72,6 +72,7 @@ export interface NodeConfig {
|
||||
preventDefault?: boolean;
|
||||
globalCompositeOperation?: globalCompositeOperationType;
|
||||
filters?: Array<Filter>;
|
||||
zOrder?: number;
|
||||
}
|
||||
|
||||
// CONSTANTS
|
||||
@ -2618,6 +2619,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
|
||||
|
||||
rotation: GetSet<number, this>;
|
||||
zIndex: GetSet<number, this>;
|
||||
zOrder: GetSet<number, this>;
|
||||
|
||||
scale: GetSet<Vector2d | undefined, this>;
|
||||
scaleX: GetSet<number, this>;
|
||||
@ -3278,6 +3280,25 @@ addGetterSetter(Node, 'dragBoundFunc');
|
||||
*/
|
||||
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, {
|
||||
rotateDeg: 'rotate',
|
||||
setRotationDeg: 'setRotation',
|
||||
|
@ -11,6 +11,7 @@ import { Layer } from './Layer';
|
||||
import { FastLayer } from './FastLayer';
|
||||
|
||||
import { Group } from './Group';
|
||||
import { AbsoluteRenderOrderGroup } from './AbsoluteRenderOrderGroup';
|
||||
|
||||
import { DD } from './DragAndDrop';
|
||||
|
||||
@ -32,6 +33,7 @@ export const Konva = Util._assign(Global, {
|
||||
Layer,
|
||||
FastLayer,
|
||||
Group,
|
||||
AbsoluteRenderOrderGroup,
|
||||
DD,
|
||||
Shape,
|
||||
shapes,
|
||||
|
4
src/index-types.d.ts
vendored
4
src/index-types.d.ts
vendored
@ -85,6 +85,10 @@ declare namespace Konva {
|
||||
export type Group = import('./Group').Group;
|
||||
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 Shape: typeof import('./Shape').Shape;
|
||||
|
@ -17,6 +17,7 @@
|
||||
import './unit/DragAndDrop-test.ts';
|
||||
import './unit/Global-test.ts';
|
||||
import './unit/Group-test.ts';
|
||||
import './unit/AbsoluteRenderOrderGroup-test.ts';
|
||||
import './unit/Layer-test.ts';
|
||||
import './unit/Util-test.ts';
|
||||
import './unit/Stage-test.ts';
|
||||
|
141
test/unit/AbsoluteRenderOrderGroup-test.ts
Normal file
141
test/unit/AbsoluteRenderOrderGroup-test.ts
Normal 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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user