overlay fix scroll cut

This commit is contained in:
Davide Tantillo 2024-08-16 17:32:21 +02:00
parent 2da5b06c2d
commit 7f5b934a64
3 changed files with 292 additions and 172 deletions

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="../dist/iife/spine-webgl.js"></script>
<!-- <script src="./spine-webgl.js"></script> -->
<title>JS Library Showcase</title>
<style>
* {
@ -18,8 +19,8 @@
font-family: Arial, sans-serif;
}
.section {
/* height: 100vh; */
height: 1000px;
/* height: 100lvh; */
/* height: 800px; */
display: flex;
justify-content: center;
align-items: center;
@ -37,14 +38,15 @@
border: 1px solid salmon;
}
.navigation {
display: flex;
position: fixed;
right: 20px;
top: 50%;
left: 20px;
bottom: 20px;
transform: translateY(-50%);
}
.nav-btn {
display: block;
margin: 10px 0;
margin: 0px 5px;
padding: 10px;
background-color: rgba(255, 255, 255, 0.7);
border: none;
@ -56,9 +58,14 @@
flex-direction: column;
}
.split-top, .split-bottom {
.split-top {
width: 100%;
height: 50%;
height: 600px;
}
.split-bottom {
width: 100%;
/* height: 600px; */
}
.split-top {
@ -88,7 +95,7 @@
</style>
</head>
<body>
<!-- <span id="fps" style="position: fixed; top: 0; left: 0">FPS</span> -->
<span id="fps" style="position: fixed; top: 0; left: 0">FPS</span>
<!--
@ -317,10 +324,16 @@ setInterval(() => {
<div class="split-right" id="section6-element">
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
overlay.addSkeleton(
{
atlasPath: "assets/cloud-pot-pma.atlas",
skeletonPath: "assets/cloud-pot.skel",
animation: 'playing-in-the-rain',
},
document.getElementById(`section6-element`)
);
</code></pre>
</div>
</div>
@ -343,13 +356,53 @@ setInterval(() => {
<div class="split-top split">
<div class="split-left">
As a bonus item, you can move you skeleton around just by setting the <code>draggable</code> property to <code>true</code>.
You can view the skeleton world origin (green), the root bone position (red), and the bounds rectangle and center (blue) by setting <code>debug</code> to <code>true</code>.
</div>
<div class="split-right" id="section7-element">
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
overlay.addSkeleton(
{
atlasPath: "assets/owl-pma.atlas",
skeletonPath: "assets/owl-pro.skel",
animation: 'idle',
},
{
element: document.getElementById(`section7-element`),
debug: true,
}
);
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section 7 //
/////////////////////
-->
<!--
/////////////////////
// start section 8 //
/////////////////////
-->
<div id="section8" class="section vertical-split">
<div class="split-top split">
<div class="split-left">
As a bonus item, you can move you skeleton around just by setting the <code>draggable</code> property to <code>true</code>.
</div>
<div class="split-right" id="section8-element">
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
overlay.addSkeleton(
@ -359,7 +412,7 @@ overlay.addSkeleton(
animation: 'wings-and-feet',
},
{
element: document.getElementById(`section7-element`),
element: document.getElementById(`section8-element`),
draggable: true,
}
);
@ -369,7 +422,7 @@ overlay.addSkeleton(
<!--
/////////////////////
// end section 7 //
// end section 8 //
/////////////////////
-->
@ -382,6 +435,7 @@ overlay.addSkeleton(
<button class="nav-btn" onclick="scrollToSection('section5')">5</button>
<button class="nav-btn" onclick="scrollToSection('section6')">6</button>
<button class="nav-btn" onclick="scrollToSection('section7')">7</button>
<button class="nav-btn" onclick="scrollToSection('section8')">8</button>
</div>
<script>
@ -406,147 +460,169 @@ overlay.addSkeleton(
(async () => {
const overlay = new spine.SpineCanvasOverlay();
// overlay.addSkeleton(
// {
// atlasPath: "assets/spineboy-pma.atlas",
// skeletonPath: "assets/spineboy-pro.skel",
// // animation: 'walk',
// },
// {
// mode: "origin",
// xAxis: 1,
// yAxis: 1,
// element: document.querySelectorAll(`#section1-element`)[0],
// debug: true
// }
// );
/////////////////////
// start section 1 //
/////////////////////
overlay.addSkeleton(
{
atlasPath: "assets/spineboy-pma.atlas",
skeletonPath: "assets/spineboy-pro.skel",
// animation: 'walk',
animation: 'walk',
},
document.querySelectorAll(`#section1-element`),
);
/////////////////////
// end section 1 //
/////////////////////
/////////////////////
// start section 2 //
/////////////////////
overlay.addSkeleton(
{
atlasPath: "assets/spineboy-pma.atlas",
skeletonPath: "assets/spineboy-pro.skel",
animation: 'run',
scale: .25,
},
{
mode: "origin",
xAxis: .5,
yAxis: 1,
element: document.querySelectorAll(`#section1-element`)[0]
}
element: document.getElementById(`section2-element`),
mode: 'origin',
xAxis: .25,
yAxis: .75,
},
);
/////////////////////
// end section 2 //
/////////////////////
/////////////////////
// start section 3 //
/////////////////////
overlay.addSkeleton(
{
atlasPath: "assets/spineboy-pma.atlas",
skeletonPath: "assets/spineboy-pro.skel",
animation: 'jump',
},
{
element: document.getElementById(`section3-element`),
mode: 'inside', // default
offsetX: 100,
offsetY: 50,
},
);
/////////////////////
// end section 3 //
/////////////////////
/////////////////////
// start section 4 //
/////////////////////
const { skeleton, state } = await overlay.addSkeleton(
{
atlasPath: "assets/raptor-pma.atlas",
skeletonPath: "assets/raptor-pro.skel",
animation: 'walk',
},
document.getElementById(`section4-element`)
);
let isRoaring = false;
setInterval(() => {
const newAnimation = isRoaring ? "walk" : "roar";
state.setAnimation(0, newAnimation, true);
overlay.recalculateBounds(skeleton);
isRoaring = !isRoaring;
}, 4000);
// /////////////////////
// // start section 1 //
// /////////////////////
// overlay.addSkeleton(
// {
// atlasPath: "assets/spineboy-pma.atlas",
// skeletonPath: "assets/spineboy-pro.skel",
// animation: 'walk',
// },
// document.querySelectorAll(`#section1-element`),
// );
// /////////////////////
// // end section 1 //
// /////////////////////
// /////////////////////
// // start section 2 //
// /////////////////////
// overlay.addSkeleton(
// {
// atlasPath: "assets/spineboy-pma.atlas",
// skeletonPath: "assets/spineboy-pro.skel",
// animation: 'run',
// scale: .25,
// },
// {
// element: document.getElementById(`section2-element`),
// mode: 'origin',
// xAxis: .25,
// yAxis: .75,
// },
// );
// /////////////////////
// // end section 2 //
// /////////////////////
// /////////////////////
// // start section 3 //
// /////////////////////
// overlay.addSkeleton(
// {
// atlasPath: "assets/spineboy-pma.atlas",
// skeletonPath: "assets/spineboy-pro.skel",
// animation: 'jump',
// },
// {
// element: document.getElementById(`section3-element`),
// mode: 'inside', // default
// offsetX: 100,
// offsetY: 50,
// },
// );
// /////////////////////
// // end section 3 //
// /////////////////////
// /////////////////////
// // start section 4 //
// /////////////////////
// const { skeleton, state } = await overlay.addSkeleton(
// {
// atlasPath: "assets/raptor-pma.atlas",
// skeletonPath: "assets/raptor-pro.skel",
// animation: 'walk',
// },
// document.getElementById(`section4-element`)
// );
// let isRoaring = false;
// setInterval(() => {
// const newAnimation = isRoaring ? "walk" : "roar";
// state.setAnimation(0, newAnimation, true);
// overlay.recalculateBounds(skeleton);
// isRoaring = !isRoaring;
// }, 4000);
// /////////////////////
// // end section 4 //
// /////////////////////
// /////////////////////
// // start section 5 //
// /////////////////////
// /////////////////////
// // end section 5 //
// /////////////////////
// /////////////////////
// // start section 6 //
// /////////////////////
// overlay.addSkeleton(
// {
// atlasPath: "assets/cloud-pot-pma.atlas",
// skeletonPath: "assets/cloud-pot.skel",
// animation: 'playing-in-the-rain',
// },
// document.getElementById(`section6-element`)
// );
// /////////////////////
// // end section 6 //
// /////////////////////
// /////////////////////
// // start section 7 //
// /////////////////////
// overlay.addSkeleton(
// {
// atlasPath: "assets/celestial-circus-pma.atlas",
// skeletonPath: "assets/celestial-circus-pro.skel",
// animation: 'wings-and-feet',
// },
// {
// element: document.getElementById(`section7-element`),
// draggable: true,
// }
// );
/////////////////////
// end section 4 //
/////////////////////
/////////////////////
// start section 5 //
/////////////////////
/////////////////////
// end section 5 //
/////////////////////
/////////////////////
// start section 6 //
/////////////////////
overlay.addSkeleton(
{
atlasPath: "assets/cloud-pot-pma.atlas",
skeletonPath: "assets/cloud-pot.skel",
animation: 'playing-in-the-rain',
},
document.getElementById(`section6-element`)
);
/////////////////////
// end section 6 //
/////////////////////
/////////////////////
// start section 7 //
/////////////////////
overlay.addSkeleton(
{
atlasPath: "assets/owl-pma.atlas",
skeletonPath: "assets/owl-pro.skel",
animation: 'idle',
},
{
element: document.getElementById(`section7-element`),
debug: true,
}
);
//////////////////////
// end section 7 //
//////////////////////
/////////////////////
// start section 8 //
/////////////////////
overlay.addSkeleton(
{
atlasPath: "assets/celestial-circus-pma.atlas",
skeletonPath: "assets/celestial-circus-pro.skel",
animation: 'wings-and-feet',
},
{
element: document.getElementById(`section8-element`),
draggable: true,
debug: true,
}
);
//////////////////////
// end section 8 //
//////////////////////
})();
@ -567,6 +643,8 @@ overlay.addSkeleton(
function makeDraggable(element) {
element.style["touch-action"] = "none";
let isDragging = false;
let startX, startY;
let originalX, originalY;
@ -626,6 +704,7 @@ overlay.addSkeleton(
resizeHandle.style["cursor"] = "se-resize";
element.style["position"] = "relative";
element.style["touch-action"] = "none";
let isResizing = false;
let startX, startY, startWidth, startHeight, startPaddingLeft, startPaddingRight, startPaddingTop, startPaddingBottom;
@ -662,8 +741,8 @@ overlay.addSkeleton(
}
// makeDraggable(document.getElementById(`section6-element`));
// makeResizable(document.getElementById(`section6-element`));
makeDraggable(document.getElementById(`section6-element`));
makeResizable(document.getElementById(`section6-element`));
</script>
</body>
</html>

View File

@ -92,7 +92,7 @@ export class Input {
let deltaY = ev.deltaY;
if (ev.deltaMode == WheelEvent.DOM_DELTA_LINE) deltaY *= 8;
if (ev.deltaMode == WheelEvent.DOM_DELTA_PAGE) deltaY *= 24;
this.listeners.map((listener) => { if (listener.wheel) listener.wheel(e.deltaY); });
this.listeners.map((listener) => { if (listener.wheel) listener.wheel(ev.deltaY); });
};
element.addEventListener("mousedown", mouseDown, true);

View File

@ -50,7 +50,7 @@ type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skelet
interface OverlayHTMLOptions {
element: HTMLElement,
mode?: OverlayElementMode,
showBounds?: boolean,
debug?: boolean,
offsetX?: number,
offsetY?: number,
xAxis?: number,
@ -80,6 +80,15 @@ export class SpineCanvasOverlay {
private resizeObserver:ResizeObserver;
private disposed = false;
// how may pixels to add to the bottom (to avoid cut on edge during scrolling)
private readonly additionalPixelsBottom = 300;
// how much the canvas is translated above (to avoid cut on edge during scrolling)
private readonly offsetHeight = 100;
// the actual base translation
private offsetHeightDraw: number;
/** Constructs a new spine canvas, rendering to the provided HTML canvas. */
constructor () {
this.canvas = document.createElement('canvas');
@ -87,11 +96,14 @@ export class SpineCanvasOverlay {
this.canvas.style.position = "absolute";
this.canvas.style.top = "0";
this.canvas.style.left = "0";
this.canvas.style.display = "inline";
this.canvas.style.setProperty("pointer-events", "none");
// this.canvas.style.width = "100%";
// this.canvas.style.height = "100%";
this.offsetHeightDraw = this.offsetHeight;
this.canvas.style.transform =`translate(0px,0px)`;
// this.canvas.style.display = "inline";
// this.canvas.style.overflow = "hidden"; // useless
// this.canvas.style.setProperty("will-change", "transform"); // performance seems to be even worse with this uncommented
this.updateCanvasSize();
this.scrollHandler();
this.resizeObserver = new ResizeObserver(() => {
this.updateCanvasSize();
@ -99,6 +111,9 @@ export class SpineCanvasOverlay {
});
this.resizeObserver.observe(document.body);
window.addEventListener('scroll', this.scrollHandler);
this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() });
this.input = new Input(document.body, false);
@ -148,7 +163,7 @@ export class SpineCanvasOverlay {
list = htmlOptionsList as Array<OverlayHTMLOptions>;
}
const mapList = list.map(({ element, mode: givenMode, showBounds = false, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0, draggable = false, }, i) => {
const mapList = list.map(({ element, mode: givenMode, debug = false, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0, draggable = false, }, i) => {
const mode = givenMode ?? 'inside';
if (mode == 'inside' && i > 0) {
console.warn("inside option works with multiple html elements only if the elements have the same dimension"
@ -158,7 +173,7 @@ export class SpineCanvasOverlay {
return {
element: element as HTMLElement,
mode,
showBounds,
debug,
offsetX,
offsetY,
xAxis,
@ -251,15 +266,14 @@ export class SpineCanvasOverlay {
skeleton.updateWorldTransform(Physics.update);
}
});
// (document.body.querySelector("#fps")! as HTMLElement).innerText = canvas.time.framesPerSecond.toFixed(2) + " fps";
(document.body.querySelector("#fps")! as HTMLElement).innerText = canvas.time.framesPerSecond.toFixed(2) + " fps";
},
render: (canvas: SpineCanvas) => {
// canvas.clear(1, 0, 0, .1);
let renderer = canvas.renderer;
renderer.begin();
// console.log(canvas.gl.getParameter(canvas.gl.MAX_RENDERBUFFER_SIZE));
const devicePixelRatio = window.devicePixelRatio;
const tempVector = new Vector3();
this.skeletonList.forEach(({ skeleton, htmlOptionsList, bounds }) => {
@ -268,10 +282,9 @@ export class SpineCanvasOverlay {
let { x: ax, y: ay, width: aw, height: ah } = bounds;
htmlOptionsList.forEach((list) => {
const { element, mode, showBounds, offsetX, offsetY, xAxis, yAxis, dragX, dragY } = list;
const { element, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY } = list;
const divBounds = element.getBoundingClientRect();
// console.log(divBounds.x, divBounds.y, divBounds.width, divBounds.height)
divBounds.y += this.offsetHeightDraw;
let x = 0, y = 0;
if (mode === 'inside') {
@ -292,8 +305,10 @@ export class SpineCanvasOverlay {
const boundsY = (ay + ah / 2) * ratio;
// get the center of the div in world coordinate
const divX = divBounds.x + divBounds.width / 2 + window.scrollX;
const divY = divBounds.y - 1 + divBounds.height / 2 + window.scrollY;
// const divX = divBounds.x + divBounds.width / 2 + window.scrollX;
// const divY = divBounds.y - 1 + divBounds.height / 2 + window.scrollY;
const divX = divBounds.x + divBounds.width / 2;
const divY = divBounds.y - 1 + divBounds.height / 2;
this.screenToWorld(tempVector, divX, divY);
// get vertices offset: calculate the distance between div center and bounds center
@ -308,8 +323,10 @@ export class SpineCanvasOverlay {
// TODO: window.devicePixelRatio to manage browser zoom
// get the center of the div in world coordinate
const divX = divBounds.x + divBounds.width * xAxis + window.scrollX;
const divY = divBounds.y + divBounds.height * yAxis + window.scrollY;
// const divX = divBounds.x + divBounds.width * xAxis + window.scrollX;
// const divY = divBounds.y + divBounds.height * yAxis + window.scrollY;
const divX = divBounds.x + divBounds.width * xAxis;
const divY = divBounds.y + divBounds.height * yAxis;
this.screenToWorld(tempVector, divX, divY);
// console.log(tempVector.x, tempVector.y)
// console.log(window.devicePixelRatio)
@ -323,9 +340,6 @@ export class SpineCanvasOverlay {
list.worldOffsetX = x + offsetX + dragX;
list.worldOffsetY = y + offsetY + dragY;
console.log(list.worldOffsetY)
// console.log("----")
renderer.drawSkeleton(skeleton, true, -1, -1, (vertices, size, vertexSize) => {
for (let i = 0; i < size; i+=vertexSize) {
vertices[i] = vertices[i] + list.worldOffsetX;
@ -334,7 +348,8 @@ export class SpineCanvasOverlay {
});
// drawing debug stuff
if (showBounds) {
if (debug) {
// if (true) {
// show bounds and its center
renderer.rect(false,
ax * skeleton.scaleX + list.worldOffsetX,
@ -378,7 +393,7 @@ export class SpineCanvasOverlay {
this.input.addListener({
down: (x, y, ev) => {
const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
tempVectorInput.set(originalEvent.pageX, originalEvent.pageY, 0);
tempVectorInput.set(originalEvent.pageX - window.scrollX, originalEvent.pageY - window.scrollY + this.offsetHeightDraw, 0);
this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight);
this.skeletonList.forEach(({ htmlOptionsList, bounds, skeleton }) => {
htmlOptionsList.forEach((element) => {
@ -404,7 +419,7 @@ export class SpineCanvasOverlay {
},
dragged: (x, y, ev) => {
const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
tempVectorInput.set(originalEvent.pageX, originalEvent.pageY, 0);
tempVectorInput.set(originalEvent.pageX - window.scrollX, originalEvent.pageY - window.scrollY + this.offsetHeightDraw, 0);
this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight);
let dragX = tempVectorInput.x - prevX;
let dragY = tempVectorInput.y - prevY;
@ -436,13 +451,40 @@ export class SpineCanvasOverlay {
}
/*
* Resize utilities
* Resize/scroll utilities
*/
private updateCanvasSize() {
const pageSize = this.getPageSize();
this.canvas.style.width = pageSize.width + "px";
this.canvas.style.height = pageSize.height + "px";
const displayWidth = document.documentElement.clientWidth;
const displayHeight = document.documentElement.clientHeight;
this.canvas.style.width = displayWidth + "px";
this.canvas.style.height = displayHeight + this.additionalPixelsBottom + "px";
}
private scrollHandler = () => {
const { width, height } = this.getPageSize();
const scrollPositionX = window.scrollX;
const floatingDivWidth = this.canvas.offsetWidth;
const maxTranslationX = width - floatingDivWidth;
const translationX = Math.min(scrollPositionX, maxTranslationX);
const scrollPositionY = window.scrollY;
const floatingDivHeight = this.canvas.offsetHeight;
const maxTranslation = height - floatingDivHeight + this.offsetHeight;
const translationY = Math.min(scrollPositionY, maxTranslation) - this.offsetHeight;
const delta = scrollPositionY - maxTranslation
this.offsetHeightDraw = this.offsetHeight;
if (delta > 0) {
this.offsetHeightDraw += delta + 1;
}
// translate should be faster
this.canvas.style.transform =`translate(${translationX}px,${translationY}px)`;
// this.canvas.style.top = `${this.currentTranslateY}px`;
// this.canvas.style.left = `${this.currentTranslateX}px`;
}
private getPageSize() {
@ -512,7 +554,6 @@ export class SpineCanvasOverlay {
private screenToWorld(vec: Vector3, x: number, y: number) {
vec.set(x, y, 0);
this.spineCanvas.renderer.camera.screenToWorld(vec, this.canvas.clientWidth, this.canvas.clientHeight);
// console.log(this.canvas.clientWidth, this.canvas.clientHeight);
}
private inside(point: { x: number; y: number }, rectangle: Rectangle): boolean {