Browse Source

Moved some examples in their own pages.

Davide Tantillo 4 months ago
parent
commit
2bf85467dc

+ 4 - 1
spine-ts/index.html

@@ -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>

+ 690 - 0
spine-ts/spine-widget/example/app.html

@@ -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>

+ 332 - 0
spine-ts/spine-widget/example/game.html

@@ -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>

+ 246 - 0
spine-ts/spine-widget/example/login.html

@@ -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>

+ 2 - 1274
spine-ts/spine-widget/example/tutorial.html

@@ -2360,7 +2360,7 @@ stretchyman.update = (canvas, delta, skeleton, state) => {
                     const [tank, tank2] = await Promise.all([
                         spine.getSpineWidget("tank").whenReady, spine.getSpineWidget("tank2").whenReady]);
 
-                    tank.beforeUpdateWorldTransforms = (skeleton, state) => {
+                    tank.beforeUpdateWorldTransforms = (delta, skeleton, state) => {
                         if (!tank.onScreen) return;
                         tank.skeleton.scaleX = tank2.skeleton.scaleX;
                         tank.skeleton.scaleY = tank2.skeleton.scaleY;
@@ -2399,7 +2399,7 @@ stretchyman.update = (canvas, delta, skeleton, state) => {
 const [tank, tank2] = await Promise.all([
     spine.getSpineWidget("tank").whenReady, spine.getSpineWidget("tank2").whenReady]);
 
-tank.beforeUpdateWorldTransforms = (skeleton, state) => {
+tank.beforeUpdateWorldTransforms = (delta, skeleton, state) => {
     if (!tank.onScreen) return;
     tank.skeleton.scaleX = tank2.skeleton.scaleX;
     tank.skeleton.scaleY = tank2.skeleton.scaleY;
@@ -3415,1278 +3415,6 @@ const darkPicker = document.getElementById("dark-picker");
     ///////
     -->
 
-    <!--
-    /////////////////////
-    // start section //
-    /////////////////////
-    -->
-
-    <div class="section vertical-split">
-
-        <div class="split-left">
-            A login UI made using the chibi stickers and a button made using Spine.
-            <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>
-
-
-
-        <span id="ruler" style="visibility: hidden; white-space: nowrap; position: absolute"></span>
-        <div style="background-color: white; width: 250px; padding: 30px; text-align: center;">
-            <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>
-
-
-
-
-
-        <script>
-            const mouseX = Smooth(0, 200);
-            const mouseY = Smooth(0, 200);
-
-            (async () => {
-
-                const form = document.getElementById('loginForm');
-                const widgetButton = spine.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 = spine.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 = spine.MixBlend.add;
-                downTrack.mixBlend = spine.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);
-                }
-
-            })();
-
-        </script>
-
-
-        <div class="split-bottom">
-            <pre><code id="code-display">
-                <script>escapeHTMLandInject(`
-Look at the source code`
-);</script>
-            </code></pre>
-        </div>
-
-    </div>
-
-    <!--
-    /////////////////////
-    // end section //
-    /////////////////////
-    -->
-
-
-
-    <!--
-    /////////////////////
-    // start section //
-    /////////////////////
-    -->
-
-
-    <div class="section vertical-split">
-
-        <style>
-
-            .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>
-
-
-        <div style="max-width: 300px; text-align: center; margin: 10px;">
-            <div class="phone">
-                <spine-overlay overlay-id="phone" scrollable></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" onclick="nextScreen()">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;" onclick="prevScreen()">Prev</button>
-                                    <button id="btn-next-2" class="btn" style="padding: 5px; background-color: gainsboro; transition: background-color 0.25s ease-in-out;" onclick="nextScreen()" 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;" onclick="prevScreen()">Prev</button>
-                                    <button id="btn-next-3" class="btn" style="padding: 5px; background-color: gainsboro; transition: background-color 0.25s ease-in-out;" onclick="nextScreen()" 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;" onclick="prevScreen()">Prev</button>
-                                </div>
-                            </div>
-
-                        </div>
-
-                    </div>
-                    <!-- SECTION 4 -->
-
-                </div>
-            </div>
-        </div>
-
-        <script>
-            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}%)`;
-                }
-            }
-        </script>
-
-        <script>
-            (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 */
-            })();
-        </script>
-
-
-    <div class="split-bottom">
-        <pre><code id="code-display">
-            <script>escapeHTMLandInject(`
-Look at the source code`
-);</script>
-        </code></pre>
-    </div>
-
-    </div>
-
-
-    <!--
-    /////////////////////
-    // end section //
-    /////////////////////
-    -->
-
-
-
-    <!--
-    /////////////////////
-    // start section //
-    /////////////////////
-    -->
-
-    <div class="section vertical-split" id="spneboy-game">
-
-        <div class="split-top split">
-            <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 style="display: flex; flex-direction: column; border: 3px solid black; border-radius: 10px; box-shadow: 5px 5px 15px grey; padding: 10px; ">
-                        Use WASD to move around!
-                    </div>
-
-                    <div style="display: flex; flex-direction: column; border: 3px solid black; border-radius: 10px; box-shadow: 5px 5px 15px grey; padding: 10px; ">
-                        <div id="killed" style="flex: 30%; font-weight: bold; text-transform: uppercase; border-bottom: 1px solid white; text-align: center;"></div>
-                        <div style="flex: 70%;"">Save the flowers from the white pest by shooting them</div>
-                    </div>
-
-                    <div style="display: flex; flex-direction: column; border: 3px solid black; border-radius: 10px; box-shadow: 5px 5px 15px grey; padding: 10px; ">
-                        <div id="ammo" style="flex: 30%; font-weight: bold; text-transform: uppercase; border-bottom: 1px solid white; text-align: center;"></div>
-                        <div>Go to the red colored rooster of bush when ammo is low</div>
-                    </div>
-
-                    <div style="display: flex; flex-direction: column; border: 3px solid black; border-radius: 10px; box-shadow: 5px 5px 15px grey; padding: 10px; ">
-                        <div id="level" style="flex: 30%; font-weight: bold; text-transform: uppercase; border-bottom: 1px solid white; text-align: center;"></div>
-                        <div>Reach level 10 to win the game</div>
-                    </div>
-
-                </div>
-            </div>
-        </div>
-
-        <script>
-            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()) {
-                        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 = 2.5;
-                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 = () => {
-                    let posX = 0;
-                    let posY = 0;
-
-                    // Move based on pressed keys
-                    const inc = MOVE_SPEED * 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>
-
-
-        <div class="split-bottom">
-            <pre><code id="code-display">
-                <script>
-                escapeHTMLandInject(`
-Look at the source code`)
-                </script>
-            </code></pre>
-        </div>
-    </div>
-
-    <!--
-    /////////////////////
-    // end section //
-    /////////////////////
-    -->
-
 <script>
     spine.SpineWebComponentWidget.SHOW_FPS = true;
 </script>

+ 6 - 7
spine-ts/spine-widget/src/SpineWebComponentWidget.ts

@@ -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);
 					}
 				}
 			}