How are Kinetic Layers composited? As in, what is the implementation?
At first I thought that each layer was its own canvas (the docs imply this), but unless these are off-screen canvas elements, that seems not to be the case. If they are off-screen, I'm still getting performance hits that are counterintuitive.
I have a component using Kinetic that has 3 layers. Call them backgroundLayer, activeLayer, and selectionLayer. The first is rendered once when a new document is loaded; the second is rarely updated but has elements that may need to be moved around or added/deleted; the last has only a single element that is very small.
What surprised me is that if I have a loop rendering at 20fps, in which I explicitly check each layer for my own "dirty flag" and only render that layer if it's dirty, the framerate crawls even if I only update selectionLayer, and even if that layer has clearBeforeDraw set to false. I can confirm that this property is preventing clearing, as the moving selection leaves a trail of pixels.
The stage is typically something like 600x2000, but the single element in selectionLayer is a Rect that is about 20x100.
What I suspect is happening is that each layer is rendered as an off-screen canvas, but then these canvases are composited together into the single visible canvas. Probably my selectionLayer is fast to update offscreen (and I could add clearing of just the old rect to keep it fast), but then it is effectively blitting 600x2000 transparent pixels when compositing the layers (or even worse, causing the 2 layers that have not been updated to be composited together as well).
Is this accurate as to what's happening? If so, is there a different approach I would take to keep my rendering fast? I'm thinking of just having a separate Kinetic.Stage (and hence canvas), but it's a workaround that starts losing some of the apparent benefits of layers. If layers are only meant to organize code but have this performance implication, I think this should be documented. It would be nice to have onscreen canvas elements per layer too, though I realize this would be a significant change to the library. But otherwise it looks like I'll need to do extra work at the DOM/CSS level to coordinate my layers and get the performance target I need.
RE-EDIT to include sample code and boil question down to its essence:
In component's init():
stage = new Kinetic.Stage({
container: containerID,
width: CANVAS_WIDTH, // in test case, 1320
height: CANVAS_HEIGHT// in test case, 8712
});
backgroundLayer = new Kinetic.Layer({
hitGraphEnabled: false,
clearBeforeDraw: true // i like explicit
});
backgroundLayer.listening(false); // is this redundant to hitGraphEnabled?
activeLayer = new Kinetic.Layer({
hitGraphEnabled: false,
clearBeforeDraw: true
});
activeLayer.listening(false);
playheadLayer = new Kinetic.Layer({
hitGraphEnabled: false,
clearBeforeDraw: true
});
playheadLayer.listening(false);
playhead = new Kinetic.Rect({
fill: 'red',
opacity: 0.3,
stroke: 'black',
x: 0, y: 0,
width: 10,
height: ROW_HEIGHT // 100
});
// playhead.transformsEnabled(false); // can't do this, or setX/Y() do nothing
playheadLayer.add(playhead);
stage.add(backgroundLayer);
stage.add(activeLayer);
stage.add(playheadLayer);
In component's render():
function render(MSSinceRuntime) {
animationFrameID = reqAnimFrame(render); // umm...
if (MSSinceRuntime - lastDrawTimeMSSinceRuntime < clamp) {
return;
}
lastDrawTimeMSSinceRuntime = MSSinceRuntime;
if (backgroundLayer.dirty) {backgroundLayer.drawScene(); }
if (activeLayer.dirty) { activeLayer.drawScene(); }
if (playheadLayer.dirty) { playheadLayer.drawScene(); }
backgroundLayer.dirty = activeLayer.dirty = playheadLayer.dirty = false;
}
And lastly:
var lastMeasureY = 0;
function playheadUpdated(event) {
var timecode = event.beat;
var measureY = getMeasureY(timecode.measure);
playhead.setX(getTimecodeX(timecode));
playhead.setY(measureY);
playheadLayer.dirty = true;
lastMeasureY = measureY;
}
Note that only the playheadLayer is set as dirty, and I have confirmed only this layer is rendered. In Chrome's profiler flame chart I can see that each single call to render() takes ~100ms, and of this, ~99ms will be Kinetic.Context.drawImage() calling [context2d.]drawImage() as the final call on the stack.
What is that call doing, and why?
I'm not at the moment concerned with various other optimizations I may well want to do, including using separate canvases or slicing my monster UI component into more cache-friendly canvas elements. I'm trying to understand this question, because it impacts what I choose to optimize next. That said, all the other optimization suggestions are appreciated.
About KineticJS Layers
Each KineticJS layer is actually 2 canvases. One canvas is for visible display and the other canvas is for off-screen work like hit-testing and drag operations. So your 3 layers are really 3 sets of canvases (6 canvases total).
Background layer
If your background layer never changes, tell it not to listen to events. The eventing system uses a good deal of resources so this should free cpu time for other tasks. You might even consider putting the background on an image element that is positioned using CSS under the Kinetic container. That way KineticJS won’t have to allocate any resources to it at all.
backgroundLayer.listening(false);
Active layer
If all adds/deletes/moves on this layer are done programatically and you don’t need the user to click/drag any element on the active layer, then set listening off on this layer too.
activeLayer.listening(false);
If you still need to listen for active layer events, but you don’t need hit testing, use drawScene instead of draw. The drawScene command draws just the visible canvas and not the offscreen hit canvas.
activeLayer.drawScene();
Suprisingly, you might find it faster to let KineticJS clear/redraw the whole activeLayer rather than micro-managing with clearBeforeDraw. That’s because the GPU can more quickly clear the entire canvas than it can clear a portion of the canvas. The canvas has an internal pixel color array which is very quickly zero-filled to erase the whole canvas. To clear a part of the canvas the browser must calc the starting pixel position and keep track as it zero-fills only a portion of the pixel array.
When updating multiple nodes on the active layer, use batchDraw() which uses window.requestAnimationFrame “behind the scene”. R.A.F. integrates itself with the display hardware and therefore reduces the likelyhood that a draw will be interrupted by a display refresh.
activeLayer.batchDraw();
Selection layer
If your node on the selection layer is not being rotated or scaled, you can disable transformations for this node to greatly improve performance.
myNode.transformsEnabled(“none”);
If you node on the selection layer is not already an image, consider caching it into an image. Images can be blitted mostly by the GPU whereas redrawing a shape requires a considerable effort by the CPU also.
myNode.cache({…});
Other performance stuff
…And, n-e-v-e-r use shadows. Shadowing requires special expensive handling by both KineticJS and also by the native html canvas. If you need a shadowed node:
If you’re managing your own animations, use requestAnimationFrame instead of setInterval/setTimeout. RAF maximizes performance by batching it’s pending commands and coordinatiing its draws with the refresh cycle of the display.
Some useful information
Read the change log for KineticJS. It contains useful info about performance related issues:
https://github.com/ericdrowell/KineticJS/wiki/Change-Log
Hope this helps and good luck with your project!