Last active
April 17, 2024 04:54
-
-
Save rushkeldon/ca5791003f87f46674218a4de3aa9fef to your computer and use it in GitHub Desktop.
gamify Jira with sound effects and fireworks when moving tickets by dragging
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
canvas { | |
position: absolute; | |
left: 0; | |
right: 0; | |
top: 0; | |
bottom: 0; | |
width: 100vw; | |
height: 100vh; | |
z-index: 1000; | |
margin: auto; | |
transition: opacity 0.75s ease-out; | |
opacity: 1; | |
} | |
canvas.outro { | |
opacity: 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const gamify = () => { | |
let tickets = document.querySelectorAll('.js-parent-drag'); | |
if (!tickets.length) return; | |
tickets.forEach((ticket) => { | |
if (!isGamified(ticket)) { | |
ticket.removeEventListener('mousedown', ticketPressed, true); | |
ticket.addEventListener('mousedown', ticketPressed, true); | |
ticket.removeEventListener('mouseup', ticketReleased, true); | |
ticket.addEventListener('mouseup', ticketReleased, true); | |
setGamified(ticket); | |
} | |
}); | |
}; | |
function ticketPressed(e) { | |
e.target.setAttribute('data-x-coord', getX(e.target)); | |
} | |
function ticketReleased(e) { | |
const startX = parseInt(e?.target?.getAttribute('data-x-coord')); | |
if (isNaN(startX)) return; | |
const endX = getX(e.target); | |
let mp3 = 'https://appcloud9.com/snd/'; | |
switch (true) { | |
case Math.abs(startX - endX) <= 20 : | |
mp3 += 'neutral.mp3'; | |
break; | |
case startX < endX : | |
mp3 += 'powerup.mp3'; | |
createFireworks(); | |
break; | |
case startX > endX : | |
mp3 += 'powerdown.mp3'; | |
break; | |
default : | |
console.log('unhandled case encountered by ticketReleased.'); | |
} | |
playSound(mp3); | |
unsetGamfied(e.target); | |
window.setTimeout(gamify, 250); | |
} | |
function setGamified(el) { | |
el.setAttribute('data-gamified', true); | |
} | |
function unsetGamfied(el) { | |
el.removeAttribute('data-gamified'); | |
} | |
function isGamified(el) { | |
return el.hasAttribute('data-gamified'); | |
} | |
function getX(el) { | |
return el.getBoundingClientRect().left; | |
} | |
function playSound(url) { | |
const audio = new Audio(url); | |
audio.play(); | |
} | |
function createFireworks() { | |
const canvas = document.createElement('canvas'); | |
document.body.appendChild(canvas); | |
var ctx = canvas.getContext('2d'), | |
cw = window.innerWidth, | |
ch = window.innerHeight, | |
fireworks = [], | |
particles = [], | |
hue = 120, | |
limiterTotal = 20, | |
limiterTick = 0, | |
timerTotal = 0, | |
randomTime = 0, | |
timerTick = 0, | |
mousedown = false, | |
mx, | |
my; | |
canvas.width = cw; | |
canvas.height = ch; | |
var snd = new Audio('https://appcloud9.com/snd/firework.mp3'); | |
function random(min, max) { | |
return min + Math.random() * (max - min); | |
} | |
function calculateDistance(p1x, p1y, p2x, p2y) { | |
return Math.sqrt((p1x - p2x) * (p1x - p2x) + (p1y - p2y) * (p1y - p2y)); | |
} | |
function Firework(sx, sy, tx, ty) { | |
this.x = sx; | |
this.y = sy; | |
this.sx = sx; | |
this.sy = sy; | |
this.tx = tx; | |
this.ty = ty; | |
this.distanceToTarget = calculateDistance(sx, sy, tx, ty); | |
this.distanceTraveled = 0; | |
this.coordinates = []; | |
this.coordinateCount = 2; | |
while (this.coordinateCount--) { | |
this.coordinates.push([this.x, this.y]); | |
} | |
this.angle = Math.atan2(ty - sy, tx - sx); | |
this.speed = 1; | |
this.acceleration = 1.2; | |
this.brightness = random(50, 70); | |
this.tragetRadius = 1; | |
} | |
Firework.prototype.update = function(index) { | |
if (this.targetRadius < 8) { | |
this.targetRadius += 0.3; | |
} else { | |
this.targetRadius = 1; | |
} | |
this.speed *= this.acceleration; | |
var vx = Math.cos(this.angle) * this.speed, | |
vy = Math.sin(this.angle) * this.speed; | |
this.distanceTraveled = calculateDistance(this.sx, this.sy, this.x + vx, this.y + vy); | |
if (this.distanceTraveled >= this.distanceToTarget) { | |
this.coordinates.pop(); | |
this.coordinates.unshift([this.tx, this.ty]); | |
createParticles(this.x, this.y); | |
snd.play(); | |
this.draw(); | |
fireworks.splice(index, 1); | |
} else { | |
this.x += vx; | |
this.y += vy; | |
} | |
this.coordinates.pop(); | |
this.coordinates.unshift([this.x, this.y]); | |
}; | |
Firework.prototype.draw = function() { | |
ctx.beginPath(); | |
ctx.moveTo(this.coordinates[this.coordinates.length - 1][0], this.coordinates[this.coordinates.length - 1][1]); | |
ctx.lineTo(this.x, this.y); | |
ctx.strokeStyle = 'hsl(' + hue + ', 100%, ' + this.brightness + '%)'; | |
ctx.stroke(); | |
}; | |
function Particle(x, y, type) { | |
this.x = x; | |
this.y = y; | |
this.type = type; | |
this.coordinates = []; | |
this.coordinateCount = 6; | |
while (this.coordinateCount--) { | |
this.coordinates.push([this.x, this.y]); | |
} | |
var variation = random(1, 5); | |
this.friction = 0.95; | |
this.alpha = 1; | |
this.angle = random(0, Math.PI * 2); | |
switch (type) { | |
case 1: | |
switch (true) { | |
case variation < 2 : | |
this.speed = random(1, 15); | |
this.gravity = 4; | |
this.hue = random(hue - 50, hue + 50); | |
this.brightness = random(50, 80); | |
this.decay = random(0.01, 0.02); | |
break; | |
case variation < 3 : | |
this.speed = random(1, 5); | |
this.gravity = 3; | |
this.hue = random(hue - 50, hue); | |
this.brightness = random(50, 80); | |
this.decay = random(0.015, 0.03); | |
break; | |
case variation < 4 : | |
this.speed = random(1, 8); | |
this.gravity = 3; | |
this.hue = random(hue, hue + 50); | |
this.brightness = random(50, 80); | |
this.decay = random(0.015, 0.03); | |
break; | |
default : | |
this.speed = random(1, 15); | |
this.gravity = 3; | |
this.hue = random(hue - 50, hue + 50); | |
this.brightness = random(10, 20); | |
this.decay = random(0.015, 0.3); | |
} | |
break; | |
case 2: | |
this.hue = 100; | |
switch (true) { | |
case variation < 2 : | |
this.speed = random(1, 10); | |
this.gravity = 4; | |
this.brightness = random(50, 80); | |
this.decay = random(0.01, 0.02); | |
break; | |
case variation < 3 : | |
this.speed = random(1, 21); | |
this.gravity = 3; | |
this.brightness = random(50, 80); | |
this.decay = random(0.015, 0.03); | |
break; | |
case variation < 4 : | |
this.speed = random(1, 3); | |
this.gravity = 3; | |
this.brightness = random(50, 80); | |
this.decay = random(0.015, 0.03); | |
break; | |
default : | |
this.speed = random(1, 5); | |
this.gravity = 3; | |
this.brightness = random(10, 20); | |
this.decay = random(0.015, 0.3); | |
} | |
break; | |
case 3: | |
this.brightness = random(10, 20); | |
switch (true) { | |
case variation < 2 : | |
this.speed = random(10, 15); | |
this.gravity = 4; | |
this.hue = 60; | |
this.decay = random(0.01, 0.02); | |
break; | |
case variation < 3 : | |
this.speed = random(11, 15); | |
this.gravity = 3; | |
this.hue = 10; | |
this.decay = random(0.015, 0.03); | |
break; | |
case variation < 4 : | |
this.speed = random(11, 18); | |
this.gravity = 3; | |
this.hue = 90; | |
this.decay = random(0.015, 0.03); | |
break; | |
default : | |
this.speed = random(11, 15); | |
this.gravity = 3; | |
this.hue = 120; | |
this.decay = random(0.015, 0.3); | |
} | |
break; | |
case 4: | |
switch (true) { | |
case variation < 2: | |
this.speed = random(1, 10); | |
this.gravity = 4; | |
this.hue = 300; | |
this.brightness = random(50, 80); | |
this.decay = random(0.01, 0.02); | |
break; | |
case variation < 3: | |
this.speed = random(1, 21); | |
this.gravity = 3; | |
this.hue = 300; | |
this.brightness = random(50, 80); | |
this.decay = random(0.015, 0.03); | |
break; | |
case variation < 4: | |
this.speed = random(1, 3); | |
this.gravity = 3; | |
this.hue = 300; | |
this.brightness = random(50, 80); | |
this.decay = random(0.015, 0.03); | |
break; | |
default: | |
this.speed = random(1, 5); | |
this.gravity = 3; | |
this.hue = 100; | |
this.brightness = random(10, 20); | |
this.decay = random(0.015, 0.3); | |
} | |
break; | |
default: | |
} | |
} | |
Particle.prototype.update = function(index) { | |
this.speed *= this.friction; | |
this.x += Math.cos(this.angle) * this.speed; | |
this.y += Math.sin(this.angle) * this.speed + this.gravity; | |
this.alpha -= this.decay; | |
if (this.type == 4 && this.alpha <= 0.5) { | |
this.brightness += 50; | |
this.hue += 200; | |
if (this.brightness >= 200) | |
this.brightness = 0; | |
} | |
if (this.alpha <= this.decay) { | |
particles.splice(index, 1); | |
} | |
this.coordinates.pop(); | |
this.coordinates.unshift([this.x, this.y]); | |
}; | |
Particle.prototype.draw = function() { | |
ctx.beginPath(); | |
ctx.moveTo(this.coordinates[this.coordinates.length - 1][0], this.coordinates[this.coordinates.length - 1][1]); | |
ctx.lineTo(this.x, this.y); | |
ctx.strokeStyle = 'hsla(' + this.hue + ', 100%, ' + this.brightness + '%, ' + this.alpha + ')'; | |
ctx.stroke(); | |
}; | |
function createParticles(x, y) { | |
var particleCount = 300; | |
var type = Math.floor(random(1, 5)); | |
while (particleCount--) { | |
particles.push(new Particle(x, y, type)); | |
} | |
} | |
function loop() { | |
hue += 0.5; | |
ctx.globalCompositeOperation = 'destination-out'; | |
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; | |
ctx.fillRect(0, 0, cw, ch); | |
ctx.globalCompositeOperation = 'lighter'; | |
var i = fireworks.length; | |
while (i--) { | |
fireworks[i].draw(); | |
fireworks[i].update(i); | |
} | |
var i = particles.length; | |
while (i--) { | |
particles[i].draw(); | |
particles[i].update(i); | |
} | |
if (timerTick >= timerTotal + randomTime) { | |
if (!mousedown) { | |
var xPos = Math.pow(Math.floor((random(-Math.pow(cw / 2, 1 / 3), Math.pow(cw / 2, 1 / 3)))), 3); | |
xPos += cw / 2; | |
fireworks.push(new Firework(cw / 2, ch, xPos, random(0, ch / 2))); | |
timerTick = 0; | |
randomTime = Math.pow(random(2, 4), 2); | |
} | |
} else { | |
timerTick++; | |
} | |
if (limiterTick >= limiterTotal) { | |
if (mousedown) { | |
fireworks.push(new Firework(cw / 2, ch, mx, my)); | |
limiterTick = 0; | |
} else { | |
limiterTick = limiterTotal; | |
} | |
} else { | |
limiterTick++; | |
} | |
} | |
canvas.addEventListener('mousemove', function(e) { | |
mx = e.pageX - canvas.offsetLeft; | |
my = e.pageY - canvas.offsetTop; | |
}); | |
canvas.addEventListener('mousedown', function(e) { | |
e.preventDefault(); | |
mousedown = true; | |
}); | |
canvas.addEventListener('mouseup', function(e) { | |
e.preventDefault(); | |
mousedown = false; | |
}); | |
setInterval(loop, 25); | |
function outroed() { | |
canvas.removeEventListener( 'transitionend', outroed ); | |
snd.pause(); | |
snd.src = ''; | |
const trash = document.createElement( 'div' ); | |
trash.appendChild( canvas ); | |
trash.innerHTML = ''; | |
} | |
function removeFireworks(){ | |
canvas.addEventListener( 'transitionend', outroed ); | |
canvas.classList.add( 'outro' ); | |
} | |
window.setTimeout( removeFireworks, 4000 ); | |
} | |
function init() { | |
window.setInterval(gamify, 500); | |
} | |
const whenDOMready = (func) => { | |
switch (document.readyState) { | |
case 'complete' : | |
case 'loaded' : | |
case 'interactive' : | |
func(); | |
break; | |
default : | |
window.addEventListener('DOMContentLoaded', function(e) { | |
func(); | |
}); | |
} | |
}; | |
whenDOMready(init); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is meant to work in Chrome.
Now when you drag and drop tickets in Jira you get sound effects!
I may add some visual FX later.