group and cache adjacent static outfit layers
perf win owo!
This commit is contained in:
parent
e817ba705b
commit
32c4e540a3
1 changed files with 87 additions and 25 deletions
|
@ -21,6 +21,11 @@ function OutfitCanvas({
|
|||
|
||||
const { loading } = useEaselDependenciesLoader();
|
||||
|
||||
// Set the canvas's internal dimensions to be higher, if the device has high
|
||||
// DPI like retina. But we'll keep the layout width/height as expected!
|
||||
const internalWidth = width * window.devicePixelRatio;
|
||||
const internalHeight = height * window.devicePixelRatio;
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
|
@ -39,46 +44,106 @@ function OutfitCanvas({
|
|||
return () => window.createjs.Ticker.removeEventListener("tick", onTick);
|
||||
}, [loading]);
|
||||
|
||||
const addChild = React.useCallback(
|
||||
(child, zIndex, { afterFirstDraw = null } = {}) => {
|
||||
// Save this child's z-index for future sorting.
|
||||
child.DTI_zIndex = zIndex;
|
||||
// Add the child, then slot it into the right place in the order.
|
||||
stage.addChild(child);
|
||||
stage.sortChildren((a, b) => a.DTI_zIndex - b.DTI_zIndex);
|
||||
if (afterFirstDraw) {
|
||||
stage.on("drawend", afterFirstDraw, null, true);
|
||||
const reorganizeChildren = React.useCallback(() => {
|
||||
// First, to simplify, let's clean out all of the main children, and any
|
||||
// caching group containers they might be in. This will empty the stage.
|
||||
// (This isn't like, _great_ to do re perf, but it only happens on
|
||||
// add/remove, and we don't update yet, and it simplifies the algo a lot.)
|
||||
//
|
||||
// NOTE: We copy the arrays below, because mutating them while iterating
|
||||
// causes elements to get lost!
|
||||
const children = [];
|
||||
for (const childOrCacheGroup of [...stage.children]) {
|
||||
if (childOrCacheGroup.DTI_isCacheGroup) {
|
||||
for (const child of [...childOrCacheGroup.children]) {
|
||||
children.push(child);
|
||||
childOrCacheGroup.removeChild(child);
|
||||
}
|
||||
stage.removeChild(childOrCacheGroup);
|
||||
} else {
|
||||
const child = childOrCacheGroup;
|
||||
children.push(child);
|
||||
stage.removeChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the children in zIndex order.
|
||||
children.sort((a, b) => a.DTI_zIndex - b.DTI_zIndex);
|
||||
|
||||
// Now, re-insert the children into the stage, while making a point of
|
||||
// grouping adjacent non-animated assets into cache group containers.
|
||||
let lastCacheGroup = null;
|
||||
for (const child of children) {
|
||||
stage.addChild(child);
|
||||
if (child.DTI_hasAnimations) {
|
||||
stage.addChild(child);
|
||||
lastCacheGroup = null;
|
||||
} else {
|
||||
if (!lastCacheGroup) {
|
||||
lastCacheGroup = new window.createjs.Container();
|
||||
lastCacheGroup.DTI_isCacheGroup = true;
|
||||
stage.addChild(lastCacheGroup);
|
||||
}
|
||||
|
||||
lastCacheGroup.addChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, cache the cache groups! This will flatten them into a single
|
||||
// bitmap, so that these adjacent static layers can render ~instantly on
|
||||
// each frame, instead of spending time compositing all of them together.
|
||||
// Doesn't seem like a big deal, but helps a lot for squeezing out that
|
||||
// last oomph of performance!
|
||||
for (const childOrCacheGroup of stage.children) {
|
||||
if (childOrCacheGroup.DTI_isCacheGroup) {
|
||||
childOrCacheGroup.cache(0, 0, internalWidth, internalHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether any of the children have animations. Either way, call the
|
||||
// onChangeHasAnimations callback to let the parent know.
|
||||
if (onChangeHasAnimations) {
|
||||
const hasAnimations = stage.children.some((c) =>
|
||||
createjsNodeHasAnimations(c)
|
||||
);
|
||||
const hasAnimations = stage.children.some((c) => c.DTI_hasAnimations);
|
||||
onChangeHasAnimations(hasAnimations);
|
||||
}
|
||||
}, [stage, onChangeHasAnimations, internalWidth, internalHeight]);
|
||||
|
||||
const addChild = React.useCallback(
|
||||
(child, zIndex, { afterFirstDraw = null } = {}) => {
|
||||
// Save this child's z-index and animation-ness for future use. (We could
|
||||
// recompute the animation one at any time, it's just a cached value!)
|
||||
child.DTI_zIndex = zIndex;
|
||||
child.DTI_hasAnimations = createjsNodeHasAnimations(child);
|
||||
|
||||
// Add the child, then reorganize the children to get them sorted and
|
||||
// grouped.
|
||||
stage.addChild(child);
|
||||
reorganizeChildren();
|
||||
|
||||
// Finally, add a one-time listener to trigger `afterFirstDraw`.
|
||||
if (afterFirstDraw) {
|
||||
stage.on("drawend", afterFirstDraw, null, true);
|
||||
}
|
||||
|
||||
// NOTE: We don't bother firing an update, because we trust the ticker
|
||||
// to do it on the next frame.
|
||||
},
|
||||
[stage, onChangeHasAnimations]
|
||||
[stage, reorganizeChildren]
|
||||
);
|
||||
|
||||
const removeChild = React.useCallback(
|
||||
(child) => {
|
||||
stage.removeChild(child);
|
||||
|
||||
if (onChangeHasAnimations) {
|
||||
const hasAnimations = stage.children.some((c) =>
|
||||
createjsNodeHasAnimations(c)
|
||||
);
|
||||
onChangeHasAnimations(hasAnimations);
|
||||
}
|
||||
// Remove the child, then reorganize the children in case this affects
|
||||
// grouping for caching. (Note that the child's parent might not be the
|
||||
// stage; it might be part of a caching group.)
|
||||
child.parent.removeChild(child);
|
||||
reorganizeChildren();
|
||||
|
||||
// NOTE: We don't bother firing an update, because we trust the ticker
|
||||
// to do it on the next frame. (And, I don't understand why, but
|
||||
// updating here actually paused remaining movies! So, don't!)
|
||||
},
|
||||
[stage, onChangeHasAnimations]
|
||||
[reorganizeChildren]
|
||||
);
|
||||
|
||||
const addResizeListener = React.useCallback((handler) => {
|
||||
|
@ -127,11 +192,8 @@ function OutfitCanvas({
|
|||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
// Set the canvas's internal dimensions to be higher, if the device has
|
||||
// high DPI like retina. But we'll keep the layout width/height as
|
||||
// expected!
|
||||
width={width * window.devicePixelRatio}
|
||||
height={height * window.devicePixelRatio}
|
||||
width={internalWidth}
|
||||
height={internalHeight}
|
||||
style={{
|
||||
width: width + "px",
|
||||
height: height + "px",
|
||||
|
|
Loading…
Reference in a new issue