Skip to content

Instantly share code, notes, and snippets.

@johnowhitaker
Created August 24, 2024 23:08
Show Gist options
  • Save johnowhitaker/95c5cbf4cd4a40994dd74fe8a885cc18 to your computer and use it in GitHub Desktop.
Save johnowhitaker/95c5cbf4cd4a40994dd74fe8a885cc18 to your computer and use it in GitHub Desktop.
let context = null;
let canvas = null;
let video = null;
let lastSendTime = 0;
const throttleInterval = 1000; // 1 second in milliseconds
let throttleTimeout = null;
let pendingData = false;
htmx.onLoad(elt => {
// Find and process the canvas element
const elements = htmx.findAll(elt, '#drawingCanvas');
if (elt.matches('#drawingCanvas')) elements.unshift(elt);
elements.forEach(c => {
canvas = c;
context = canvas.getContext('2d');
// Create video element for webcam feed
video = document.createElement('video');
video.setAttribute('playsinline', '');
video.setAttribute('autoplay', '');
video.setAttribute('muted', '');
// Start webcam feed
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
video.srcObject = stream;
video.play();
})
.catch(err => {
console.error("Error accessing the webcam", err);
});
}
// Update canvas with video feed
function updateCanvas() {
if (video.readyState === video.HAVE_ENOUGH_DATA) {
context.save(); // Save the current context state
context.translate(canvas.width, canvas.height); // Move to far corner
context.rotate(Math.PI); // Rotate 180 degrees (π radians)
context.drawImage(video, 0, 0, canvas.width, canvas.height);
context.restore(); // Restore the context state
}
requestAnimationFrame(updateCanvas);
}
video.addEventListener('loadedmetadata', updateCanvas);
// Start sending frames
setInterval(throttledSendCanvasData, throttleInterval);
});
// If elt is a guess, log it and update latest-guess
if (elt.matches('.guess')) {
console.log("Got guess:", elt);
const latestGuess = document.getElementById('latest-guess');
let correct = elt.innerText.includes(" (correct!)");
let guess = elt.innerText.split(": ")[1].split(" ")[0];
let guesser = elt.innerText.split(": ")[0];
let text = elt.innerText.split(" (")[0];
if (latestGuess) {
latestGuess.innerText = text; //correct ? text + " (correct!)" : text;
latestGuess.classList.add('guess-animation');
if (correct) {
latestGuess.classList.add('correct-guess');
// End game means 3 seconds before the game ends and moves to new screen
// TODO pause and animate fireworks
let tl = document.getElementById("time-left")
if (tl){
console.log("Stopping countdown");
document.getElementById("active-header").innerText = "Game Over!";
document.getElementById("active-subheader").innerText = guesser + " correctly guessed the word "+ guess + "!";
window.confetti(ticks=100);
tl.remove();// Stops the countdown
}
} else {
latestGuess.classList.remove('correct-guess');
}
// Remove animation class after it's done
setTimeout(() => {
latestGuess.classList.remove('guess-animation');
}, 500);
}
}
});
function throttledSendCanvasData() {
const now = Date.now();
if (now - lastSendTime >= throttleInterval) {
sendCanvasData();
lastSendTime = now;
if (throttleTimeout) {
clearTimeout(throttleTimeout);
throttleTimeout = null;
}
if (pendingData) {
throttleTimeout = setTimeout(() => {
sendCanvasData();
lastSendTime = Date.now();
pendingData = false;
}, throttleInterval);
}
} else {
pendingData = true;
if (!throttleTimeout) {
throttleTimeout = setTimeout(() => {
sendCanvasData();
lastSendTime = Date.now();
pendingData = false;
throttleTimeout = null;
}, throttleInterval - (now - lastSendTime));
}
}
}
function sendCanvasData() {
canvas.toBlob((blob) => {
const formData = new FormData();
formData.append('image', blob, 'webcam.png');
console.log("Sending webcam frame");
fetch('/process-canvas', {
method: 'POST',
body: formData,
}).then(response => response.json())
.then(data => {
console.log("Received data");
if (data['active_game'] == "no") {
// if the canvas exists, update the active area to remove it
setTimeout(() => {
if (document.getElementById('drawingCanvas')) {
htmx.ajax('GET', '/active_area', {target:'#active-area', swap:'outerHTML'});
}
}, 4000);
}
console.log(data);
})
.catch(error => console.error('Error:', error));
});
}
// Countdonw timer
setInterval(() => {
let timeLeftElement = document.getElementById("time-left");
let progressElement = document.getElementById("progress");
let containerElement = document.getElementById("countdown-container");
// do nothing if it doesn't exist
if (!timeLeftElement || !progressElement) return;
let start = parseFloat(document.getElementById("start-time").value);
let elapsed = (Date.now() / 1000) - start;
let max_duration = parseFloat(document.getElementById("game-max-duration").value);
let time = Math.max(0, max_duration - elapsed);
let percentage = (time / max_duration) * 100;
// console.log(start, elapsed, max_duration, time, percentage);
timeLeftElement.innerText = `Time left: ${time} seconds`;
progressElement.style.width = `${percentage}%`;
// Add urgency effects
if (percentage <= 25) {
progressElement.classList.add('urgent');
containerElement.classList.add('urgency');
} else {
progressElement.classList.remove('urgent');
containerElement.classList.remove('urgency');
}
if (time <= 0) {
// Check the game hasn't already been ended
let timeLeftElement = document.getElementById("time-left");
if (timeLeftElement){
console.log("Time's up!");
// remove the time-left element
timeLeftElement.remove();
// trigger HTMX to end the game and replace the active area
htmx.ajax('GET', '/endgame', {target:'#active-area', swap:'outerHTML'});
}
}
}, 100); // every 100ms
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment