Created
May 11, 2024 14:22
-
-
Save LifeJustDLC/35d14c67ecc4f84b4c14f1cee5c77f4d to your computer and use it in GitHub Desktop.
Global Speed for Android interaction design PoC
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
"use strict"; | |
; | |
(function main() { | |
let truePlayRate = 1.0; | |
let trueCurrentTime = NaN; | |
const speedList = [0.8, 1.0, 1.25, 1.5, 2.0]; | |
const isFrefox = navigator.userAgent.indexOf("Firefox") === -1 ? false : true; | |
const isEdge = navigator.userAgent.indexOf("EdgA") === -1 ? false : true; // yeah it's "EdgA" | |
let player; | |
const playerSize = { width: NaN, height: NaN }; | |
let display; | |
function setPlayerOnceFullscreen(_player) { | |
player = _player; | |
// default `passive` for touch events possibly be `true`. | |
player.addEventListener("touchstart", handleContactStart, { | |
passive: false, | |
capture: true, | |
}); | |
// safari doesn't support touch? also for website compatibility | |
player.addEventListener("pointerdown", handleContactStart, { | |
capture: true, | |
}); | |
player.addEventListener("touchmove", handleContactMove, { | |
passive: false, | |
capture: true, | |
}); | |
player.addEventListener("pointermove", handleContactMove, { capture: true }); | |
player.addEventListener("touchend", drawDebugAndResetAccumulator, { | |
capture: true, | |
}); | |
player.addEventListener("touchcancel", drawDebugAndResetAccumulator, { | |
capture: true, | |
}); | |
player.addEventListener("pointerup", drawDebugAndResetAccumulator, { | |
capture: true, | |
}); | |
player.addEventListener("pointercancel", drawDebugAndResetAccumulator, { | |
capture: true, | |
}); | |
} | |
const debugPoints = []; | |
function drawDebugAndResetAccumulator(ev) { | |
// TODO: refactor | |
if (!isStartFromOverrideArea) | |
return; | |
ev.stopImmediatePropagation(); | |
accumulator = 0; | |
console.log("drawing"); | |
if (document.getElementById("svg")) { | |
player.removeChild(document.getElementById("svg")); | |
} | |
const svg = document.createElement("div"); | |
svg.id = "svg"; | |
Object.assign(svg.style, { | |
zIndex: "1000", // TODO: refactor. this is maintainability hell | |
width: playerSize.width + "px", | |
height: playerSize.height + "px", | |
background: "transparent", | |
}); | |
player.appendChild(svg); | |
for (const point of debugPoints) { | |
const circle = document.createElement("div"); | |
circle.style.top = point.y + "px"; | |
circle.style.left = point.x + "px"; | |
if (point.type === "normal") { | |
Object.assign(circle.style, { | |
position: "fixed", | |
width: "1px", | |
height: "1px", | |
background: "white", | |
}); | |
} | |
else if (point.type === "vibrate") { | |
Object.assign(circle.style, { | |
position: "fixed", | |
width: "5px", | |
height: "5px", | |
background: "yellow", | |
}); | |
} | |
svg.appendChild(circle); | |
} | |
debugPoints.length = 0; | |
} | |
let video; | |
let isOverridingEnabled = false; | |
document.addEventListener("fullscreenchange", (ev) => { | |
// TODO: how to find it without waiting for fullscreen | |
if (document.fullscreenElement) { | |
isOverridingEnabled = true; | |
if (!player) { | |
setPlayerOnceFullscreen(document.fullscreenElement); | |
} | |
} | |
else { | |
isOverridingEnabled = false; | |
} | |
display = initDisplay(); | |
if (!video) { | |
video = document.getElementsByTagName("video")[0]; | |
} | |
video.onratechange = () => { | |
if (Math.abs(video.playbackRate - truePlayRate) < 0.001) | |
return; | |
video.playbackRate = truePlayRate; | |
}; | |
const { width, height } = player.getBoundingClientRect(); | |
Object.assign(playerSize, { width, height }); | |
}); | |
let isAbleToBeep = true; | |
// setInterval(() => isAbleToBeep = true, 500) | |
let isStartFromOverrideArea = false; | |
const initPos = { x: NaN, y: NaN, id: NaN }; | |
function handleContactStart(ev) { | |
if (!isOverridingEnabled) | |
return; | |
// TODO: multi-touch will be buggy | |
const evPos = { x: NaN, y: NaN, id: NaN }; | |
if (ev instanceof TouchEvent) { | |
const touch = ev.touches[0]; | |
evPos.x = touch.clientX; | |
evPos.y = touch.clientY; | |
evPos.id = touch.identifier; | |
} | |
else if (ev instanceof PointerEvent) { | |
evPos.x = ev.clientX; | |
evPos.y = ev.clientY; | |
evPos.id = ev.pointerId; | |
} | |
if (!isInOverrideArea(evPos)) { | |
isStartFromOverrideArea = false; | |
return; | |
} | |
isStartFromOverrideArea = true; | |
ev.stopImmediatePropagation(); | |
Object.assign(initPos, evPos); | |
console.log({ initPos }); | |
} | |
function isInOverrideArea({ x, y }) { | |
const { width, height } = player.getBoundingClientRect(); | |
Object.assign(playerSize, { width, height }); | |
if (x < playerSize.width * 0.3 || x > playerSize.width * 0.6) { | |
return true; | |
} | |
else | |
return false; | |
} | |
let accumulator = 0; // TODO: refactor | |
function handleContactMove(ev) { | |
if (!isOverridingEnabled) | |
return; | |
if (!isStartFromOverrideArea) | |
return; | |
// TODO: multi-touch will be buggy | |
const evPos = { x: NaN, y: NaN, id: NaN }; | |
if (ev instanceof TouchEvent) { | |
const touch = ev.touches[0]; | |
evPos.id = touch.identifier; | |
evPos.x = touch.clientX; | |
evPos.y = touch.clientY; | |
} | |
else if (ev instanceof PointerEvent) { | |
evPos.id = ev.pointerId; | |
evPos.x = ev.clientX; | |
evPos.y = ev.clientY; | |
} | |
if (evPos.id !== initPos.id) | |
return; | |
ev.stopImmediatePropagation(); | |
debugPoints.push({ x: evPos.x, y: evPos.y, type: "normal" }); | |
const vectX = evPos.x - initPos.x; | |
const vectY = evPos.y - initPos.y; | |
const interval = (playerSize.height + playerSize.width) * 0.07; | |
if (Math.abs(vectX) < interval && Math.abs(vectY) < interval) | |
return; | |
const tan = -vectY / vectX; | |
if (vectY < 0 && (tan > Math.tan(rad(60)) || tan < Math.tan(rad(120)))) { | |
// dragging up | |
const nextSpeed = Math.min(...speedList.filter((speed) => speed - truePlayRate > 0.001)); | |
video.playbackRate = truePlayRate = nextSpeed; | |
display("↓ " + truePlayRate); | |
} | |
if (vectY > 0 && (tan > Math.tan(rad(-120)) || tan < Math.tan(rad(-60)))) { | |
// dragging down | |
const nextSpeed = Math.max(...speedList.filter((speed) => speed - truePlayRate < -0.001)); | |
video.playbackRate = truePlayRate = nextSpeed; | |
display("↑ " + truePlayRate); | |
} | |
if (vectX < 0 && tan > Math.tan(rad(150)) && tan < Math.tan(rad(210))) { | |
// dragging left | |
trueCurrentTime = video.currentTime - 1; | |
video.currentTime = trueCurrentTime; | |
display("← " + trueCurrentTime.toFixed(1)); | |
} | |
if (vectX > 0 && tan > Math.tan(rad(-30)) && tan < Math.tan(rad(30))) { | |
// dragging right | |
trueCurrentTime = video.currentTime + 1; | |
video.currentTime = trueCurrentTime; | |
display("→ " + trueCurrentTime.toFixed(1)); | |
} | |
if (vectX < 0 && | |
vectY < 0 && | |
tan > Math.tan(rad(120)) && | |
tan < Math.tan(rad(150))) { | |
// dragging up left | |
console.log("up left"); | |
if (accumulator < 1) { | |
accumulator += 1; | |
display("drag further to confirm: disable"); | |
} | |
else { | |
isOverridingEnabled = false; | |
display("disabled"); | |
accumulator = 0; | |
} | |
} | |
initPos.x = evPos.x; | |
initPos.y = evPos.y; | |
debugPoints.push({ x: evPos.x, y: evPos.y, type: "vibrate" }); | |
if (isEdge) { | |
navigator.vibrate(20); | |
} | |
if (isFrefox && isAbleToBeep) { | |
beep(); | |
// isAbleToBeep = false | |
} | |
} | |
function initDisplay() { | |
const overlay = document.createElement("div"); | |
Object.assign(overlay.style, { | |
position: "fixed", | |
padding: "1rem", | |
display: "none", | |
backgroundColor: "rgba(0, 0, 0, 0.5)", | |
color: "white", | |
}); | |
player.appendChild(overlay); | |
let lastUpdate = 0; | |
const showingTime = 1000; | |
setInterval(() => { | |
if (lastUpdate + showingTime < Date.now()) { | |
overlay.style.display = "none"; | |
} | |
}, 100); // TODO: refactor, support toggle on-off | |
return function display(msg) { | |
overlay.textContent = msg; | |
overlay.style.display = "block"; | |
lastUpdate = Date.now(); | |
}; | |
} | |
const audio = new Audio("data:audio/wav;base64,UklGRkIFAABXQVZFZm10IBIAAAADAAIARKwAACBiBQAIACAAAABkYXRhEAUAAB17GLsdexi7qpCGOaqQhjnsur067Lq9OquYhbqrmIW6EuMZuhLjGbqXQzQ7l0M0O/AiATrwIgE6Qqaeu0KmnrufOJS7nziUu66xcbuusXG7dFW7u3RVu7vcNZy73DWcu1tdvrlbXb65kFP0OpBT9DpmelQ7ZnpUO4QXpjuEF6Y7iZ8sO4mfLDsvm6Q6L5ukOkypqTtMqak7yO22O8jttjs5PdW5OT3VueENzTnhDc05OKAzPTigMz1atnw9WrZ8PWxM67xsTOu8d3yxvXd8sb165GS7euRkuyPU2Twj1Nk8Yc4fvWHOH73evwW83r8FvESrBD1EqwQ9m//ou5v/6Ls9XZq8PV2avKAU+DugFPg7QiC7ukIgu7qUG7i8lBu4vM86oLzPOqC84/4fvOP+H7zhUAq84VAKvPLKwzzyysM8l8IoPZfCKD1tw6+6bcOvuuZJpbzmSaW8ryZZPK8mWTxRRQI9UUUCPUH/7zxB/+88zS7GPM0uxjzxO4678TuOuwOTE70DkxO9NoIQvTaCEL0P7OS7D+zku9+NjrvfjY67W3arvFt2q7wFd0s6BXdLOmxc9TxsXPU8/0f6PP9H+jwa4ag7GuGoO/Ppv7zz6b+8SbR4vEm0eLzLNHM8yzRzPBcagjsXGoI7owehvKMHobyFo6W8haOlvMqry7vKq8u7BYwaPAWMGjxuSFE8bkhRPIhegTyIXoE8oSy2PKEstjxIYQY8SGEGPLiPXry4j168HK4/vByuP7wVoVC7FaFQu8oefLvKHny7/AFtu/wBbbtiYXK7YmFyuy/hOjsv4To78XSOO/F0jjvOWz27zls9u5WAJDyVgCQ8/jq7PP46uzyyWjq7slo6u6e4vrynuL687CHeu+wh3rs49Zs7OPWbO5AXrLuQF6y7AbvbuwG727vATxa7wE8Wu3XYLbx12C28We9BvFnvQbx7lk87e5ZPO6RbXjykW148PEN9PDxDfTwcm448HJuOPJtJEDybSRA8C0XYuwtF2Ls2ObC7NjmwuyShljskoZY7UiZTO1ImUzu10Jy7tdCcuyGXfrshl367upeLubqXi7lt4JK5beCSuRQKhLsUCoS7bPFwu2zxcLv+j6I6/o+iOnVUuzp1VLs65QiDuuUIg7oEEJe7BBCXu9/fxLvf38S7zEeAu8xHgLtjxVM5Y8VTOX5Tazt+U2s7DtFVug7RVbofAyy7HwMsu7p6ZTu6emU7LXWrOy11qzs/clE7P3JRO3NssjtzbLI7Hbw+Ox28PjvIl227yJdtu2V6brtlem67LUmsui1JrLq+Eey6vhHsuo/dMLqP3TC6w60CO8OtAjs0I0w7NCNMOxVSvjsVUr47j8gFPI/IBTz5WbY6+Vm2OhQHBrwUBwa8yV41vMleNbzjMfq74zH6uzY0B7s2NAe7zQbKOs0GyjotOMk6LTjJOrIovbmyKL25mCD8Opgg/Do7p6Y7O6emO9SFXDvUhVw7UAqUulAKlLr8a+O6/GvjuhucszkbnLM5MzAzujMwM7qrKoS6qyqEujqPp7o6j6e6b2Xnum9l57qxFuC4sRbguMW/lzrFv5c6gVGdOoFRnTocvzI7HL8yOxqlfzsapX87VIYuOlSGLjq+30y7vt9Mu9dLN7vXSze79NVfuvTVX7rQXlU50F5VOV768zle+vM57cHMOu3BzDqKZAw7imQMO7CmyDqwpsg6Zn9NuGZ/Tbh6mlK6eppSumZhY3QEAAAAogAAAA=="); | |
function beep() { | |
audio.play(); // FIXME: delay/latency, conflict with the sound playing. maybe an audio lib dependency? | |
} | |
function rad(deg) { | |
return (deg / 360) * 2 * Math.PI; | |
} | |
})(); |
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
;(function main() { | |
let truePlayRate = 1.0 | |
let trueCurrentTime = NaN | |
const speedList = [0.8, 1.0, 1.25, 1.5, 2.0] | |
const isFrefox = navigator.userAgent.indexOf("Firefox") === -1 ? false : true | |
const isEdge = navigator.userAgent.indexOf("EdgA") === -1 ? false : true // yeah it's "EdgA" | |
type Player = HTMLDivElement | HTMLVideoElement | |
let player: Player | |
const playerSize = { width: NaN, height: NaN } | |
let display: Function | |
function setPlayerOnceFullscreen(_player: Player) { | |
player = _player | |
// default `passive` for touch events possibly be `true`. | |
player.addEventListener("touchstart", handleContactStart, { | |
passive: false, | |
capture: true, | |
}) | |
// safari doesn't support touch? also for website compatibility | |
player.addEventListener("pointerdown", handleContactStart, { | |
capture: true, | |
}) | |
player.addEventListener("touchmove", handleContactMove, { | |
passive: false, | |
capture: true, | |
}) | |
player.addEventListener("pointermove", handleContactMove, { capture: true }) | |
player.addEventListener("touchend", drawDebugAndResetAccumulator, { | |
capture: true, | |
}) | |
player.addEventListener("touchcancel", drawDebugAndResetAccumulator, { | |
capture: true, | |
}) | |
player.addEventListener("pointerup", drawDebugAndResetAccumulator, { | |
capture: true, | |
}) | |
player.addEventListener("pointercancel", drawDebugAndResetAccumulator, { | |
capture: true, | |
}) | |
} | |
const debugPoints: { x: number; y: number; type: "normal" | "vibrate" }[] = [] | |
function drawDebugAndResetAccumulator(ev: Event) { | |
// TODO: refactor | |
if (!isStartFromOverrideArea) return | |
ev.stopImmediatePropagation() | |
accumulator = 0 | |
console.log("drawing") | |
if (document.getElementById("svg")) { | |
player.removeChild(document.getElementById("svg")!) | |
} | |
const svg = document.createElement("div") | |
svg.id = "svg" | |
Object.assign(svg.style, { | |
zIndex: "1000", // TODO: refactor. this is maintainability hell | |
width: playerSize.width + "px", | |
height: playerSize.height + "px", | |
background: "transparent", | |
} as CSSStyleDeclaration) | |
player.appendChild(svg) | |
for (const point of debugPoints) { | |
const circle = document.createElement("div") | |
circle.style.top = point.y + "px" | |
circle.style.left = point.x + "px" | |
if (point.type === "normal") { | |
Object.assign(circle.style, { | |
position: "fixed", | |
width: "1px", | |
height: "1px", | |
background: "white", | |
} as CSSStyleDeclaration) | |
} else if (point.type === "vibrate") { | |
Object.assign(circle.style, { | |
position: "fixed", | |
width: "5px", | |
height: "5px", | |
background: "yellow", | |
} as CSSStyleDeclaration) | |
} | |
svg.appendChild(circle) | |
} | |
debugPoints.length = 0 | |
} | |
let video: HTMLVideoElement | |
let isOverridingEnabled = false | |
document.addEventListener("fullscreenchange", (ev) => { | |
// TODO: how to find it without waiting for fullscreen | |
if (document.fullscreenElement) { | |
isOverridingEnabled = true | |
if (!player) { | |
setPlayerOnceFullscreen(document.fullscreenElement as Player) | |
} | |
} else { | |
isOverridingEnabled = false | |
} | |
display = initDisplay() | |
if (!video) { | |
video = document.getElementsByTagName("video")[0] | |
} | |
video.onratechange = () => { | |
if (Math.abs(video.playbackRate - truePlayRate) < 0.001) return | |
video.playbackRate = truePlayRate | |
} | |
const { width, height } = player.getBoundingClientRect() | |
Object.assign(playerSize, { width, height }) | |
}) | |
let isAbleToBeep = true | |
// setInterval(() => isAbleToBeep = true, 500) | |
let isStartFromOverrideArea = false | |
const initPos = { x: NaN, y: NaN, id: NaN } | |
function handleContactStart(ev: Event) { | |
if (!isOverridingEnabled) return | |
// TODO: multi-touch will be buggy | |
const evPos = { x: NaN, y: NaN, id: NaN } | |
if (ev instanceof TouchEvent) { | |
const touch = ev.touches[0] | |
evPos.x = touch.clientX | |
evPos.y = touch.clientY | |
evPos.id = touch.identifier | |
} else if (ev instanceof PointerEvent) { | |
evPos.x = ev.clientX | |
evPos.y = ev.clientY | |
evPos.id = ev.pointerId | |
} | |
if (!isInOverrideArea(evPos)) { | |
isStartFromOverrideArea = false | |
return | |
} | |
isStartFromOverrideArea = true | |
ev.stopImmediatePropagation() | |
Object.assign(initPos, evPos) | |
console.log({ initPos }) | |
} | |
function isInOverrideArea({ x, y }: { x: number; y: number }) { | |
const { width, height } = player.getBoundingClientRect() | |
Object.assign(playerSize, { width, height }) | |
if (x < playerSize.width * 0.3 || x > playerSize.width * 0.6) { | |
return true | |
} else return false | |
} | |
let accumulator = 0 // TODO: refactor | |
function handleContactMove(ev: Event) { | |
if (!isOverridingEnabled) return | |
if (!isStartFromOverrideArea) return | |
// TODO: multi-touch will be buggy | |
const evPos = { x: NaN, y: NaN, id: NaN } | |
if (ev instanceof TouchEvent) { | |
const touch = ev.touches[0] | |
evPos.id = touch.identifier | |
evPos.x = touch.clientX | |
evPos.y = touch.clientY | |
} else if (ev instanceof PointerEvent) { | |
evPos.id = ev.pointerId | |
evPos.x = ev.clientX | |
evPos.y = ev.clientY | |
} | |
if (evPos.id !== initPos.id) return | |
ev.stopImmediatePropagation() | |
debugPoints.push({ x: evPos.x, y: evPos.y, type: "normal" }) | |
const vectX = evPos.x - initPos.x | |
const vectY = evPos.y - initPos.y | |
const interval = (playerSize.height + playerSize.width) * 0.07 | |
if (Math.abs(vectX) < interval && Math.abs(vectY) < interval) return | |
const tan = -vectY / vectX | |
if (vectY < 0 && (tan > Math.tan(rad(60)) || tan < Math.tan(rad(120)))) { | |
// dragging up | |
const nextSpeed = Math.min( | |
...speedList.filter((speed) => speed - truePlayRate > 0.001) | |
) | |
video.playbackRate = truePlayRate = nextSpeed | |
display("↓ " + truePlayRate) | |
} | |
if (vectY > 0 && (tan > Math.tan(rad(-120)) || tan < Math.tan(rad(-60)))) { | |
// dragging down | |
const nextSpeed = Math.max( | |
...speedList.filter((speed) => speed - truePlayRate < -0.001) | |
) | |
video.playbackRate = truePlayRate = nextSpeed | |
display("↑ " + truePlayRate) | |
} | |
if (vectX < 0 && tan > Math.tan(rad(150)) && tan < Math.tan(rad(210))) { | |
// dragging left | |
trueCurrentTime = video.currentTime - 1 | |
video.currentTime = trueCurrentTime | |
display("← " + trueCurrentTime.toFixed(1)) | |
} | |
if (vectX > 0 && tan > Math.tan(rad(-30)) && tan < Math.tan(rad(30))) { | |
// dragging right | |
trueCurrentTime = video.currentTime + 1 | |
video.currentTime = trueCurrentTime | |
display("→ " + trueCurrentTime.toFixed(1)) | |
} | |
if ( | |
vectX < 0 && | |
vectY < 0 && | |
tan > Math.tan(rad(120)) && | |
tan < Math.tan(rad(150)) | |
) { | |
// dragging up left | |
console.log("up left") | |
if (accumulator < 1) { | |
accumulator += 1 | |
display("drag further to confirm: disable") | |
} else { | |
isOverridingEnabled = false | |
display("disabled") | |
accumulator = 0 | |
} | |
} | |
initPos.x = evPos.x | |
initPos.y = evPos.y | |
debugPoints.push({ x: evPos.x, y: evPos.y, type: "vibrate" }) | |
if (isEdge) { | |
navigator.vibrate(20) | |
} | |
if (isFrefox && isAbleToBeep) { | |
beep() | |
// isAbleToBeep = false | |
} | |
} | |
function initDisplay() { | |
const overlay = document.createElement("div") | |
Object.assign(overlay.style, { | |
position: "fixed", | |
padding: "1rem", | |
display: "none", | |
backgroundColor: "rgba(0, 0, 0, 0.5)", | |
color: "white", | |
} as CSSStyleDeclaration) | |
player.appendChild(overlay) | |
let lastUpdate = 0 | |
const showingTime = 1000 | |
setInterval(() => { | |
if (lastUpdate + showingTime < Date.now()) { | |
overlay.style.display = "none" | |
} | |
}, 100) // TODO: refactor, support toggle on-off | |
return function display(msg: string) { | |
overlay.textContent = msg | |
overlay.style.display = "block" | |
lastUpdate = Date.now() | |
} | |
} | |
const audio = new Audio( | |
"data:audio/wav;base64,UklGRkIFAABXQVZFZm10IBIAAAADAAIARKwAACBiBQAIACAAAABkYXRhEAUAAB17GLsdexi7qpCGOaqQhjnsur067Lq9OquYhbqrmIW6EuMZuhLjGbqXQzQ7l0M0O/AiATrwIgE6Qqaeu0KmnrufOJS7nziUu66xcbuusXG7dFW7u3RVu7vcNZy73DWcu1tdvrlbXb65kFP0OpBT9DpmelQ7ZnpUO4QXpjuEF6Y7iZ8sO4mfLDsvm6Q6L5ukOkypqTtMqak7yO22O8jttjs5PdW5OT3VueENzTnhDc05OKAzPTigMz1atnw9WrZ8PWxM67xsTOu8d3yxvXd8sb165GS7euRkuyPU2Twj1Nk8Yc4fvWHOH73evwW83r8FvESrBD1EqwQ9m//ou5v/6Ls9XZq8PV2avKAU+DugFPg7QiC7ukIgu7qUG7i8lBu4vM86oLzPOqC84/4fvOP+H7zhUAq84VAKvPLKwzzyysM8l8IoPZfCKD1tw6+6bcOvuuZJpbzmSaW8ryZZPK8mWTxRRQI9UUUCPUH/7zxB/+88zS7GPM0uxjzxO4678TuOuwOTE70DkxO9NoIQvTaCEL0P7OS7D+zku9+NjrvfjY67W3arvFt2q7wFd0s6BXdLOmxc9TxsXPU8/0f6PP9H+jwa4ag7GuGoO/Ppv7zz6b+8SbR4vEm0eLzLNHM8yzRzPBcagjsXGoI7owehvKMHobyFo6W8haOlvMqry7vKq8u7BYwaPAWMGjxuSFE8bkhRPIhegTyIXoE8oSy2PKEstjxIYQY8SGEGPLiPXry4j168HK4/vByuP7wVoVC7FaFQu8oefLvKHny7/AFtu/wBbbtiYXK7YmFyuy/hOjsv4To78XSOO/F0jjvOWz27zls9u5WAJDyVgCQ8/jq7PP46uzyyWjq7slo6u6e4vrynuL687CHeu+wh3rs49Zs7OPWbO5AXrLuQF6y7AbvbuwG727vATxa7wE8Wu3XYLbx12C28We9BvFnvQbx7lk87e5ZPO6RbXjykW148PEN9PDxDfTwcm448HJuOPJtJEDybSRA8C0XYuwtF2Ls2ObC7NjmwuyShljskoZY7UiZTO1ImUzu10Jy7tdCcuyGXfrshl367upeLubqXi7lt4JK5beCSuRQKhLsUCoS7bPFwu2zxcLv+j6I6/o+iOnVUuzp1VLs65QiDuuUIg7oEEJe7BBCXu9/fxLvf38S7zEeAu8xHgLtjxVM5Y8VTOX5Tazt+U2s7DtFVug7RVbofAyy7HwMsu7p6ZTu6emU7LXWrOy11qzs/clE7P3JRO3NssjtzbLI7Hbw+Ox28PjvIl227yJdtu2V6brtlem67LUmsui1JrLq+Eey6vhHsuo/dMLqP3TC6w60CO8OtAjs0I0w7NCNMOxVSvjsVUr47j8gFPI/IBTz5WbY6+Vm2OhQHBrwUBwa8yV41vMleNbzjMfq74zH6uzY0B7s2NAe7zQbKOs0GyjotOMk6LTjJOrIovbmyKL25mCD8Opgg/Do7p6Y7O6emO9SFXDvUhVw7UAqUulAKlLr8a+O6/GvjuhucszkbnLM5MzAzujMwM7qrKoS6qyqEujqPp7o6j6e6b2Xnum9l57qxFuC4sRbguMW/lzrFv5c6gVGdOoFRnTocvzI7HL8yOxqlfzsapX87VIYuOlSGLjq+30y7vt9Mu9dLN7vXSze79NVfuvTVX7rQXlU50F5VOV768zle+vM57cHMOu3BzDqKZAw7imQMO7CmyDqwpsg6Zn9NuGZ/Tbh6mlK6eppSumZhY3QEAAAAogAAAA==" | |
) | |
function beep() { | |
audio.play() // FIXME: delay/latency, conflict with the sound playing. maybe an audio lib dependency? | |
} | |
function rad(deg: number) { | |
return (deg / 360) * 2 * Math.PI | |
} | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment