Add OffScreenUpdateBehaviour

This commit is contained in:
Davide Tantillo 2024-08-29 09:00:55 +02:00
parent bc192f4178
commit 26d1ca739d
8 changed files with 2081 additions and 1163 deletions

View File

@ -1456,7 +1456,143 @@ skins.forEach((skin, i) => {
///////////////////// /////////////////////
--> -->
<!--
/////////////////////
// start section 8 //
/////////////////////
-->
<div id="section1" class="section vertical-split">
<div class="split" style="width: 100%; flex-direction: column;">
<div class="split-left" style="width: 80%; box-sizing: border-box;">
Widgets are not render while they are off screen.
<br>
<br>
The state and skeleton <code>update</code>, and the skeleton <code>apply</code> and the skeleton <code>updateWorldTransform</code> functions are not invoked when the widget is off screen.
<br>
<br>
If you want the update functions to be invoked in any case, set <code>offscreen=update</code>.
<br>
If you want all the functions to be invoked in any case, set <code>offscreen=pose</code>.
<br>
<br>
You can also overwrite the update function. Just assign a function to the <code>update</code> property of the widget.
In that it's your responsibility to skip the update/apply. You can use the <code>onScreen</code> property for convinience.
</div>
<div class="split-left" style="width: 80%; box-sizing: border-box; height: 150px;">
<spine-widget
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>
</div>
<div class="split-left" style="width: 80%; box-sizing: border-box; height: 150px;">
<spine-widget
identifier="stretchyman"
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>
</div>
<script>
const stretchyman = spine.getSpineWidget("stretchyman");
stretchyman.update = (canvas, delta, skeleton, state) => {
// skin logiv if widget is off screen
if (!stretchyman.onScreen) return;
delta = delta * 2;
state.update(delta);
skeleton.update(delta);
state.apply(skeleton);
skeleton.updateWorldTransform(spine.Physics.update);
};
</script>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<spine-widget
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>
<spine-widget
identifier="stretchyman"
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>
...
const stretchyman = spine.getSpineWidget("stretchyman");
stretchyman.update = (canvas, delta, skeleton, state) => {
// skin logiv if widget is off screen
if (!stretchyman.onScreen) return;
delta = delta * 2;
state.update(delta);
skeleton.update(delta);
state.apply(skeleton);
skeleton.updateWorldTransform(spine.Physics.update);
};`)
</script>
</code></pre>
</div>
</div>
<div id="section8" class="section vertical-split">
<div class="split-top split">
<div class="split-left">
Widgets are not render while they are off screen.
<br>
The state and skeleton <code>update</code>, and the skeleton <code>apply</code> and the skeleton <code>updateWorldTransform</code> functions are not invoked when the widget is off screen.
<br>If you want the update functions to be invoked in any case, set <code>offscreen=update</code>.
<br>If you want all the functions to be invoked in any case, set <code>offscreen=pose</code>.
</div>
<div class="split-right" id="section8-element">
<spine-widget
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>`);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section 8 //
/////////////////////
-->

View File

@ -160,32 +160,6 @@
pages="0,1,4,6" pages="0,1,4,6"
></spine-widget> ></spine-widget>
</div> </div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="movement/trot-left"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/idea"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/hooray"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div> </div>
</div> </div>
@ -197,33 +171,7 @@
animation="emotes/wave" animation="emotes/wave"
skin="erikari" skin="erikari"
pages="0,1,4,6" pages="0,1,4,6"
></spine-widget> offscreen="update"
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="movement/trot-left"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/idea"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/hooray"
skin="erikari"
pages="0,1,4,6"
></spine-widget> ></spine-widget>
</div> </div>
</div> </div>
@ -236,33 +184,7 @@
animation="emotes/wave" animation="emotes/wave"
skin="mario" skin="mario"
pages="0,1,4,6" pages="0,1,4,6"
></spine-widget> offscreen="pose"
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="movement/trot-left"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/idea"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/hooray"
skin="mario"
pages="0,1,4,6"
></spine-widget> ></spine-widget>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -464,7 +464,6 @@ export class SceneRenderer implements Disposable {
} }
resize2 () { resize2 () {
console.log("RESIZE COMMAND")
let canvas = this.canvas; let canvas = this.canvas;
this.context.gl.viewport(0, 0, canvas.width, canvas.height); this.context.gl.viewport(0, 0, canvas.width, canvas.height);
this.camera.setViewport(canvas.width, canvas.height); this.camera.setViewport(canvas.width, canvas.height);

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,15 @@ interface Rectangle {
type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void; type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void;
type OffScreenUpdateBehaviourType = "pause" | "update" | "pose";
function isOffScreenUpdateBehaviourType(value: string): value is OffScreenUpdateBehaviourType {
return (
value === "pause" ||
value === "update" ||
value === "pose"
);
}
type ModeType = 'inside' | 'origin'; type ModeType = 'inside' | 'origin';
function isModeType(value: string): value is ModeType { function isModeType(value: string): value is ModeType {
return ( return (
@ -115,6 +124,7 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
public loadingSpinner = true; public loadingSpinner = true;
public manualStart = false; public manualStart = false;
public pages?: Array<number>; public pages?: Array<number>;
public offScreenUpdateBehaviour: OffScreenUpdateBehaviourType = "pause";
// state // state
public skeleton?: Skeleton; public skeleton?: Skeleton;
@ -137,7 +147,6 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
if (widget.loading && !widget.onScreenAtLeastOnce) { if (widget.loading && !widget.onScreenAtLeastOnce) {
widget.onScreenAtLeastOnce = true; widget.onScreenAtLeastOnce = true;
console.log(widget.manualStart)
if (widget.manualStart) { if (widget.manualStart) {
widget.start(); widget.start();
} }
@ -164,27 +173,28 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
public loadingScreen: LoadingScreenWidget | null = null; public loadingScreen: LoadingScreenWidget | null = null;
static get observedAttributes(): string[] { static get observedAttributes(): string[] {
return [ return [
"atlas", "atlas",
"skeleton", "skeleton",
"scale", "scale",
"animation", "animation",
"skin", "skin",
"fit", "fit",
"width", "width",
"height", "height",
"draggable", "draggable",
"mode", "mode",
"x-axis", "x-axis",
"y-axis", "y-axis",
"identifier", "identifier",
"offset-x", "offset-x",
"offset-y", "offset-y",
"debug", "debug",
"manual-start", "manual-start",
"spinner", "spinner",
"pages" "pages",
]; "offscreen"
];
} }
constructor() { constructor() {
@ -254,6 +264,10 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
this.mode = isModeType(newValue) ? newValue : "inside"; this.mode = isModeType(newValue) ? newValue : "inside";
} }
if (name === "offscreen") {
this.offScreenUpdateBehaviour = isOffScreenUpdateBehaviourType(newValue) ? newValue : "pause";
}
if (name === "x-axis") { if (name === "x-axis") {
let float = this.xAxis; let float = this.xAxis;
float = parseFloat(newValue); float = parseFloat(newValue);
@ -579,7 +593,8 @@ class SpineWebComponentOverlay extends HTMLElement {
// how many pixels to add to the edges to prevent "edge cuttin" on fast scrolling // how many pixels to add to the edges to prevent "edge cuttin" on fast scrolling
// be aware that the canvas is already big as the display size // be aware that the canvas is already big as the display size
private overflowTop = .0; // making it bigger might reduce performance significantly
private overflowTop = .2;
private overflowBottom = .0; private overflowBottom = .0;
private overflowLeft = .0; private overflowLeft = .0;
private overflowRight = .0; private overflowRight = .0;
@ -621,6 +636,9 @@ class SpineWebComponentOverlay extends HTMLElement {
this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() }); this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() });
this.updateCanvasSize(); this.updateCanvasSize();
this.zoomHandler();
this.translateCanvas();
this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth; this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth;
this.overflowTopSize = this.overflowTop * document.documentElement.clientHeight; this.overflowTopSize = this.overflowTop * document.documentElement.clientHeight;
@ -635,11 +653,10 @@ class SpineWebComponentOverlay extends HTMLElement {
const screen = window.screen; const screen = window.screen;
screen.orientation.onchange = () => { screen.orientation.onchange = () => {
this.updateCanvasSize(); this.updateCanvasSize();
// after an orientation change the scrolling changes, but the scroll event does not fire
this.scrollHandler();
} }
this.zoomHandler();
// scroll
window.addEventListener('scroll', this.scrollHandler); window.addEventListener('scroll', this.scrollHandler);
this.scrollHandler(); this.scrollHandler();
@ -658,15 +675,19 @@ class SpineWebComponentOverlay extends HTMLElement {
const blue = new Color(0, 0, 1, 1); const blue = new Color(0, 0, 1, 1);
return { return {
update: (canvas: SpineCanvas, delta: number) => { update: (canvas: SpineCanvas, delta: number) => {
this.skeletonList.forEach(({ skeleton, state, update }) => { this.skeletonList.forEach(({ skeleton, state, update, onScreen, offScreenUpdateBehaviour, skeletonPath }) => {
if (!skeleton || !state) return; if (!skeleton || !state) return;
if (!onScreen && offScreenUpdateBehaviour === "pause") return;
if (update) update(canvas, delta, skeleton, state) if (update) update(canvas, delta, skeleton, state)
else { else {
// delta = 0 // delta = 0
state.update(delta); state.update(delta);
state.apply(skeleton);
skeleton.update(delta); skeleton.update(delta);
skeleton.updateWorldTransform(Physics.update);
if (onScreen || (!onScreen && offScreenUpdateBehaviour === "pose") ) {
state.apply(skeleton);
skeleton.updateWorldTransform(Physics.update);
}
} }
}); });
this.fps.innerText = canvas.time.framesPerSecond.toFixed(2) + " fps"; this.fps.innerText = canvas.time.framesPerSecond.toFixed(2) + " fps";
@ -913,19 +934,13 @@ class SpineWebComponentOverlay extends HTMLElement {
} }
private resizeCanvas() { private resizeCanvas() {
console.log("START RESI:") const { width, height } = this.getScreenSize();
const screen = window.screen;
const angle = screen.orientation.angle;
const rotated = angle === 90 || angle === 270;
const width = rotated ? screen.height : screen.width;
const height = rotated ? screen.width : screen.height;
if (this.currentCanvasBaseWidth !== width || this.currentCanvasBaseHeight !== height) { if (this.currentCanvasBaseWidth !== width || this.currentCanvasBaseHeight !== height) {
this.currentCanvasBaseWidth = width; this.currentCanvasBaseWidth = width;
this.currentCanvasBaseHeight = height; this.currentCanvasBaseHeight = height;
this.overflowLeftSize = this.overflowLeft * width; this.overflowLeftSize = this.overflowLeft * width;
this.overflowTopSize = this.overflowTop * height; this.overflowTopSize = this.overflowTop * height;
console.log("FROM RESI: ", width, height)
const totalWidth = width * (1 + (this.overflowLeft + this.overflowRight)); const totalWidth = width * (1 + (this.overflowLeft + this.overflowRight));
const totalHeight = height * (1 + (this.overflowTop + this.overflowBottom)); const totalHeight = height * (1 + (this.overflowTop + this.overflowBottom));
@ -935,7 +950,6 @@ class SpineWebComponentOverlay extends HTMLElement {
const dpr = window.devicePixelRatio; const dpr = window.devicePixelRatio;
this.canvas.width = Math.round(totalWidth * dpr); this.canvas.width = Math.round(totalWidth * dpr);
this.canvas.height = Math.round(totalHeight * dpr); this.canvas.height = Math.round(totalHeight * dpr);
console.log("FROM RESI2: ", this.canvas.width, this.canvas.height)
this.spineCanvas.renderer.resize2(); this.spineCanvas.renderer.resize2();
} }
@ -946,20 +960,12 @@ class SpineWebComponentOverlay extends HTMLElement {
} }
private translateCanvas() { private translateCanvas() {
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
// this.overflowLeftSize = this.overflowLeft * viewportWidth;
// this.overflowTopSize = this.overflowTop * viewportHeight;
const scrollPositionX = window.scrollX - this.overflowLeftSize; const scrollPositionX = window.scrollX - this.overflowLeftSize;
const scrollPositionY = window.scrollY - this.overflowTopSize; const scrollPositionY = window.scrollY - this.overflowTopSize;
console.log("FROM TRAN: ", scrollPositionY, window.scrollY, this.overflowTopSize)
this.canvas.style.transform =`translate(${scrollPositionX}px,${scrollPositionY}px)`; this.canvas.style.transform =`translate(${scrollPositionX}px,${scrollPositionY}px)`;
} }
private zoomHandler = () => { private zoomHandler = () => {
console.log("ZOOM")
this.skeletonList.forEach((widget) => { this.skeletonList.forEach((widget) => {
// inside mode scale automatically to fit the skeleton within its parent // inside mode scale automatically to fit the skeleton within its parent
if (widget.mode !== 'origin' && widget.fit !== 'none') return; if (widget.mode !== 'origin' && widget.fit !== 'none') return;
@ -973,13 +979,25 @@ class SpineWebComponentOverlay extends HTMLElement {
}) })
} }
// we need the bounding client rect otherwise decimals won't be returned
// this means that during zoom it might occurs that the div would be resized
// rounded 1px more making a scrollbar appear
private getPageSize() { private getPageSize() {
// we need the bounding client rect otherwise decimals won't be returned
// this means that during zoom it might occurs that the div would be resized
// rounded 1px more making a scrollbar appear
return document.body.getBoundingClientRect(); return document.body.getBoundingClientRect();
} }
// screen size remain the same when it is rotated
// we need to swap them based and the orientation angle
private getScreenSize() {
const { screen } = window;
const { width, height } = window.screen;
const angle = screen.orientation.angle;
const rotated = angle === 90 || angle === 270;
return rotated
? { width: height, height: width }
: { width, height };
}
/* /*
* Other utilities * Other utilities
*/ */

View File

@ -14,7 +14,7 @@ export * from "./ShapeRenderer.js";
export * from "./SkeletonDebugRenderer.js"; export * from "./SkeletonDebugRenderer.js";
export * from "./SkeletonRenderer.js"; export * from "./SkeletonRenderer.js";
export * from "./SpineCanvas.js"; export * from "./SpineCanvas.js";
export * from "./SpineWebComponent.js"; export * from "./SpineWebComponentWidget.js";
export * from "./Vector3.js"; export * from "./Vector3.js";
export * from "./WebGL.js"; export * from "./WebGL.js";
export * from "@esotericsoftware/spine-core"; export * from "@esotericsoftware/spine-core";