Project Management
5 TopicsGamified Onboarding
Collect the gems by exploring the office! This gamified eLearning was developed for team members to explore their workspace while getting some product knowledge. The process: Conducting a training needs analysis through discussions with leaders Building a learner persona using surveys and polls Planning the desired outcomes Create a fun course Explore the project.1.3KViews6likes4Comments'Cup and Ball' game using sliders with dynamic Z-index control
Hello! For a little while now, I've been experimenting with customised sliders as a more accessible alternative to drag-and-drop. By changing the appearance of the slider thumb and making the track invisible, I've created everything from claw machines, to catapults and animated superhero characters. Believe it or not, this 'Cup and Ball' game is also built using sliders... and a bit of Javascript: Play Beat the Machines here Why sliders? Sliders are one of the most versatile, accessible, and customisable objects in Storyline. Straight out of the box, they come with: A built-in variable Key press controls (tab and arrow keys) State changes (including Disabled and Hidden) Fill, outline and transparency settings and they can also be animated with Entrance and Exit animations and motion paths. However, despite this level of customisation, using more than one slider in the same area can be tricky, as the thumb or track can block the elements beneath them, even when they are invisible. This is why, until now, I have only ever used one slider at a time. But then I discovered that it was possible to control the z-index of sliders using Javascript, to move the active slider above the other sliders. Even pirates have table manners Recently, I used this technique to change the relative position three sliders for the E-Learning Heroes 'How to Set a Dinner Table Challenge'. You can read more about this here. (Thanks again for your help, Nedim!) Cups and balls This demo required a slightly different approach, as the three items are identical in size and shape and at times can completely overlap each other. This reduced the 'grabbable' area of each thumb as you can see below: To mitigate this, this code manages the z-index of the three sliders, ensuring the one being hovered over or touched appears on top of the others. const object1 = document.querySelector("[data-model-id='6RsJm5AiIs6']"); const object2 = document.querySelector("[data-model-id='5aVi8WZUooi']"); const object3 = document.querySelector("[data-model-id='6regUP43dQ1']"); function bringToFront(element) { const elements = [object1, object2, object3]; elements.forEach(obj => { obj.style.setProperty("z-index", "100", "important") }); element.style.setProperty("z-index", "300", "important"); } // Add hover/touch listeners to each slider [object1, object2, object3].forEach(slider => { // For mouse users slider.addEventListener('mouseover', () => bringToFront(slider)); // For touch devices slider.addEventListener('touchstart', () => bringToFront(slider)); }); // Initial setup bringToFront(object1); And this code also ensures that when each slider is active (being clicked, grabbed and moved) it jumps to the front: const object1 = document.querySelector("[data-model-id='6RsJm5AiIs6']"); const object2 = document.querySelector("[data-model-id='5aVi8WZUooi']"); const object3 = document.querySelector("[data-model-id='6regUP43dQ1']"); function bringToFront(element) { const elements = [object1, object2, object3]; elements.forEach(obj => { obj.style.setProperty("z-index", "100", "important"); }); element.style.setProperty("z-index", "300", "important"); } bringToFront(object1); This lends the appearance of the cups passing in front of one another when dragged: This works a little better on a computer with a mouse than with a touchscreen, but on both platforms it's no 'stickier' than a traditional drag-and-drop. Collision detection So, you may have noticed that it's pretty much impossible to place the three cups on top of each other. This is because the position of each slider is being constantly monitored by this code, which then amends the position of each slider as necessary: let activeSlider = null; let lastMovedSlider = null; let moveCount = 0; let checkInterval = null; const MAX_MOVES = 3; const MIN_SPACING = 2; function resetState() { activeSlider = null; lastMovedSlider = null; moveCount = 0; if (checkInterval) { clearInterval(checkInterval); } checkInterval = setInterval(checkPartialOverlaps, 10); } function cleanup() { if (checkInterval) { clearInterval(checkInterval); checkInterval = null; } activeSlider = null; lastMovedSlider = null; moveCount = 0; } function validatePositions(positions) { for(let i = 1; i < positions.length; i++) { if(Math.abs(positions[i].pos - positions[i-1].pos) < MIN_SPACING) { return false; } } return true; } function findEmptySpaces(positions) { let spaces = []; for(let i = 0; i <= 10; i++) { if(!positions.find(p => p.pos === i)) { spaces.push(i); } } return spaces; } function adjustPosition(position, direction, min, max) { return Math.max(min, Math.min(max, position + (direction === 'Right' ? 2 : -2))); } function checkPartialOverlaps() { if (!activeSlider) { moveCount = 0; const slider1 = GetPlayer().GetVar("Slider1"); const slider2 = GetPlayer().GetVar("Slider2"); const slider3 = GetPlayer().GetVar("Slider3"); const direction = GetPlayer().GetVar("Direction"); let positions = [ {id: 1, pos: slider1, lastMoved: lastMovedSlider === 1}, {id: 2, pos: slider2, lastMoved: lastMovedSlider === 2}, {id: 3, pos: slider3, lastMoved: lastMovedSlider === 3} ].sort((a, b) => a.pos - b.pos); for(let i = 1; i < positions.length; i++) { if(Math.abs(positions[i].pos - positions[i-1].pos) < MIN_SPACING && moveCount < MAX_MOVES) { moveCount++; const displacedSlider = !positions[i].lastMoved ? positions[i] : positions[i-1]; let newPos = displacedSlider.pos; if(direction === "Right") { newPos = Math.max(0, displacedSlider.pos - MIN_SPACING); } else { newPos = Math.min(10, displacedSlider.pos + MIN_SPACING); } let testPositions = positions.map(p => ({...p})); const sliderIndex = testPositions.findIndex(p => p.id === displacedSlider.id); testPositions[sliderIndex].pos = newPos; testPositions.sort((a, b) => a.pos - b.pos); if(validatePositions(testPositions)) { GetPlayer().SetVar(`Slider${displacedSlider.id}`, newPos); GetPlayer().SetVar("Teeter", "Item " + displacedSlider.id); return; } if(positions[2].pos >= 9 || positions[0].pos <= 1) { const inactiveSliders = positions.filter(p => !p.lastMoved); let validMove = true; inactiveSliders.forEach((slider, idx) => { const newPosition = direction === "Right" ? Math.max(0, slider.pos - MIN_SPACING) : Math.min(10, slider.pos + MIN_SPACING); let testPositions = positions.map(p => ({...p})); const index = testPositions.findIndex(p => p.id === slider.id); testPositions[index].pos = newPosition; testPositions.sort((a, b) => a.pos - b.pos); if(validatePositions(testPositions)) { GetPlayer().SetVar(`Slider${slider.id}`, newPosition); GetPlayer().SetVar("Teeter", "Item " + slider.id); } else { validMove = false; } }); if(validMove) return; } } } } } document.addEventListener('mousedown', (e) => { const slider = e.target.closest('[data-model-id]'); if (slider) { activeSlider = parseInt(slider.getAttribute('data-model-id').slice(-1)); lastMovedSlider = activeSlider; } }); document.addEventListener('mouseup', () => { activeSlider = null; checkPartialOverlaps(); }); // Reset state when slide loads resetState(); // Cleanup when slide unloads/revisits window.addEventListener('unload', cleanup); // Listen for slide revisits const slideObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.target.style.display === 'block') { resetState(); } }); }); slideObserver.observe(document.querySelector('.slide-container'), { attributes: true, attributeFilter: ['style'] Additionally, this code runs every 0.5 secs to resolve any overlaps and 'bunching together' of the cups: function restoreSpacing() { const positions = [ {id: 1, pos: GetPlayer().GetVar("Slider1")}, {id: 2, pos: GetPlayer().GetVar("Slider2")}, {id: 3, pos: GetPlayer().GetVar("Slider3")} ]; const needsSpacing = positions.some((p1, i) => positions.some((p2, j) => i !== j && Math.abs(p1.pos - p2.pos) < 2) ); if(needsSpacing) { const sorted = [...positions].sort((a, b) => a.pos - b.pos); sorted[0].pos = Math.max(0, sorted[0].pos); sorted[1].pos = Math.min(8, Math.max(sorted[0].pos + 2, sorted[1].pos)); sorted[2].pos = Math.min(10, Math.max(sorted[1].pos + 2, sorted[2].pos)); positions.forEach(item => { GetPlayer().SetVar(`Slider${item.id}`, Math.round(item.pos)); }); GetPlayer().SetVar("Teeter", "All Items"); // Reset Teeter after 500ms setTimeout(() => { GetPlayer().SetVar("Teeter", ""); }, 500); } } setInterval(restoreSpacing, 500); And, finally, this code monitors the direction the active slider is travelling in and also counts the number of times the cups are swapped: let lastSliderValues = { 1: GetPlayer().GetVar("Slider1"), 2: GetPlayer().GetVar("Slider2"), 3: GetPlayer().GetVar("Slider3") }; let lastSignificantPositions = {...lastSliderValues}; const MOVEMENT_THRESHOLD = 2; function checkSliderChanges() { const slider1New = GetPlayer().GetVar("Slider1"); const slider2New = GetPlayer().GetVar("Slider2"); const slider3New = GetPlayer().GetVar("Slider3"); if (slider1New !== lastSliderValues[1] || slider2New !== lastSliderValues[2] || slider3New !== lastSliderValues[3]) { let changedValue, oldValue, sliderId; if (slider1New !== lastSliderValues[1]) { changedValue = slider1New; oldValue = lastSliderValues[1]; sliderId = 1; } if (slider2New !== lastSliderValues[2]) { changedValue = slider2New; oldValue = lastSliderValues[2]; sliderId = 2; } if (slider3New !== lastSliderValues[3]) { changedValue = slider3New; oldValue = lastSliderValues[3]; sliderId = 3; } // Only count as swap if movement exceeds threshold if (Math.abs(changedValue - lastSignificantPositions[sliderId]) >= MOVEMENT_THRESHOLD) { const currentSwaps = GetPlayer().GetVar("Number_Swaps"); GetPlayer().SetVar("Number_Swaps", currentSwaps + 1); lastSignificantPositions[sliderId] = changedValue; } if (changedValue >= 3 && changedValue <= 6) { GetPlayer().SetVar("Direction", "Middle"); } else { GetPlayer().SetVar("Direction", changedValue > oldValue ? "Right" : "Left"); } lastSliderValues = {1: slider1New, 2: slider2New, 3: slider3New}; } } setInterval(checkSliderChanges, 5); Performance is variable Knowing the position of each slider based on its underlying variable is another reason why I like using sliders in this way. I have used this information to make the robot turn its head as it tries to follow your movements and, ultimately, score your performance. The entire demo is just three slides long, and players revisit the game slide up to three times to reach the end. I haven't shared my master file as I am still tinkering with it, and it will probably become a portfolio piece. But it's been a while since I have used Storyline to create such a game, and I thought others would like to see what is going on under the hood. If you have any questions at all, please ask in the comments below. Enjoy your chips!253Views1like4CommentsMobile Version Demo Micro Course : (RAG) Inspired by Mobile for Mobile Users
This is the alpha version of my micro-learning course on Retrieval Augmented Generation (RAG). The course has been developed with mobile users in mind, ensuring a responsive design that enhances learning on the go. It offers a brief, engaging introduction to the RAG model, focusing on its role in improving question-answering systems. As a work-in-progress demo, I welcome your feedback to help me refine this course and enhance the overall user experience. Your insights will assist me in improving the course's content, design, and functionality. #mobile #example #demo-work #prototype #vrendramoakdam Review Demo Here : Micro-Course Link268Views2likes0Comments