Moved some examples in their own pages.

This commit is contained in:
Davide Tantillo 2025-05-05 16:13:55 +02:00
parent 15918c0342
commit 2bf85467dc
6 changed files with 1280 additions and 1282 deletions

View File

@ -195,8 +195,11 @@
</ul>
<li>Widget (Webcomponent)</li>
<ul>
<li><a href="/spine-widget/example/tutorial.html">Example</a></li>
<li><a href="/spine-widget/example/tutorial.html">Tutorial</a></li>
<li><a href="/spine-widget/example/team.html">Team</a></li>
<li><a href="/spine-widget/example/app.html">App</a></li>
<li><a href="/spine-widget/example/login.html">Login</a></li>
<li><a href="/spine-widget/example/game.html">Game</a></li>
<li><a href="/spine-widget/example/gui.html">GUI</a></li>
</ul>
</ul>

View File

@ -0,0 +1,690 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webcomponent GUI</title>
<style>
body {
margin: 0 auto;
padding: 0;
font-family: Arial, sans-serif;
font-size: 16px;
}
.phone {
min-width: 288px;
height: 80%;
background: white;
border-radius: 30px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
overflow: hidden;
border: 16px solid black;
position: relative;
display: flex;
aspect-ratio: 1/2;
color: black;
}
.screen-container {
display: flex;
width: 100%;
transition: transform 0.25s linear;
}
.screen {
width: 100%;
height: 100%;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.top-section {
flex: 1;
background: #ddd;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
font-weight: bold;
border-bottom: 5px solid black;
}
.bottom-section {
display: flex;
flex-direction: row;
/* height: 40%; */
background: #fff;
}
.left-section, .right-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
border-right: 5px solid black;
}
.right-section {
border-right: none;
}
.left-section {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 5px;
padding: 5px;
width: 100%;
max-width: 800px;
}
.item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
background: #f0f0f0;
padding: 1px;
border-radius: 5px;
}
.item img {
width: 100%;
height: auto;
max-width: 30px;
}
.controls {
display: flex;
gap: 5px;
flex-direction: row-reverse;
}
.controls button {
width: 20px;
height: 20px;
font-size: 12px;
}
.btn-small {
font-size: 16px;
border: none;
background: #007bff;
color: white;
cursor: pointer;
border-radius: 3px;
margin: 1px 0;
}
.btn-small:active {
background: #0056b3;
}
.list {
flex: 1;
display: flex;
flex-direction: column;
background: #f0f0f0;
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
font-size: 16px;
font-weight: bold;
padding: 5px;
border-bottom: 3px solid black;
}
.buttons {
display: flex;
}
.btn {
flex: 1;
padding: 10px;
font-size: 16px;
font-weight: bold;
border: none;
background: #dc3545;
color: white;
cursor: pointer;
}
.btn.next {
background: #28a745;
}
.btn:active {
opacity: 0.8;
}
.buttons-section-2 {
height: 40%;
}
.food-piece-circle {
width: 45px;
height: 45px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
border: 3px solid transparent;
transition: border-color 0.3s ease-in-out;
cursor: pointer;
background: white;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
}
.box-button:active {
box-shadow: inset 0px 4px 6px rgba(0, 0, 0, 0.1);
transform: translateY(2px);
}
.group-buttons {
width: 100%;
padding: 10px;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="container" style="display: flex; justify-content: center; align-items: center; padding: 10px;">
<div style="max-width: 280px;" class="phone">
<spine-overlay overlay-id="phone"></spine-overlay>
<div class="screen-container" id="screenContainer">
<!-- SECTION 1 -->
<div class="screen">
<div class="top-section">
<spine-widget
identifier="list"
overlay-id="phone"
default-mix=".2"
atlas="assets/food/food-app-pro.atlas"
skeleton="assets/food/list-search.json"
animation="animation"
isinteractive
></spine-widget>
</div>
<div class="bottom-section">
<div class="left-section">
<div id="carrotDiv" class="item">
<img src="assets/food/carrot-body.png">
<div class="controls">
<button id="carrotPlus" class="btn-small">+</button>
<button id="carrotMinus" class="btn-small">-</button>
</div>
</div>
<div id="tomatoDiv" class="item">
<img src="assets/food/tomato-body.png">
<div class="controls">
<button id="tomatoPlus" class="btn-small">+</button>
<button id="tomatoMinus" class="btn-small">-</button>
</div>
</div>
<div id="breadDiv" class="item">
<img src="assets/food/bread.png">
<div class="controls">
<button id="breadPlus" class="btn-small">+</button>
<button id="breadMinus" class="btn-small">-</button>
</div>
</div>
<div id="mushroomDiv" class="item">
<img src="assets/food/mushroom.png">
<div class="controls">
<button id="mushroomPlus" class="btn-small">+</button>
<button id="mushroomMinus" class="btn-small">-</button>
</div>
</div>
</div>
<div class="right-section">
<div class="list">
<div class="list-item"><span>Carrots</span> <span id="list-item-carrot">0</span></div>
<div class="list-item"><span>Tomatoes</span> <span id="list-item-tomato">0</span></div>
<div class="list-item"><span>Breads</span> <span id="list-item-bread">0</span></div>
<div class="list-item"><span>Mushrooms</span> <span id="list-item-mushroom">0</span></div>
</div>
<div class="buttons">
<button class="btn">Clear</button>
<button class="btn next">Next</button>
</div>
</div>
</div>
</div>
<!-- SECTION 1 -->
<!-- SECTION 2 -->
<div class="screen">
<div class="top-section">
<spine-widget
identifier="pan"
overlay-id="phone"
default-mix=".2"
atlas="assets/food/food-app-pro.atlas"
skeleton="assets/food/pan-cooking-pro.json"
animation="animation"
></spine-widget>
</div>
<div class="buttons-section-2">
<div class="group-buttons">
<div style="text-align: center;">
Click on the food to cook it
</div>
<div class="buttons">
<button class="btn" style="padding: 5px;">Prev</button>
<button id="btn-next-2" class="btn" style="padding: 5px; background-color: gainsboro; transition: background-color 0.25s ease-in-out;" disabled>Next</button>
</div>
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px">
<div id="foodPieceDiv1" class="food-piece-circle">
<img src="assets/food/food-piece-1.png" style="width: 70%; height: auto;">
</div>
<div id="foodPieceDiv2" class="food-piece-circle">
<img src="assets/food/food-piece-2.png" style="width: 70%; height: auto;">
</div>
<div id="foodPieceDiv3" class="food-piece-circle">
<img src="assets/food/food-piece-3.png" style="width: 70%; height: auto;">
</div>
<div id="foodPieceDiv4" class="food-piece-circle">
<img src="assets/food/food-piece-4.png" style="width: 70%; height: auto;">
</div>
<div id="foodPieceDiv5" class="food-piece-circle">
<img src="assets/food/food-piece-5.png" style="width: 70%; height: auto;">
</div>
<div id="foodPieceDiv6" class="food-piece-circle">
<img src="assets/food/food-piece-6.png" style="width: 70%; height: auto;">
</div>
<div id="foodPieceDiv7" class="food-piece-circle">
<img src="assets/food/food-piece-7.png" style="width: 70%; height: auto;">
</div>
</div>
</div>
</div>
<!-- SECTION 2 -->
<!-- SECTION 3 -->
<div class="screen">
<div class="top-section">
<spine-widget
identifier="delivery"
overlay-id="phone"
default-mix=".2"
atlas="assets/food/food-app-pro.atlas"
skeleton="assets/food/meal-delivery-pro.json"
animation="animation"
></spine-widget>
</div>
<div class="buttons-section-2">
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px">
<div id="buttonDistance" class="food-piece-circle box-button" style="user-select: none;">
<img src="assets/food/box.png" style="width: 70%; height: auto;">
</div>
</div>
<div class="group-buttons">
<div style="text-align: center;">
Help the box to reach the destination by clicking the button above!
</div>
<div class="buttons">
<button class="btn" style="padding: 5px;">Prev</button>
<button id="btn-next-3" class="btn" style="padding: 5px; background-color: gainsboro; transition: background-color 0.25s ease-in-out;" disabled>Next</button>
</div>
</div>
</div>
</div>
<!-- SECTION 3 -->
<!-- SECTION 4 -->
<div class="screen">
<div class="top-section">
<spine-widget
identifier="ready"
overlay-id="phone"
default-mix=".2"
atlas="assets/food/food-app-pro.atlas"
skeleton="assets/food/meal-ready-pro.json"
animation="base"
isinteractive
></spine-widget>
</div>
<div class="buttons-section-2">
<div class="group-buttons">
<div style="text-align: center;">
Congratulation! You food has just been delivered!
</div>
<div class="buttons">
<button class="btn" style="padding: 5px;">Prev</button>
</div>
</div>
</div>
</div>
<!-- SECTION 4 -->
</div>
</div>
</div>
<script type="module">
import * as spine from '../dist/esm/spine-widget.mjs';
(async () => {
/* SECTION 1 */
const widget1 = await spine.getSpineWidget("list").whenReady;
const setInteractionSectionOne = (itemName, trackNumber) => {
const divName = `${itemName}Div`;
const buttonNamePlus = `${itemName}Plus`;
const buttonNameMinus = `${itemName}Minus`;
const listItemName = `list-item-${itemName}`;
const itemDiv = document.getElementById(divName);
const listItemDiv = document.getElementById(listItemName);
itemDiv.addEventListener('mouseenter', () => {
widget1.state.setAnimation(0, `focus-${itemName}`, true);
widget1.state.setAnimation(trackNumber, `shake-${itemName}`, true);
});
const setDefaultState = () => {
const currentEntry = widget1.state.getCurrent(trackNumber);
if (!currentEntry) return;
if (currentEntry.animation.name === `shake-${itemName}`) {
widget1.state.setEmptyAnimation(trackNumber);
} else if (currentEntry.next && currentEntry.next.animation && currentEntry.next.animation.name === `shake-${itemName}`) {
widget1.state.clearNext(currentEntry);
widget1.state.addEmptyAnimation(trackNumber);
}
widget1.state.setAnimation(0, "animation", true);
}
itemDiv.addEventListener('mouseleave', setDefaultState);
const addItemAction = () => {
widget1.state.setAnimation(trackNumber, `add-${itemName}`, false);
widget1.state.addAnimation(trackNumber, `shake-${itemName}`, true);
listItemDiv.textContent = parseInt(listItemDiv.textContent) + 1;
}
const itemSlot = widget1.skeleton.findSlot(`bubble-base-${itemName.charAt(0)}`);
let onItem = false;
widget1.addCursorSlotEventCallback(itemSlot, (slot, event) => {
if (event === "enter") {
widget1.state.setAnimation(0, `focus-${itemName}`, true);
widget1.state.setAnimation(trackNumber, `shake-${itemName}`, true);
}
if (event === "leave") {
onItem = false;
const currentEntry = widget1.state.getCurrent(trackNumber);
if (currentEntry.animation.name === `shake-${itemName}`) {
widget1.state.setEmptyAnimation(trackNumber);
} else if (currentEntry.next && currentEntry.next.animation && currentEntry.next.animation.name === `shake-${itemName}`) {
widget1.state.clearNext(currentEntry);
widget1.state.addEmptyAnimation(trackNumber);
}
const currentEntryZero = widget1.state.getCurrent(0);
if (currentEntryZero.animation.name === `focus-${itemName}`) {
widget1.state.setAnimation(0, "animation", true);
}
}
if (event === "down") {
addItemAction();
}
});
const itemButtonPlus = document.getElementById(buttonNamePlus);
itemButtonPlus.onclick = addItemAction;
const itemButtonMinus = document.getElementById(buttonNameMinus);
const removeItemAction = () => {
const current = parseInt(listItemDiv.textContent);
if (current > 0) listItemDiv.textContent = current - 1;
}
itemButtonMinus.onclick = removeItemAction;
}
setInteractionSectionOne("carrot", 1);
setInteractionSectionOne("tomato", 2);
setInteractionSectionOne("bread", 3);
setInteractionSectionOne("mushroom", 4);
/* SECTION 1 */
/* SECTION 2 */
const btnNext2 = document.getElementById("btn-next-2");
const widget2 = await spine.getSpineWidget("pan").whenReady;
const foodPiece1 = widget2.skeleton.findSlot(`food-piece-1`);
const foodPiece2 = widget2.skeleton.findSlot(`food-piece-2`);
const foodPiece3 = widget2.skeleton.findSlot(`food-piece-3`);
const foodPiece4 = widget2.skeleton.findSlot(`food-piece-4`);
const foodPiece5 = widget2.skeleton.findSlot(`food-piece-5`);
const foodPiece6 = widget2.skeleton.findSlot(`food-piece-6`);
const foodPiece7 = widget2.skeleton.findSlot(`food-piece-7`);
const foodPieces = [
[foodPiece1, document.getElementById("foodPieceDiv1")],
[foodPiece2, document.getElementById("foodPieceDiv2")],
[foodPiece3, document.getElementById("foodPieceDiv3")],
[foodPiece4, document.getElementById("foodPieceDiv4")],
[foodPiece5, document.getElementById("foodPieceDiv5")],
[foodPiece6, document.getElementById("foodPieceDiv6")],
[foodPiece7, document.getElementById("foodPieceDiv7")],
];
foodPieces.forEach(([foodPiece, itemDiv], index) => {
foodPiece.color.set(1, 1, 1, 0);
itemDiv.addEventListener('mousedown', () => {
if (itemDiv.dataset.cooking) {
itemDiv.style.borderColor = "transparent";
delete itemDiv.dataset.cooking;
const interval = setInterval(() => {
let alpha = foodPiece.color.a;
if (alpha <= 0) {
clearInterval(interval);
return;
}
foodPiece.color.set(1, 1, 1, alpha - 0.1);
}, 10);
} else {
itemDiv.style.borderColor = "#4CAF50";
itemDiv.dataset.cooking = true;
const interval = setInterval(() => {
let alpha = foodPiece.color.a;
if (alpha >= 1) {
clearInterval(interval);
return;
}
foodPiece.color.set(1, 1, 1, alpha + 0.1);
}, 10);
}
if (foodPieces.every(([,{ dataset }]) => dataset.cooking)) {
btnNext2.style.backgroundColor = "#28a745";
btnNext2.removeAttribute("disabled");
} else {
btnNext2.style.backgroundColor = "gainsboro";
btnNext2.setAttribute("disabled", "");
}
});
})
/* SECTION 2 */
/* SECTION 3 */
const widget3 = await spine.getSpineWidget("delivery").whenReady;
const btnNext3 = document.getElementById("btn-next-3");
const box = widget3.skeleton.findSlot("box");
let distance = -1300;
const buttonDistance = document.getElementById("buttonDistance");
buttonDistance.addEventListener("mousedown", () => {
let toAdd = 200;
const interval = setInterval(() => {
if (toAdd <= 0) {
clearInterval(interval);
return;
}
toAdd -= 10;
distance += 10;
}, 10)
})
setInterval(() => {
if (distance <= -1300) return;
distance -= 5;
if (distance > 2000) {
btnNext3.style.backgroundColor = "#28a745";
btnNext3.removeAttribute("disabled");
} else {
btnNext3.style.backgroundColor = "gainsboro";
btnNext3.setAttribute("disabled", "");
}
}, 10)
widget3.beforeUpdateWorldTransforms = () => {
box.bone.x += distance;
}
if (foodPieces.every(([,{ dataset }]) => dataset.cooking)) {
btnNext3.style.backgroundColor = "#28a745";
btnNext3.removeAttribute("disabled");
} else {
btnNext3.style.backgroundColor = "gainsboro";
btnNext3.setAttribute("disabled", "");
}
/* SECTION 3 */
/* SECTION 4 */
const widget4 = await spine.getSpineWidget("ready").whenReady;
const slot4Bread = widget4.skeleton.findSlot("salad");
widget4.addCursorSlotEventCallback(slot4Bread, (slot, event) => {
if (event === "enter") {
widget4.state.setAnimation(1, "bread-opening", false);
widget4.state.addAnimation(1, "bread-open", true);
}
if (event === "leave") {
widget4.state.setAnimation(1, "bread-closing", false);
}
});
const slot4Bottle = widget4.skeleton.findSlot("bottle-base");
widget4.addCursorSlotEventCallback(slot4Bottle, (slot, event) => {
if (event === "enter") {
widget4.state.setAnimation(2, "bottle-opening", false);
widget4.state.addAnimation(2, "bottle-open", true);
}
if (event === "leave") {
widget4.state.setAnimation(2, "bottle-closing", false);
}
});
const slot4Fries = widget4.skeleton.findSlot("fries-case-back");
widget4.addCursorSlotEventCallback(slot4Fries, (slot, event) => {
if (event === "enter") {
widget4.state.setAnimation(3, "fries", true);
}
if (event === "leave") {
let track = widget4.state.setEmptyAnimation(3);
track.mixDuration = 1;
}
});
/* SECTION 4 */
})();
let currentIndex = 0;
function nextScreen() {
const container = document.getElementById('screenContainer');
const totalScreens = document.querySelectorAll('.screen').length;
if (currentIndex < totalScreens - 1) {
currentIndex++;
container.style.transform = `translateX(-${currentIndex * 100}%)`;
}
}
function prevScreen() {
const container = document.getElementById('screenContainer');
const totalScreens = document.querySelectorAll('.screen').length;
if (currentIndex > 0) {
currentIndex--;
container.style.transform = `translateX(-${currentIndex * 100}%)`;
}
}
const nextButtons = [
document.querySelector("button.btn.next"),
document.getElementById("btn-next-2"),
document.getElementById("btn-next-3"),
];
for (const button of nextButtons) button.onclick = nextScreen;
const prevButtons = Array.from(document.querySelectorAll('button')).filter(button => button.textContent.trim() === 'Prev');
for (const button of prevButtons) button.onclick = prevScreen;
</script>
</body>
</html>

View File

@ -0,0 +1,332 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webcomponent GUI</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
font-size: 16px;
background-color: rgb(24, 149, 89);
}
.instruction {
display: flex; flex-direction: column; border: 3px solid black; border-radius: 10px; box-shadow: 5px 5px 15px grey; padding: 10px; background-color: white;
}
.instruction .title {
flex: 30%; font-weight: bold; text-transform: uppercase; border-bottom: 1px solid white; text-align: center;
}
</style>
</head>
<body>
<div style="display: flex; justify-content: center; align-items: center; padding: 10px;">
<div class="split-top split" style="max-width: 800px; width: 100%;">
<div class="split-left" style="padding: 0;">
<div id="container-game" style="display: flex; flex-direction: column; height: 400px; position: relative;">
<div
id="win-panel"
style="top: 0; left: 0; width: 100%; height: 100%; position: absolute; display: none; align-items: center; justify-content: center; background-color: #000000aa; z-index: 1; color: white; font-size: xx-large;">
YOU LOSE!
</div>
<div class="bottom" style="flex: 80%; background-color: rgb(24, 149, 89); display: flex; align-items: center; justify-content: center;">
<spine-widget
identifier="windmill-game"
atlas="assets/windmill-ess.atlas"
skeleton="assets/windmill-ess.json"
animation="animation"
isinteractive
></spine-widget>
<spine-widget
identifier="spineboy-game"
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.json"
animation="hoverboard"
fit="none"
></spine-widget>
</div>
</div>
</div>
<div class="split-right">
<div class="top" style="flex: 20%; flex-direction: column; gap: 10px; display: flex; align-items: center; justify-content: center; align-items: stretch; height: 100%;">
<div class="instruction">
Use WASD to move around!
</div>
<div class="instruction">
<div id="killed" class="title"></div>
<div style="flex: 70%;"">Save the flowers from the white pest by shooting them</div>
</div>
<div class="instruction">
<div id="ammo" class="title"></div>
<div>Go to the red colored rooster of bush when ammo is low</div>
</div>
<div class="instruction">
<div id="level" class="title"></div>
<div>Reach level 10 to win the game</div>
</div>
</div>
</div>
</div>
</div>
<script type="module">
import * as spine from '../dist/esm/spine-widget.mjs';
const winPanel = document.getElementById("win-panel");
const killedSpan = document.getElementById("killed");
const ammoSpan = document.getElementById("ammo");
const levelSpan = document.getElementById("level");
const containerGame = document.getElementById("container-game");
(async () => {
const spineboy = spine.getSpineWidget("spineboy-game");
const windmill = spine.getSpineWidget("windmill-game");
await Promise.all([spineboy.whenReady, windmill.whenReady]);
spineboy.state.setAnimation(2, "aim", true);
spineboy.skeleton.slots.forEach(slot => {
if (slot.data.name === "gun") {
spineboy.addCursorSlotEventCallback(slot, (slot,event) => {
if (event === "down") {
spineboy.state.setAnimation(1, "shoot", false);
}
});
}
if (slot.data.name === "torso") {
spineboy.addCursorSlotEventCallback(slot, (slot,event) => {
if (event === "down") {
spineboy.state.setAnimation(0, "jump", false).mixDuration = 0.2;
spineboy.state.addAnimation(0, "walk", true).mixDuration = 0.2;
}
});
}
if (slot.data.name === "head") {
spineboy.addCursorSlotEventCallback(slot, (slot,event) => {
if (event === "down") {
spineboy.state.setAnimation(1, "run", true).mixDuration = 0.2;
} else {
if (event === "up") {
spineboy.state.setEmptyAnimation(1, 1);
spineboy.state.clearTrack(1);
}
}
});
}
});
const tempVector = new spine.Vector2();
const crosshairSlot = spineboy.skeleton.findSlot("crosshair");
crosshairSlot.color.a = 0;
const crosshairBone = crosshairSlot.bone;
let points = 0;
let ammo = 5;
let killed = 0;
let level = 1;
ammoSpan.innerText = `Ammo: ${ammo}`;
killedSpan.innerText = `Saved: ${killed}`;
levelSpan.innerText = `Level: ${level}`;
const flowers = [];
const ammoLocations = [];
windmill.skeleton.slots.forEach(slot => {
if (slot.data.name === "rooster" || slot.data.name === "bush1") {
ammoLocations.push(slot)
}
if (!Number.isNaN(parseInt(slot.data.name.replace("flower", ""))) || slot.data.name === "flower") {
slot.color.set(1, 0, 0, 1);
flowers.push(slot);
windmill.addCursorSlotEventCallback(slot, (slot, event) => {
if (ammo === 0) return;
if (event !== "down") return;
if (slot.color.g === 0) return;
spineboy.state.setAnimation(1, "shoot", false);
ammo--;
points++;
killed++;
if (points === 10) {
level++
points = 0
maxTime -= 100
}
ammoSpan.innerText = `Ammo: ${ammo}`;
killedSpan.innerText = `Saved: ${killed}`;
tempVector.x = slot.bone.x;
tempVector.y = slot.bone.y;
slot.bone.parentToWorld(tempVector);
crosshairBone.x = (tempVector.x + (windmill.worldX - spineboy.worldX) - spineboy.skeleton.x) / spineboy.skeleton.scaleX;
crosshairBone.y = (tempVector.y + (windmill.worldY - spineboy.worldY) - spineboy.skeleton.y) / spineboy.skeleton.scaleY;
slot.color.set(1, 0, 0, 1);
});
}
});
let time = 0;
let maxTime = 1000;
let ammoLocationActive = false;
let gameVectorTemp = new spine.Vector3();
let interval = setInterval(() => {
if (!ammoLocationActive && ammo <= 2) {
ammoLocationActive = true;
spineboy.overlay.worldToScreen(gameVectorTemp, spineboy.worldX + spineboy.skeleton.x, spineboy.worldY + spineboy.skeleton.y);
const { x, width } = containerGame.getBoundingClientRect();
const containerGameMiddle = x + width / 2;
const left = gameVectorTemp.x < containerGameMiddle;
const ammoLocation = ammoLocations[left ? 1 : 0];
(ammoLocation.darkColor ||= new spine.Color()).set(1, 0, 0, 1);
levelSpan.innerText = `Level: ${level} / 10`;
};
if (time >= maxTime) {
time = 0;
const flower = random(flowers);
flower.color.set(1, 1, 1, 1);
}
if (checkLoseCondition()) {
endGame(false, interval);
return;
}
if (checkWinCondition()) {
levelSpan.innerText = `Level: 10 / 10`;
endGame(true, interval);
return;
}
time += 100;
}, 100);
const checkWinCondition = () => level === 10;
const checkLoseCondition = () => flowers.every((flowerSlot) => flowerSlot.color.g !== 0);
const endGame = (win, interval) => {
clearInterval(interval);
winPanel.style.display = "flex";
winPanel.innerText = win ? "YOU WIN!" : "YOU LOSE!"
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keyup', handleKeyUp);
keys.w = false;
keys.a = false;
keys.s = false;
keys.d = false;
spineboy.state.setAnimation(1, win ? "jump" : "death", win);
}
const random = array => array[Math.floor(Math.random() * array.length)];
const MOVE_SPEED = 350;
const keys = {
w: false,
a: false,
s: false,
d: false
};
function handleKeyDown(e) {
const key = e.key.toLowerCase();
if (key in keys) {
keys[key] = true;
}
}
function handleKeyUp(e) {
const key = e.key.toLowerCase();
if (key in keys) keys[key] = false;
}
spineboy.skeleton.x += 0
spineboy.skeleton.y -= 330
let direction = 1;
spineboy.beforeUpdateWorldTransforms = (delta) => {
let posX = 0;
let posY = 0;
// Move based on pressed keys
const inc = (MOVE_SPEED * delta) * windmill.skeleton.scaleX;
if (keys.w) posY -= inc;
if (keys.a) posX -= inc;
if (keys.s) posY += inc;
if (keys.d) posX += inc;
// Update visual position
spineboy.skeleton.x += posX;
spineboy.skeleton.y -= posY;
direction = posX < 0 ? direction = -1 : posX > 0 ? 1 : direction;
spineboy.skeleton.scaleX = .25 * windmill.skeleton.scaleX * direction;
spineboy.skeleton.scaleY = .25 * windmill.skeleton.scaleY;
const spineboyPosition = {
x: spineboy.worldX + spineboy.skeleton.x - spineboy.bounds.width / 2 * spineboy.skeleton.scaleX * direction,
y: spineboy.worldY + spineboy.skeleton.y ,
width: spineboy.bounds.width * spineboy.skeleton.scaleX * direction,
height: spineboy.bounds.height * spineboy.skeleton.scaleY,
}
ammoLocations.forEach(element => {
const width = element.attachment.region.width * windmill.skeleton.scaleX;
const height = element.attachment.region.height * windmill.skeleton.scaleY;
const x = windmill.worldX + element.bone.worldX - width / 2;
const y = windmill.worldY + element.bone.worldY - height / 2;
if (isIntersecting(spineboyPosition, { x, y, width, height })) {
if (element.darkColor) {
ammo = 5;
element.darkColor = null;
ammoLocationActive = false;
ammoSpan.innerText = `Ammo: ${ammo}`;
}
}
});
}
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
})();
function isIntersecting(rect1, rect2) {
return (
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}
</script>
</body>
</html>

View File

@ -0,0 +1,246 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webcomponent GUI</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
font-size: 16px;
}
</style>
</head>
<body>
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 10px; gap: 10px;">
<span id="ruler" style="visibility: hidden; white-space: nowrap; position: absolute"></span>
<div style="background-color: white; width: 250px; padding: 30px; text-align: center; border-radius: 10px; border: 3px solid black; box-shadow: 5px 5px rgb(0, 0, 0);">
<div style="display: flex; justify-content: center;">
<div style="width: 150px; height:150px; border-radius: 5%; border: 1px solid rgb(113, 113, 113); background-color: rgb(211, 211, 211); margin-bottom: 30px;">
<spine-widget
identifier="spineboy-login"
atlas="assets/pwd/chibi-stickers-pro-pwd-test.atlas"
skeleton="assets/pwd/chibi-stickers.json"
skin="spineboy"
bounds-x="-177"
bounds-y="238"
bounds-width="364"
bounds-height="412"
animation="interactive/head/idle"
clip
isinteractive
></spine-widget>
</div>
</div>
<div class="input-group" style="margin-bottom: 15px;"><select id="skinSelect"></select></div>
<form id="loginForm">
<div class="input-group" style="margin-bottom: 15px;">
<input style="width: 100%; padding: 10px; box-sizing: border-box;" type="text" id="username" name="username" placeholder="Username" autocomplete="off" required>
</div>
<div class="input-group" style="margin-bottom: 15px;">
<input style="width: 100%; padding: 10px; box-sizing: border-box;" type="password" id="password" name="password" placeholder="Password" required>
</div>
<div style="margin-bottom: 10px; color: black">Password is <code>password</code></div>
<div style="height: 75px; cursor: pointer;">
<div id="button-text" style="font-size: xx-large; cursor: pointer; user-select: none; display: none;"> LOGIN </div>
<spine-widget
identifier="button-login"
atlas="assets/pwd/button.atlas"
skeleton="assets/pwd/button.json"
animation="idle"
isinteractive
fit="fill"
></spine-widget>
</div>
</form>
</div>
<div style="background-color: white; padding: 10px; border-radius: 10px; border: 3px solid black; box-shadow: 5px 5px rgb(0, 0, 0);">
A login UI made using the chibi stickers and a button made using
<p>
The chibi sticker does the following:
<ul>
<li>It looks at the cursor when no input field is selected</li>
<li>Look at the caret when username input field is selected</li>
<li>Cover its eyes when password input field is selected</li>
<li>React in two different ways depending on the password</li>
</ul>
</p>
<p>
The button does the following:
<ul>
<li>Starts some animation when cursor enters, leaves, stays, or click the button</li>
<li>Appends a div containing the <code>LOGIN</code> text to a slot</li>
<li>Submits the form on click</li>
</ul>
</p>
</div>
</div>
<script type="module">
import { getSpineWidget, MixBlend } from '../dist/esm/spine-widget.mjs';
const mouseX = Smooth(0, 200);
const mouseY = Smooth(0, 200);
(async () => {
const form = document.getElementById('loginForm');
const widgetButton = getSpineWidget("button-login");
await widgetButton.whenReady;
widgetButton.skeleton.color.set(.85, .85, .85, 1);
widgetButton.cursorEventCallback = (event, originalEvent) => {
if (event === "enter") {
widgetButton.state.setAnimation(0, "enhance-in", false);
widgetButton.state.setAnimation(1, "shadow-in", false);
widgetButton.state.setAnimation(2, "jump", true);
};
if (event === "leave") {
widgetButton.state.setAnimation(0, "enhance-out", false).timeScale = 2;
widgetButton.state.setAnimation(1, "shadow-out", false);
widgetButton.state.setEmptyAnimation(2, 0).mixDuration = .25;
};
if (event === "down") {
widgetButton.state.setAnimation(0, "enhance-in", false).timeScale = 2;
originalEvent.preventDefault();
if (form.reportValidity()) {
if (passwordInput.value === "password") {
widget.state.setAnimation(0, "interactive/pwd/hooray", 0);
widget.state.addAnimation(0, "interactive/head/idle", true);
} else {
widget.state.setAnimation(0, "interactive/pwd/sad", 0);
widget.state.addAnimation(0, "interactive/head/idle", true);
}
}
};
}
const textSlot = widgetButton.skeleton.findSlot("CLICK ME");
textSlot.setAttachment(null);
const divText = document.getElementById("button-text");
widgetButton.followSlot("CLICK ME", divText, { followScale: false });
const widget = getSpineWidget("spineboy-login");
await widget.whenReady;
// default settings
widget.state.data.defaultMix = 0.15;
// Skin selection
const skinSelect = document.getElementById('skinSelect');
widget.skeleton.data.skins.forEach(({ name }) => {
if (name === "default") return;
skinSelect.add(new Option(name, name, name === "spineboy"));
});
skinSelect.value = "spineboy";
skinSelect.addEventListener('change', (e) => widget.skin = e.target.value);
// click on spineboy
widget.cursorEventCallback = (event) => {
if (event === "down") {
widget.state.setAnimation(0, "interactive/pwd/touch", false);
widget.state.addAnimation(0, "interactive/head/idle", true);
}
if (event === "enter") widget.state.setAnimation(0, "emotes/wave", true);
if (event === "leave") widget.state.setAnimation(0, "interactive/head/idle", false);
}
// Head follow logic
let focusInput = false;
// Password input field
const passwordInput = document.getElementById('password');
passwordInput.addEventListener('focus', () => {
focusInput = true;
resetBlendTracks();
widget.state.setAnimation(0, "interactive/pwd/hide", 0);
});
passwordInput.addEventListener('blur', () => {
focusInput = false;
widget.state.setAnimation(0, "interactive/head/idle", 0);
});
// Username input field
const usernameInput = document.getElementById('username');
usernameInput.addEventListener('input', () => {
setBlendTracks();
});
usernameInput.addEventListener('focus', () => {
focusInput = true;
setBlendTracks();
});
usernameInput.addEventListener('blur', () => {
focusInput = false;
setBlendTracks();
});
// Animation left/down blending
const ALPHA_LEFT_RANGE = 4;
const ALPHA_DOWN = .8;
const ALPHA_DOWN_RANGE = 2;
const moveVector = [0, 0];
const leftTrack = widget.state.setAnimation(1, "interactive/head/left", true);
const downTrack = widget.state.setAnimation(2, "interactive/head/down", true);
leftTrack.mixBlend = MixBlend.add;
downTrack.mixBlend = MixBlend.add;
const setBlendTracks = () => {
moveVector[0] = getAlphaLeft();
moveVector[1] = ALPHA_DOWN;
}
const resetBlendTracks = () => {
moveVector[0] = 0;
moveVector[1] = 0;
}
resetBlendTracks();
const getWidthOfText = (txt) => {
const ruler = document.getElementById("ruler");
ruler.innerHTML = txt;
return ruler.offsetWidth;
}
const getAlphaLeft = () => {
const { width } = usernameInput.getBoundingClientRect();
const length = getWidthOfText(usernameInput.value);
const alpha = Math.min(1, length / width) * 2
return 1 - alpha;
};
widget.afterUpdateWorldTransforms = () => {
if (focusInput) {
blendTo(moveVector[0], moveVector[1]);
} else {
const { x, y, width, height} = widget.getHostElement().getBoundingClientRect();
const l = -((widget.overlay.cursorCanvasX - (x + width)) / window.innerWidth) * ALPHA_LEFT_RANGE;
const d = ((widget.overlay.cursorCanvasY - (y + height)) / window.innerHeight) * ALPHA_DOWN_RANGE;
blendTo(l, d);
}
};
const blendTo = (x, y) => {
leftTrack.alpha = mouseX(x);
downTrack.alpha = mouseY(y);
}
})();
function Smooth(f,t){var p=performance,b=p.now(),o=f,s=0,m,n,x,d,k=f;return function(v){n=p.now()-b;m=t*t/n/n;d=o-f;x=s*t+d+d;if(n>0)k=n<t?o-x/m/t*n+(x+x-d)/m-s*n-d:o;if(v!=void 0&&v!=o){f=k;b+=n;o=v;s=n>0&&n<t?s+3*x/m/t-(4*x-d-d)/m/n:0}return k}}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,6 @@ interface Rectangle extends Point {
height: number,
}
type BeforeAfterUpdateSpineWidgetFunction = (skeleton: Skeleton, state: AnimationState) => void;
type UpdateSpineWidgetFunction = (delta: number, skeleton: Skeleton, state: AnimationState) => void;
export type OffScreenUpdateBehaviourType = "pause" | "update" | "pose";
@ -207,8 +206,8 @@ interface WidgetAttributes {
// The methods user can override to have custom behaviour
interface WidgetOverridableMethods {
update?: UpdateSpineWidgetFunction;
beforeUpdateWorldTransforms: BeforeAfterUpdateSpineWidgetFunction;
afterUpdateWorldTransforms: BeforeAfterUpdateSpineWidgetFunction;
beforeUpdateWorldTransforms: UpdateSpineWidgetFunction;
afterUpdateWorldTransforms: UpdateSpineWidgetFunction;
onScreenFunction: (widget: SpineWebComponentWidget) => void
}
@ -657,12 +656,12 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
/**
* This callback is invoked before the world transforms are computed allows to execute additional logic.
*/
public beforeUpdateWorldTransforms: BeforeAfterUpdateSpineWidgetFunction = () => { };
public beforeUpdateWorldTransforms: UpdateSpineWidgetFunction = () => { };
/**
* This callback is invoked after the world transforms are computed allows to execute additional logic.
*/
public afterUpdateWorldTransforms: BeforeAfterUpdateSpineWidgetFunction = () => { };
public afterUpdateWorldTransforms: UpdateSpineWidgetFunction = () => { };
/**
* A callback invoked each time the element container enters the screen viewport.
@ -1801,9 +1800,9 @@ class SpineWebComponentOverlay extends HTMLElement implements OverlayAttributes,
if (onScreen || (!onScreen && offScreenUpdateBehaviour === "pose")) {
state.apply(skeleton);
beforeUpdateWorldTransforms(skeleton, state);
beforeUpdateWorldTransforms(delta, skeleton, state);
skeleton.updateWorldTransform(Physics.update);
afterUpdateWorldTransforms(skeleton, state);
afterUpdateWorldTransforms(delta, skeleton, state);
}
}
}