diff --git a/src/AbsoluteRenderOrderGroup.ts b/src/AbsoluteRenderOrderGroup.ts new file mode 100644 index 00000000..644c03e8 --- /dev/null +++ b/src/AbsoluteRenderOrderGroup.ts @@ -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(); + + // 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([...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>):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()); + } + + 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); diff --git a/src/Node.ts b/src/Node.ts index a1bc5438..5f322b9a 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -72,6 +72,7 @@ export interface NodeConfig { preventDefault?: boolean; globalCompositeOperation?: globalCompositeOperationType; filters?: Array; + zOrder?: number; } // CONSTANTS @@ -2618,6 +2619,7 @@ export abstract class Node { rotation: GetSet; zIndex: GetSet; + zOrder: GetSet; scale: GetSet; scaleX: GetSet; @@ -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', diff --git a/src/_CoreInternals.ts b/src/_CoreInternals.ts index 9744e3a0..2ce0c422 100644 --- a/src/_CoreInternals.ts +++ b/src/_CoreInternals.ts @@ -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, diff --git a/src/index-types.d.ts b/src/index-types.d.ts index ab517869..149a3222 100644 --- a/src/index-types.d.ts +++ b/src/index-types.d.ts @@ -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; diff --git a/test/unit-tests.html b/test/unit-tests.html index 1262ea64..1900a647 100644 --- a/test/unit-tests.html +++ b/test/unit-tests.html @@ -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'; diff --git a/test/unit/AbsoluteRenderOrderGroup-test.ts b/test/unit/AbsoluteRenderOrderGroup-test.ts new file mode 100644 index 00000000..9800cb84 --- /dev/null +++ b/test/unit/AbsoluteRenderOrderGroup-test.ts @@ -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); + }); +}); \ No newline at end of file