/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); var facekit = LK.import("@upit/facekit.v1"); /**** * Classes ****/ var ButtonPunchline = Container.expand(function () { var self = Container.call(this); // Attach the ButtonPunchline asset var buttonGraphics = self.attachAsset('ButtonPunchline', { anchorX: 0.5, anchorY: 0.5 }); // Add down event handler for punchline button self.down = function (x, y, obj) { // Play sound if enabled if (soundEnabled) { // Play the currently selected punchline sound if (currentPunchlineSound >= 0 && currentPunchlineSound < punchlineSounds.length && punchlineSounds[currentPunchlineSound].asset) { LK.getSound(punchlineSounds[currentPunchlineSound].asset).play(); } } // Create confetti effect if enabled playConfettiEffect(self.x, self.y - 100); }; return self; }); var ButtonSettings = Container.expand(function () { var self = Container.call(this); // Attach the ButtonSettings asset var buttonGraphics = self.attachAsset('ButtonSettingsBackground', { anchorX: 0.5, anchorY: 0.5, tint: 0x000000, alpha: 0.5 }); var buttonGraphics = self.attachAsset('ButtonSettings', { anchorX: 0.5, anchorY: 0.5 }); // Add down event handler to show settings popup self.down = function (x, y, obj) { // Toggle settings popup visibility if (settingsPopup) { settingsPopup.visible = !settingsPopup.visible; bypassTracking = settingsPopup.visible; // Toggle bypassTracking based on visibility trollFace.visible = !settingsPopup.visible; // Hide trollface when settings popup is shown // Call onShow when the popup becomes visible if (settingsPopup.visible && settingsPopup.onShow) { settingsPopup.onShow(); } } // Play beep sound when settings button is toggled LK.getSound('beep').play(); // Stop event propagation to prevent changing the face style if (obj.event && typeof obj.event.stopPropagation === 'function') { obj.event.stopPropagation(); } }; // Define any additional properties or methods for the ButtonSettings here return self; }); var Confetti = Container.expand(function () { var self = Container.call(this); // Properties self.particles = []; self.particleCount = 50; self.colors = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF]; self.gravity = 0.5; self.active = false; // Create confetti particles self.createParticles = function (x, y) { // Clear existing particles for (var i = 0; i < self.particles.length; i++) { if (self.particles[i].parent) { self.particles[i].parent.removeChild(self.particles[i]); } } self.particles = []; // Create new particles for (var i = 0; i < self.particleCount; i++) { var particle = LK.getAsset('debugFacePoints', { anchorX: 0.5, anchorY: 0.5 }); particle.tint = self.colors[Math.floor(Math.random() * self.colors.length)]; particle.scale.set(0.2, 0.2); // Set initial position particle.x = x; particle.y = y; // Set random velocity particle.vx = (Math.random() - 0.5) * 20; particle.vy = (Math.random() - 0.5) * 20 - 10; // Add to container self.addChild(particle); self.particles.push(particle); } self.active = true; }; // Update particles self.update = function () { if (!self.active) { return; } var allSettled = true; for (var i = 0; i < self.particles.length; i++) { var particle = self.particles[i]; // Apply gravity particle.vy += self.gravity; // Update position particle.x += particle.vx; particle.y += particle.vy; // Check if particle is still moving if (particle.vy < 10) { allSettled = false; } // Check if particle is off screen if (particle.y > 2732) { particle.vy = 0; particle.vx = 0; } } // If all particles have settled, stop updating if (allSettled) { self.active = false; // Remove particles for (var i = 0; i < self.particles.length; i++) { if (self.particles[i].parent) { self.particles[i].parent.removeChild(self.particles[i]); } } self.particles = []; } }; return self; }); var ConfettisHaha = Container.expand(function () { var self = Container.call(this); // Properties self.particles = []; self.particleCount = 10; self.active = false; self.screenWidth = 2048; self.screenHeight = 2732; // Configuration properties self.initialScale = 0.1; // Initial scale when particles appear self.minScale = 0.5; // Minimum final scale of particles self.scaleVariation = 0.5; // Random variation added to scale self.minDuration = 2; // Minimum animation duration in seconds self.durationVariation = 2; // Random variation added to duration self.maxDelaySeconds = 0.5; // Maximum random delay before animation starts self.fadeInThreshold = 0.2; // Progress threshold for fade in (0-1) self.fadeOutThreshold = 0.8; // Progress threshold for fade out (0-1) self.scaleUpThreshold = 0.25; // Progress threshold for scale up (0-1) self.settleThreshold = 0.35; // Progress threshold for settling to final scale (0-1) self.overshootFactor = 1.15; // How much to overshoot during scale up self.oscillationFrequency = 12; // Frequency of oscillation self.oscillationAmplitude = 0.05; // Amplitude of oscillation self.rotationRange = 30; // Range of random rotation in degrees (±30°) // Create confetti particles self.createParticles = function (x, y) { // Clear existing particles for (var i = 0; i < self.particles.length; i++) { if (self.particles[i].parent) { self.particles[i].parent.removeChild(self.particles[i]); } } self.particles = []; // Create new particles for (var i = 0; i < self.particleCount; i++) { // Randomly select one of the Haha assets var assetIndex = Math.floor(Math.random() * 3) + 1; var particle = LK.getAsset('ConfettisHaha' + assetIndex, { anchorX: 0.5, anchorY: 0.5 }); // Set initial position randomly on the screen particle.x = Math.random() * self.screenWidth; particle.y = Math.random() * self.screenHeight; // Set initial scale and alpha particle.scale.set(self.initialScale, self.initialScale); particle.alpha = 0; // Set random initial rotation (±15 degrees) particle.rotation = (Math.random() * 60 - 30) * (Math.PI / 180); // Convert to radians // Set animation parameters particle.duration = self.minDuration + Math.random() * self.durationVariation; // 2-4 seconds (longer overall duration) particle.age = 0; particle.maxScale = self.minScale + Math.random() * self.scaleVariation; // 0.5-1.0 // Add a small random delay for each particle (slightly longer for less abrupt appearance) particle.delay = Math.random() * self.maxDelaySeconds; // 0-0.5 seconds delay // Add to container self.addChild(particle); self.particles.push(particle); } self.active = true; }; // Update particles self.update = function () { if (!self.active) { return; } var activeParticles = false; for (var i = 0; i < self.particles.length; i++) { var particle = self.particles[i]; // Handle delay if (particle.delay > 0) { particle.delay -= 1 / 60; // Assuming 60fps activeParticles = true; continue; } // Update age particle.age += 1 / 60; // Assuming 60fps // Calculate progress (0 to 1) var progress = particle.age / particle.duration; if (progress < 1) { activeParticles = true; // Fade in gradually, stay visible longer, fade out gradually if (progress < self.fadeInThreshold) { particle.alpha = progress * (1 / self.fadeInThreshold); // 0 to 1 in first 20% of time } else if (progress > self.fadeOutThreshold) { particle.alpha = (1 - progress) * (1 / (1 - self.fadeOutThreshold)); // 1 to 0 in last 20% of time } else { particle.alpha = 1; // Stay fully visible for 60% of time } // Explosive scale effect - slightly slower scale up, then stay at max var scaleProgress; if (progress < self.scaleUpThreshold) { // Slightly slower elastic-like scaling in first 25% of time // Overshoot slightly and then settle scaleProgress = self.overshootFactor * Math.pow(progress / self.scaleUpThreshold, 0.8); if (scaleProgress > 1.1) { scaleProgress = 1.1; } } else if (progress < self.settleThreshold) { // Slower settle to normal scale scaleProgress = 1.1 - (progress - self.scaleUpThreshold) * (0.1 / (self.settleThreshold - self.scaleUpThreshold)); // 1.1 to 1.0 } else { scaleProgress = 1; // Stay at max scale } var currentScale = particle.maxScale * scaleProgress; // Add oscillation after reaching max scale to simulate laughing if (progress >= self.settleThreshold) { // Calculate oscillation frequency and amplitude var oscillationFreq = self.oscillationFrequency; // Higher = faster oscillation var oscillationAmplitude = self.oscillationAmplitude; // Higher = more intense oscillation // Apply sine wave oscillation that decreases in amplitude over time var oscillationFactor = Math.sin(progress * oscillationFreq * Math.PI) * oscillationAmplitude; // Gradually reduce oscillation amplitude as we approach the end var dampingFactor = 1 - (progress - self.settleThreshold) / (1 - self.settleThreshold); oscillationFactor *= dampingFactor; // Apply oscillation to scale currentScale *= 1 + oscillationFactor; } particle.scale.set(currentScale, currentScale); } } // If no active particles, stop updating if (!activeParticles) { self.active = false; // Remove particles for (var i = 0; i < self.particles.length; i++) { if (self.particles[i].parent) { self.particles[i].parent.removeChild(self.particles[i]); } } self.particles = []; } }; return self; }); var ConfettisParty = Container.expand(function () { var self = Container.call(this); // Properties self.particles = []; self.particleCount = 200; self.colors = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF]; self.gravity = 0.25; self.active = false; self.screenWidth = 2048; self.screenHeight = 2732; self.spawnedParticles = 0; // Track how many particles have been spawned self.spawnRate = 10; // Particles to spawn per frame self.spawnComplete = false; // Flag to track if all particles have been spawned // Configuration properties self.minScale = 0.5; // Minimum scale of particles self.scaleVariation = 1.0; // Random variation added to scale self.yStartPosition = 600; // Starting Y position for particles self.yVariation = self.screenHeight / 2; // Random variation in Y position self.centerY = self.screenHeight / 8; // Target center Y position self.minSpeedX = 5; // Minimum horizontal speed self.speedXVariation = 5; // Random variation in X speed self.minSpeedY = 15; // Minimum vertical speed self.speedYVariation = 10; // Random variation in Y speed self.spawnInterval = 50; // Milliseconds between spawning batches // Create confetti particles self.createParticles = function (x, y) { // Clear existing particles for (var i = 0; i < self.particles.length; i++) { if (self.particles[i].parent) { self.particles[i].parent.removeChild(self.particles[i]); } } self.particles = []; self.spawnedParticles = 0; self.spawnComplete = false; self.active = true; // Initial spawn self.spawnParticles(); // Set up interval for continued spawning var spawnInterval = LK.setInterval(function () { self.spawnParticles(); if (self.spawnComplete) { LK.clearInterval(spawnInterval); } }, self.spawnInterval); // Spawn every 50ms for a more gradual effect }; // Spawn a batch of particles self.spawnParticles = function () { if (self.spawnComplete) { return; } var particlesToSpawn = Math.min(self.spawnRate, self.particleCount - self.spawnedParticles); for (var i = 0; i < particlesToSpawn; i++) { var particle = LK.getAsset('ConfettiPartyBase', { anchorX: 0.5, anchorY: 0.5 }); // Random color particle.tint = self.colors[Math.floor(Math.random() * self.colors.length)]; // Random scale between 0.5 and 1.5 var scale = self.minScale + Math.random() * self.scaleVariation; particle.scale.set(scale, scale); // Random rotation particle.rotation = Math.random() * Math.PI * 2; // Set initial position at left or right border var startFromLeft = Math.random() > 0.5; particle.x = startFromLeft ? 0 : self.screenWidth; particle.y = self.yStartPosition + Math.random() * self.yVariation; // Set velocity toward upper center var centerX = self.screenWidth / 2; var centerY = self.centerY; // Calculate angle to center var dx = centerX - particle.x; var dy = centerY - particle.y; var angle = Math.atan2(dy, dx); // Set velocity based on angle with some randomness var speedX = self.minSpeedX + Math.random() * self.speedXVariation; var speedY = self.minSpeedY + Math.random() * self.speedYVariation; particle.vx = Math.cos(angle) * speedX; particle.vy = Math.sin(angle) * speedY; // Add to container self.addChild(particle); self.particles.push(particle); } self.spawnedParticles += particlesToSpawn; if (self.spawnedParticles >= self.particleCount) { self.spawnComplete = true; } }; // Update particles self.update = function () { if (!self.active) { return; } var activeParticles = false; for (var i = 0; i < self.particles.length; i++) { var particle = self.particles[i]; // Apply gravity after particles reach their peak if (particle.vy > 0) { particle.vy += self.gravity; } else { // Slow down upward movement particle.vy += self.gravity * 0.5; } // Update position particle.x += particle.vx; particle.y += particle.vy; // Rotate particle particle.rotation += 0.05; // Check if particle is still active if (particle.y < self.screenHeight) { activeParticles = true; } } // If no active particles, stop updating if (!activeParticles) { self.active = false; // Remove particles for (var i = 0; i < self.particles.length; i++) { if (self.particles[i].parent) { self.particles[i].parent.removeChild(self.particles[i]); } } self.particles = []; } }; return self; }); var ConfettisSmiley = Container.expand(function () { var self = Container.call(this); // Properties self.particles = []; self.particleCount = 10; self.active = false; self.screenWidth = 2048; self.screenHeight = 2732; // Configuration properties self.baseOffsetX = 250; // How far offscreen particles start self.minSpeed = 9; // Minimum horizontal speed self.speedVariation = 3; // Random variation added to speed self.rotationSpeedRatio = 0.005; // Ratio of rotation speed to horizontal speed self.minScale = 0.7; // Minimum scale of particles self.scaleVariation = 0.3; // Random variation added to scale self.maxDelaySeconds = 2; // Maximum random delay before movement starts self.yMargin = 200; // Margin from top and bottom of screen // Create confetti particles self.createParticles = function (x, y) { // Clear existing particles for (var i = 0; i < self.particles.length; i++) { if (self.particles[i].parent) { self.particles[i].parent.removeChild(self.particles[i]); } } self.particles = []; // Create new particles for (var i = 0; i < self.particleCount; i++) { // Randomly select one of the Smiley assets var assetIndex = Math.floor(Math.random() * 3) + 1; var particle = LK.getAsset('ConfettisSmiley' + assetIndex, { anchorX: 0.5, anchorY: 0.5 }); // Set initial position at left or right border var startFromLeft = Math.random() > 0.5; particle.x = startFromLeft ? -self.baseOffsetX : self.screenWidth + self.baseOffsetX; particle.y = self.yMargin + Math.random() * (self.screenHeight - 2 * self.yMargin); // Random Y position with margin // Set horizontal velocity particle.vx = startFromLeft ? self.minSpeed + Math.random() * self.speedVariation : -(self.minSpeed + Math.random() * self.speedVariation); // Set rotation speed (faster for faster moving particles) particle.rotationSpeed = particle.vx * self.rotationSpeedRatio; // Set scale var scale = self.minScale + Math.random() * self.scaleVariation; particle.scale.set(scale, scale); // Add delay before starting movement particle.delay = Math.random() * self.maxDelaySeconds; // 0-maxDelaySeconds seconds delay // Add to container self.addChild(particle); self.particles.push(particle); } self.active = true; }; // Update particles self.update = function () { if (!self.active) { return; } var activeParticles = false; for (var i = 0; i < self.particles.length; i++) { var particle = self.particles[i]; // Handle delay if (particle.delay > 0) { particle.delay -= 1 / 60; // Assuming 60fps activeParticles = true; continue; } // Update position particle.x += particle.vx; // Rotate particle (rolling effect) particle.rotation += particle.rotationSpeed; // Check if particle is still on screen (with margin) if (particle.vx > 0 && particle.x < self.screenWidth + self.baseOffsetX || particle.vx < 0 && particle.x > -self.baseOffsetX) { activeParticles = true; } } // If no active particles, stop updating if (!activeParticles) { self.active = false; // Remove particles for (var i = 0; i < self.particles.length; i++) { if (self.particles[i].parent) { self.particles[i].parent.removeChild(self.particles[i]); } } self.particles = []; } }; return self; }); var DebugPoints = Container.expand(function () { var self = Container.call(this); // Create points for face tracking debugging self.points = { leftEye: self.attachAsset('debugFacePoints', { anchorX: 0.5, anchorY: 0.5 }), rightEye: self.attachAsset('debugFacePoints', { anchorX: 0.5, anchorY: 0.5 }), noseTip: self.attachAsset('debugFacePoints', { anchorX: 0.5, anchorY: 0.5 }), mouthCenter: self.attachAsset('debugFacePoints', { anchorX: 0.5, anchorY: 0.5 }), upperLip: self.attachAsset('debugFacePoints', { anchorX: 0.5, anchorY: 0.5 }), lowerLip: self.attachAsset('debugFacePoints', { anchorX: 0.5, anchorY: 0.5 }), chin: self.attachAsset('debugFacePoints', { anchorX: 0.5, anchorY: 0.5 }) }; // Update debug points to match face points self.update = function () { if (!facekit) { return; } if (facekit.leftEye) { self.points.leftEye.x = facekit.leftEye.x; self.points.leftEye.y = facekit.leftEye.y; } if (facekit.rightEye) { self.points.rightEye.x = facekit.rightEye.x; self.points.rightEye.y = facekit.rightEye.y; } if (facekit.noseTip) { self.points.noseTip.x = facekit.noseTip.x; self.points.noseTip.y = facekit.noseTip.y; } if (facekit.mouthCenter) { self.points.mouthCenter.x = facekit.mouthCenter.x; self.points.mouthCenter.y = facekit.mouthCenter.y; } if (facekit.upperLip) { self.points.upperLip.x = facekit.upperLip.x; self.points.upperLip.y = facekit.upperLip.y; } if (facekit.lowerLip) { self.points.lowerLip.x = facekit.lowerLip.x; self.points.lowerLip.y = facekit.lowerLip.y; } if (facekit.chin) { self.points.chin.x = facekit.chin.x; self.points.chin.y = facekit.chin.y; } }; return self; }); var SettingsPopup = Container.expand(function () { var self = Container.call(this); var popupGraphics = self.attachAsset('FrameSettingsPopup', { anchorX: 0.5, anchorY: 0.5 }); var popupGraphics = self.attachAsset('FrameSettingsTitle', { anchorX: 0.5, anchorY: 0.5, y: -1060, blendMode: 0 }); // Create sound toggle container self.soundToggle = new Container(); self.soundToggle.x = -450; self.soundToggle.y = -700; self.addChild(self.soundToggle); // Add sound icon var soundIcon = self.soundToggle.attachAsset('IconSound', { anchorX: 0.5, anchorY: 0.5, x: -0 }); // Add click handler for sound icon soundIcon.down = function (x, y, obj) { // Play the current sound if it's not "None" and sound is enabled if (soundEnabled && punchlineSounds[currentPunchlineSound].asset) { LK.getSound(punchlineSounds[currentPunchlineSound].asset).play(); } }; // Add left chevron button self.soundLeftChevronButton = self.soundToggle.attachAsset('ButtonLeftChevron', { anchorX: 0.5, anchorY: 0.5, x: 220, y: 0 }); // Add click handler for left chevron button self.soundLeftChevronButton.down = function (x, y, obj) { self.changeSound(-1); }; // Add sound text that displays "None" by default self.soundText = new Text2("None", { size: 80, fill: 0x333344, align: "left", fontWeight: "bold" }); self.soundText.x = 320; self.soundText.y = -40; self.soundToggle.addChild(self.soundText); // Add right chevron button self.soundRightChevronButton = self.soundToggle.attachAsset('ButtonRightChevron', { anchorX: 0.5, anchorY: 0.5, x: 920, y: 0 }); // Add click handler for right chevron button self.soundRightChevronButton.down = function (x, y, obj) { self.changeSound(1); }; // Function to change the selected sound self.changeSound = function (direction) { // Update the current sound index currentPunchlineSound += direction; storage.currentPunchlineSound = currentPunchlineSound; // Cycle through the sounds instead of clamping if (currentPunchlineSound < 0) { currentPunchlineSound = punchlineSounds.length - 1; } else if (currentPunchlineSound >= punchlineSounds.length) { currentPunchlineSound = 0; } // Ensure currentPunchlineSound is within bounds if (currentPunchlineSound >= 0 && currentPunchlineSound < punchlineSounds.length) { // Update the sound text self.soundText.setText(punchlineSounds[currentPunchlineSound].name); } else { // Fallback to "None" if out of bounds self.soundText.setText("None"); } // Play the sound if it's not "None" and sound is enabled if (soundEnabled && punchlineSounds[currentPunchlineSound].asset) { LK.getSound(punchlineSounds[currentPunchlineSound].asset).play(); } updatePunchlineButtonVisibility(); }; // Initialize sound text with current selection if (currentPunchlineSound >= 0 && currentPunchlineSound < punchlineSounds.length) { self.soundText.setText(punchlineSounds[currentPunchlineSound].name); } else { self.soundText.setText("None"); } // Create confetti toggle container self.confettiToggle = new Container(); self.confettiToggle.x = -450; self.confettiToggle.y = -300; self.addChild(self.confettiToggle); // Add confetti icon var confettiIcon = self.confettiToggle.attachAsset('IconConfetti', { anchorX: 0.5, anchorY: 0.5, x: 0 }); // Add click handler for confetti icon confettiIcon.down = function (x, y, obj) { // Play the selected confetti effect when icon is tapped playConfettiEffect(self.x, self.y); // Play beep sound when confetti is toggled LK.getSound('beep').play(); }; // Add left chevron button self.confettiLeftChevronButton = self.confettiToggle.attachAsset('ButtonLeftChevron', { anchorX: 0.5, anchorY: 0.5, x: 220, y: 0 }); // Add click handler for left chevron button self.confettiLeftChevronButton.down = function (x, y, obj) { self.changeConfetti(-1); }; // Add confetti text that displays "None" by default self.confettiText = new Text2("None", { size: 80, fill: 0x333344, align: "left", fontWeight: "bold" }); self.confettiText.x = 320; self.confettiText.y = -40; self.confettiToggle.addChild(self.confettiText); // Add right chevron button self.confettiRightChevronButton = self.confettiToggle.attachAsset('ButtonRightChevron', { anchorX: 0.5, anchorY: 0.5, x: 920, y: 0 }); // Add click handler for right chevron button self.confettiRightChevronButton.down = function (x, y, obj) { self.changeConfetti(1); }; // Function to change the selected confetti effect self.changeConfetti = function (direction) { // Update the current confetti index currentPunchlineConfetti += direction; // Validate the index to ensure it's within bounds if (currentPunchlineConfetti < 0) { currentPunchlineConfetti = punchlineConfettis.length - 1; } else if (currentPunchlineConfetti >= punchlineConfettis.length) { currentPunchlineConfetti = 0; } // Store the validated index storage.currentPunchlineConfetti = currentPunchlineConfetti; // Update the confetti text self.confettiText.setText(punchlineConfettis[currentPunchlineConfetti].name); // Play the selected confetti effect when changing types playConfettiEffect(self.x, self.y); updatePunchlineButtonVisibility(); // Play beep sound when confetti is toggled LK.getSound('beep').play(); }; // Initialize confetti text with current selection self.confettiText.setText(punchlineConfettis[currentPunchlineConfetti].name); // Create background toggle container self.backgroundToggle = new Container(); self.backgroundToggle.x = -450; self.backgroundToggle.y = 100; self.addChild(self.backgroundToggle); // Add background icon var backgroundIcon = self.backgroundToggle.attachAsset('IconBackgroundToggle', { anchorX: 0.5, anchorY: 0.5, x: 0 }); // Add click handler for background icon backgroundIcon.down = function (x, y, obj) { // Toggle background visibility toggleBackground(); }; // Add background text self.backgroundText = new Text2(backgroundVisible ? "Blank" : "Camera", { size: 80, fill: 0x333344, align: "left", fontWeight: "bold" }); self.backgroundText.x = 320; self.backgroundText.y = -40; self.backgroundToggle.addChild(self.backgroundText); // Add left chevron button self.backgroundLeftChevronButton = self.backgroundToggle.attachAsset('ButtonLeftChevron', { anchorX: 0.5, anchorY: 0.5, x: 220, y: 0 }); // Add click handler for left chevron button self.backgroundLeftChevronButton.down = function (x, y, obj) { self.changeBackground(-1); }; // Add right chevron button self.backgroundRightChevronButton = self.backgroundToggle.attachAsset('ButtonRightChevron', { anchorX: 0.5, anchorY: 0.5, x: 920, y: 0 }); // Add click handler for right chevron button self.backgroundRightChevronButton.down = function (x, y, obj) { self.changeBackground(1); }; // Function to change the background visibility self.changeBackground = function (direction) { // Toggle background visibility using the central function // which will also update the text in the settings popup toggleBackground(); }; // Initialize background text with current selection self.backgroundText.setText(backgroundVisible ? "Blank" : "Camera"); // Create zoom toggle container self.zoomToggle = new Container(); self.zoomToggle.x = -450; self.zoomToggle.y = 500; // Position below background toggle self.addChild(self.zoomToggle); // Add zoom icon var zoomIcon = self.zoomToggle.attachAsset('IconZoom', { anchorX: 0.5, anchorY: 0.5, x: 0 }); // Add click handler for zoom icon zoomIcon.down = function (x, y, obj) { // Play beep sound when zoom icon is clicked LK.getSound('beep').play(); }; // Add zoom text self.zoomText = new Text2("Zoom " + Math.round(currentZoomLevel * 100) + "%", { size: 80, fill: 0x333344, align: "left", fontWeight: "bold" }); self.zoomText.x = 320; self.zoomText.y = -40; self.zoomToggle.addChild(self.zoomText); // Add left chevron button self.zoomLeftChevronButton = self.zoomToggle.attachAsset('ButtonLeftChevron', { anchorX: 0.5, anchorY: 0.5, x: 220, y: 0 }); // Add click handler for left chevron button self.zoomLeftChevronButton.down = function (x, y, obj) { self.changeZoom(-0.1); // Decrease zoom by 10% }; // Add right chevron button self.zoomRightChevronButton = self.zoomToggle.attachAsset('ButtonRightChevron', { anchorX: 0.5, anchorY: 0.5, x: 920, y: 0 }); // Add click handler for right chevron button self.zoomRightChevronButton.down = function (x, y, obj) { self.changeZoom(0.1); // Increase zoom by 10% }; // Function to change the zoom level self.changeZoom = function (delta) { // Update zoom level with bounds checking (0-100%) currentZoomLevel = Math.max(0, Math.min(1, currentZoomLevel + delta)); // Calculate the current scale ratio based on zoom level // Linear interpolation between minScaleRatio and maxScaleRatio currentScaleRatio = minScaleRatio + (maxScaleRatio - minScaleRatio) * currentZoomLevel; // Apply the new scale ratio to the troll face container if (trollFace) { trollFace.scale.x = currentScaleRatio; trollFace.scale.y = currentScaleRatio; } // Save to storage storage.currentZoomLevel = currentZoomLevel; // Update the zoom text self.updateZoomText(); // Play beep sound when zoom is changed LK.getSound('beep').play(); }; // Function to update zoom text self.updateZoomText = function () { self.zoomText.setText("Zoom " + Math.round(currentZoomLevel * 100) + "%"); }; // Initialize zoom text with current level self.updateZoomText(); // Add ButtonOk at the bottom of the settingsPopup var buttonOk = self.attachAsset('ButtonOk', { anchorX: 0.5, anchorY: 0.5, y: 1060 // Position at the bottom of the popup }); buttonOk.down = function (x, y, obj) { // Hide the settings popup when ButtonOk is pressed self.visible = false; bypassTracking = false; // Reset bypassTracking when closing settings trollFace.visible = true; // Show trollface when settings popup is hidden // Play beep sound when settings popup is closed LK.getSound('beep').play(); }; // Define any additional properties or methods for the SettingsPopup here // Add a function to update the background toggle label self.updateBackgroundToggleLabel = function () { // Update the background text to reflect the current state self.backgroundText.setText(backgroundVisible ? "Blank" : "Camera"); }; // Define onShow to call the update function self.onShow = function () { self.updateBackgroundToggleLabel(); self.updateZoomText(); }; return self; }); var TrollFace = Container.expand(function () { var self = Container.call(this); self.scales = { head: { x: 1, y: 1 }, leftEye: { x: 1, y: 1 }, rightEye: { x: 1, y: 1 }, upperLip: { x: 1, y: 1 }, lowerLip: { x: 1, y: 1 } }; // Properties self.currentStyle = 1; self.currentOffsets = trollFaceOffsets[self.currentStyle - 1]; self.elements = {}; // Initialize cached position objects to reuse self.positionCache = { head: { x: 0, y: 0, scaleX: 1, scaleY: 1 }, leftEye: { x: 0, y: 0, scaleX: 1, scaleY: 1 }, rightEye: { x: 0, y: 0, scaleX: 1, scaleY: 1 }, upperLip: { x: 0, y: 0, scaleX: 1, scaleY: 1 }, lowerLip: { x: 0, y: 0, scaleX: 1, scaleY: 1 } }; // Initialize the troll face elements self.initialize = function (style) { // Clear previous elements self.removeAllElements(); // Create new elements for the selected style self.createFaceElements(style || self.currentStyle); }; // Create face elements for a specific style self.createFaceElements = function (style) { // Create head self.elements.head = self.attachAsset('trollHead' + style, { anchorX: 0.5, anchorY: 0.5, scale: 1, alpha: 1 // DEBUG }); // Create eyes self.elements.leftEye = self.attachAsset('trollLeftEye' + style, { anchorX: 0.5, anchorY: 0.5, scale: 1 }); self.elements.rightEye = self.attachAsset('trollRightEye' + style, { anchorX: 0.5, anchorY: 0.5, scale: 1 }); // Create upper lip self.elements.upperLip = self.attachAsset('trollUpperLip' + style, { anchorX: 0.5, anchorY: 0.5, scale: 1 }); // Create lower lip self.elements.lowerLip = self.attachAsset('trollLowerLip' + style, { anchorX: 0.5, anchorY: 0.5, scale: 1 }); }; // Remove all face elements self.removeAllElements = function () { if (self.elements.head) { self.removeChild(self.elements.head); } if (self.elements.leftEye) { self.removeChild(self.elements.leftEye); } if (self.elements.rightEye) { self.removeChild(self.elements.rightEye); } if (self.elements.upperLip) { self.removeChild(self.elements.upperLip); } if (self.elements.lowerLip) { self.removeChild(self.elements.lowerLip); } }; // Change to the next troll face style self.nextStyle = function (forcedStyle) { self.currentStyle = forcedStyle || self.currentStyle % trollFaceOffsets.length + 1; self.initialize(); self.currentOffsets = trollFaceOffsets[self.currentStyle - 1]; // Calculate face scale once and reuse var baseScale = self.calculateFaceScale(facekit); // Calculate all scales upfront self.scales = { head: { x: baseScale * self.currentOffsets.head.sx, y: baseScale * self.currentOffsets.head.sy }, leftEye: { x: baseScale * self.currentOffsets.leftEye.sx, y: baseScale * self.currentOffsets.leftEye.sy }, rightEye: { x: baseScale * self.currentOffsets.rightEye.sx, y: baseScale * self.currentOffsets.rightEye.sy }, upperLip: { x: baseScale * self.currentOffsets.upperLip.sx, y: baseScale * self.currentOffsets.upperLip.sy }, lowerLip: { x: baseScale * self.currentOffsets.lowerLip.sx, y: baseScale * self.currentOffsets.lowerLip.sy } }; return self.currentStyle; }; // Helper function to clamp a value between min and max self.clampPosition = function (value, min, max) { return Math.min(Math.max(value, min), max); }; // Helper function to ensure scale is an object self.ensureScaleIsObject = function (element) { if (_typeof(element.scale) !== 'object') { element.scale = { x: 1, y: 1 }; } }; // Helper function to update a face element self.updateFaceElement = function (elementName, x, y, scaleX, scaleY, makeVisible) { var element = self.elements[elementName]; if (!element) { return; } // Ensure scale is an object self.ensureScaleIsObject(element); // Apply position with clamping using scaled boundaries var elementOffset = self.currentOffsets[elementName]; element.x = self.clampPosition(x, elementOffset.minX * currentEyeDistance, elementOffset.maxX * currentEyeDistance); element.y = self.clampPosition(y, elementOffset.minY * currentEyeDistance, elementOffset.maxY * currentEyeDistance); // Apply scale element.scale.x = scaleX; element.scale.y = scaleY; // Set visibility if needed if (makeVisible) { element.visible = true; } }; // Helper function to update all face elements self.updateAllFaceElements = function (kit, makeVisible, useDefaultScales) { var elementNames = ['head', 'leftEye', 'rightEye', 'upperLip', 'lowerLip']; var positions = self.positionCache; // Calculate positions for all elements positions.head.x = 0; positions.head.y = 0; positions.head.scaleX = useDefaultScales ? 1 : self.scales.head.x; positions.head.scaleY = useDefaultScales ? 1 : self.scales.head.y; // Get the rotation angle for constraint calculations var rotationAngle = self.rotation; var cosAngle = Math.cos(-rotationAngle); // Negative to counter-rotate var sinAngle = Math.sin(-rotationAngle); // For other elements, calculate based on kit positions for (var i = 1; i < elementNames.length; i++) { var name = elementNames[i]; var kitElement = kit[name]; if (kitElement) { var scaleX, scaleY; // Determine which scale to use based on element type and useDefaultScales flag if (useDefaultScales) { scaleX = 1 * self.currentOffsets[name].sx; scaleY = 1 * self.currentOffsets[name].sy; } else { if (name === 'leftEye') { scaleX = self.scales.leftEye.x; scaleY = self.scales.leftEye.y; } else if (name === 'rightEye') { scaleX = self.scales.rightEye.x; scaleY = self.scales.rightEye.y; } else if (name === 'upperLip') { scaleX = self.scales.upperLip.x; scaleY = self.scales.upperLip.y; } else if (name === 'lowerLip') { scaleX = self.scales.lowerLip.x; scaleY = self.scales.lowerLip.y; } } // Calculate position using relative offsets scaled by eye distance var rawX = kitElement.x - self.x + self.currentOffsets[name].x * currentEyeDistance; var rawY = kitElement.y - self.y + self.currentOffsets[name].y * currentEyeDistance; // Apply rotation constraints to maintain relative positions // This counter-rotates the positions to keep elements in proper alignment positions[name].x = rawX * cosAngle - rawY * sinAngle; positions[name].y = rawX * sinAngle + rawY * cosAngle; positions[name].scaleX = scaleX; positions[name].scaleY = scaleY; } } // Update each element with calculated positions for (var j = 0; j < elementNames.length; j++) { var elemName = elementNames[j]; if (self.elements[elemName] && positions[elemName]) { var pos = positions[elemName]; self.updateFaceElement(elemName, pos.x, pos.y, pos.scaleX, pos.scaleY, makeVisible); } } // Handle mouth open adjustment if (kit.mouthOpen && self.elements.lowerLip) { //self.elements.lowerLip.scale.y = self.scales.lip.y * 1.5; } }; // Update face elements to match real face self.updateFacePosition = function () { if (!facekit) { return; } // If in first phase of centering, directly apply stored positions with translation if (self.isFirstPhaseCentering && self.relativePositions) { // Apply stored positions to each element for (var name in self.elements) { if (self.relativePositions[name]) { var element = self.elements[name]; element.x = self.relativePositions[name].x; element.y = self.relativePositions[name].y; element.scale.x = self.relativePositions[name].scaleX; element.scale.y = self.relativePositions[name].scaleY; } } // Set rotation to stored value self.rotation = self.relativePositions.rotation; return; // Skip the normal update process } // Get kit based on tracking state var kit = bypassTracking ? fakeCamera : facekitMgr.isTracking ? facekitMgr.currentFacekit : facekit; // If re-centering, use fakeCamera data for positioning without changing bypassTracking if (self.isRecentering) { kit = fakeCamera; } // Update global eye distance once per frame currentEyeDistance = self.getEyeDistance(kit); var baseScale = self.calculateFaceScale(kit); // Calculate face rotation - use FacekitManager if tracking, otherwise use local calculation var rotation = facekitMgr.isTracking ? facekitMgr.currentRotation + Math.PI : self.calculateFaceRotation(kit); // If re-centering or in first phase, reset rotation to 0 if (self.isRecentering || self.isFirstPhaseCentering) { rotation = 0; } // Update scales self.scales = { head: { x: baseScale * self.currentOffsets.head.sx, y: baseScale * self.currentOffsets.head.sy }, leftEye: { x: baseScale * self.currentOffsets.leftEye.sx, y: baseScale * self.currentOffsets.leftEye.sy }, rightEye: { x: baseScale * self.currentOffsets.rightEye.sx, y: baseScale * self.currentOffsets.rightEye.sy }, upperLip: { x: baseScale * self.currentOffsets.upperLip.sx, y: baseScale * self.currentOffsets.upperLip.sy }, lowerLip: { x: baseScale * self.currentOffsets.lowerLip.sx, y: baseScale * self.currentOffsets.lowerLip.sy } }; if (bypassTracking) { self.x = 2048 / 2; self.y = 2732 / 2; self.rotation = 0; // Reset rotation in bypass mode // Use the global fakeCamera with dynamic scales self.updateAllFaceElements(fakeCamera, true, false); return; } // Apply rotation to the entire troll face container self.rotation = rotation; // Update all elements with kit self.updateAllFaceElements(kit, true, false); }; // Calculate scale based on eye distance self.calculateFaceScale = function (kit) { return currentEyeDistance * 0.005; }; // Calculate face rotation angle based on eye positions self.calculateFaceRotation = function (kit) { if (kit && kit.leftEye && kit.rightEye) { // Get eye positions var leftEye = kit.leftEye; var rightEye = kit.rightEye; // Calculate angle - simpler approach // This gives us the angle of the line connecting the eyes var angle = Math.atan2(rightEye.y - leftEye.y, rightEye.x - leftEye.x); // More efficient angle wrapping using modulo var angleDiff = angle - currentRotation; // Normalize the difference to [-PI, PI] range // Using efficient modulo approach rather than while loops angleDiff = (angleDiff + Math.PI) % (2 * Math.PI) - Math.PI; // Apply smoothing to the rotation currentRotation = currentRotation + angleDiff * 0.2; return currentRotation + Math.PI; } return 0; // Default rotation (no rotation) }; // Get eye distance from kit self.getEyeDistance = function (kit) { if (kit && kit.leftEye && kit.rightEye) { var dx = kit.leftEye.x - kit.rightEye.x; var dy = kit.leftEye.y - kit.rightEye.y; return Math.sqrt(dx * dx + dy * dy); } return 200; // Default eye distance }; // Add a property to track if we're in re-centering mode self.isRecentering = false; // Add a property to track if we're in the first phase of centering (maintaining relative positions) self.isFirstPhaseCentering = false; // Store relative positions of elements during centering self.relativePositions = {}; // Capture current face positions for first phase centering self.captureCurrentPositions = function () { // Store the exact current positions and scales of all elements if (!self.elements) { return; } // Create a snapshot of the current state self.relativePositions = { // Store positions and scales for each element head: self.elements.head ? { x: self.elements.head.x, y: self.elements.head.y, scaleX: self.elements.head.scale.x, scaleY: self.elements.head.scale.y } : null, leftEye: self.elements.leftEye ? { x: self.elements.leftEye.x, y: self.elements.leftEye.y, scaleX: self.elements.leftEye.scale.x, scaleY: self.elements.leftEye.scale.y } : null, rightEye: self.elements.rightEye ? { x: self.elements.rightEye.x, y: self.elements.rightEye.y, scaleX: self.elements.rightEye.scale.x, scaleY: self.elements.rightEye.scale.y } : null, upperLip: self.elements.upperLip ? { x: self.elements.upperLip.x, y: self.elements.upperLip.y, scaleX: self.elements.upperLip.scale.x, scaleY: self.elements.upperLip.scale.y } : null, lowerLip: self.elements.lowerLip ? { x: self.elements.lowerLip.x, y: self.elements.lowerLip.y, scaleX: self.elements.lowerLip.scale.x, scaleY: self.elements.lowerLip.scale.y } : null, // Store current rotation rotation: self.rotation, // Store current container position containerX: self.x, containerY: self.y }; }; return self; }); /**** * Initialize Game ****/ // FacekitManager: handles face tracking, smoothing, and fallback var game = new LK.Game({ backgroundColor: 0xFFFFFF }); /**** * Game Code ****/ // FacekitManager: handles face tracking, smoothing, and fallback /**** * Global Variables ****/ var FacekitManager = function FacekitManager() { var self = new Container(); // Public properties self.currentFacekit = null; // Current smoothed face data self.isTracking = false; // Current tracking state self.currentRotation = 0; // Current smoothed rotation (accessible directly for performance) self.smoothingFactor = 0.5; // Default smoothing factor for facial features self.headSmoothingFactor = 0.2; // Separate smoothing factor for head position (noseTip) self.lastLipPosition = null; // Last lower lip position for tracking detection self.trackingStoppedCounter = 100; // Counter for tracking detection self.trackingStoppedDelay = 100; // Delay threshold for tracking detection // Default positions - defined inline var defaultFacekit = { leftEye: { x: 1380, y: 958 }, rightEye: { x: 673, y: 970 }, upperLip: { x: 1027, y: 1610 }, lowerLip: { x: 1030, y: 1613 }, noseTip: { x: 1024, y: 1366 }, mouthOpen: false }; // Initialize manager self.initialize = function (fakeFacekit) { // Use provided fallback or default self.fallbackFacekit = fakeFacekit || defaultFacekit; // Initialize current data as reference to fallback self.currentFacekit = self.fallbackFacekit; // Create a reusable object for smoothing calculations self.smoothFacekit = { leftEye: { x: 0, y: 0 }, rightEye: { x: 0, y: 0 }, upperLip: { x: 0, y: 0 }, lowerLip: { x: 0, y: 0 }, noseTip: { x: 0, y: 0 }, mouthOpen: false }; return self; }; // Process new tracking data and update tracking status self.updateTrackingStatus = function (rawFacekit) { // Check if there's valid face data var hasFaceData = !!(rawFacekit && rawFacekit.lowerLip); if (!hasFaceData) { self.isTracking = false; return false; } // Check if lower lip position has changed using existing tracking mechanism if (self.lastLipPosition === rawFacekit.lowerLip.y) { self.trackingStoppedCounter--; if (self.trackingStoppedCounter <= 0) { self.isTracking = false; self.trackingStoppedCounter = self.trackingStoppedDelay; // Reset delay } } else { self.isTracking = true; self.lastLipPosition = rawFacekit.lowerLip.y; self.trackingStoppedCounter = self.trackingStoppedDelay; // Reset delay } // If tracking, process the face data if (self.isTracking) { // If tracking just started, initialize smooth values if (self.currentFacekit === self.fallbackFacekit) { self._initSmoothValues(rawFacekit); } // Apply smoothing to each facial feature self._smoothValues(rawFacekit); // Set currentFacekit to the smoothed values self.currentFacekit = self.smoothFacekit; } else { // Use fallback when not tracking self.currentFacekit = self.fallbackFacekit; } // Return tracking state return self.isTracking; }; // Initialize smooth values with raw data self._initSmoothValues = function (rawFacekit) { // Directly set initial values from raw data for (var key in rawFacekit) { if (rawFacekit[key] && self.smoothFacekit[key] && typeof rawFacekit[key].x !== 'undefined') { self.smoothFacekit[key].x = rawFacekit[key].x; self.smoothFacekit[key].y = rawFacekit[key].y; } } // Initialize rotation directly if (rawFacekit.leftEye && rawFacekit.rightEye) { self.currentRotation = Math.atan2(rawFacekit.rightEye.y - rawFacekit.leftEye.y, rawFacekit.rightEye.x - rawFacekit.leftEye.x); } }; // Apply smoothing to all values self._smoothValues = function (rawFacekit) { // Smooth positions (reuse existing objects) for (var key in rawFacekit) { if (rawFacekit[key] && self.smoothFacekit[key] && typeof rawFacekit[key].x !== 'undefined') { // Apply position smoothing directly with different factors for head vs. other elements var factor = key === 'noseTip' ? self.headSmoothingFactor : self.smoothingFactor; self.smoothFacekit[key].x += (rawFacekit[key].x - self.smoothFacekit[key].x) * factor; self.smoothFacekit[key].y += (rawFacekit[key].y - self.smoothFacekit[key].y) * factor; } } // Calculate and smooth rotation if (rawFacekit.leftEye && rawFacekit.rightEye) { var newAngle = Math.atan2(rawFacekit.rightEye.y - rawFacekit.leftEye.y, rawFacekit.rightEye.x - rawFacekit.leftEye.x); // Improved angle normalization to prevent full rotations - no while loops var angleDiff = newAngle - self.currentRotation; // Efficiently handle -PI/PI boundary crossing (replaces the while loops) if (angleDiff > Math.PI) { angleDiff -= 2 * Math.PI; } else if (angleDiff < -Math.PI) { angleDiff += 2 * Math.PI; } // Apply safeguard against large changes var maxRotationPerFrame = Math.PI / 10; // Limit rotation change // Clamp the rotation change to prevent sudden large rotations if (angleDiff > maxRotationPerFrame) { angleDiff = maxRotationPerFrame; } if (angleDiff < -maxRotationPerFrame) { angleDiff = -maxRotationPerFrame; } // Apply smoothing self.currentRotation += angleDiff * self.smoothingFactor; // Normalize the result angle to ensure it stays in -PI to PI range // Using efficient modulo approach rather than while loops self.currentRotation = (self.currentRotation + 3 * Math.PI) % (2 * Math.PI) - Math.PI; } // Copy mouth state self.smoothFacekit.mouthOpen = rawFacekit.mouthOpen; }; return self; }; var debugMode = false; // DEBUG MODE DEBUG MODE DEBUG MODE var bypassTracking = false; // Global variable to bypass face tracking var currentEyeDistance = 200; // Global variable to store current eye distance var currentRotation = 0; // Current smoothed rotation value var settingsPopup; // Global variable for settings popup var soundEnabled = true; // Global variable to track if sound is enabled var confettiEnabled = true; // Global variable to track if confetti is enabled var backgroundVisible = true; // Global variable to track if background is visible var currentZoomLevel = 0.5; // Global variable to track zoom level (50%) var minScaleRatio = 0.5; // Minimum scale ratio for zoom var maxScaleRatio = 2; // Maximum scale ratio for zoom var currentScaleRatio = 1.0; // Current scale ratio based on zoom level var punchlineSounds = [{ name: "None", asset: "" }, { name: "Rimshot", asset: "punchlineSound1" }, { name: "Trombone 1", asset: "punchlineSound2" }, { name: "Trombone 2", asset: "punchlineSound3" }, { name: "Laugh", asset: "punchlineSound4" }, { name: "Crowd Laugh", asset: "punchlineSound5" }]; // Array to store available punchline sounds var punchlineConfettis = [{ name: "None" }, { name: "Confettis", "class": "ConfettisParty" }, { name: "Ha ha ha!", "class": "ConfettisHaha" }, { name: "Smileys", "class": "ConfettisSmiley" }]; // Array to store available punchline confetti effects var currentPunchlineSound = 5; // Default to "Crowd Laugh" var storedSoundIndex = storage.currentPunchlineSound; // If there's a valid stored sound index, use it if (typeof storedSoundIndex === 'number' && storedSoundIndex >= 0 && storedSoundIndex < punchlineSounds.length) { currentPunchlineSound = storedSoundIndex; } var currentPunchlineConfetti = 2; // Default to "Ha ha ha" var storedConfettiIndex = storage.currentPunchlineConfetti; // If there's a valid stored confetti index, use it if (typeof storedConfettiIndex === 'number' && storedConfettiIndex >= 0 && storedConfettiIndex < punchlineConfettis.length) { currentPunchlineConfetti = storedConfettiIndex; } var facekitMgr; var background; var instructionText; var styleText; var trollFace; var debugPoints; var backgroundContainer; var middlegroundContainer; var foregroundContainer; var isTrackingFace = false; // Global flag to track face detection state var targetPosition; var lastNosePosition = null; // Global variable to store last facekit.noseTip.x var trackingStoppedDelay = 100; var trackingStoppedCounter = trackingStoppedDelay; var buttonSettings; var buttonPunchline; // Global variable for punchline button var faceContainerBg; // Global variable for face container background var settingsButtonBounds; var trollFaceOffsets = [{ head: { x: 0, y: 0, sx: 1, sy: 1 }, leftEye: { x: 0.13, y: 0.5, sx: 0.4, sy: 0.4, minX: 0.7, maxX: 0.9, minY: -0.5, maxY: 0 }, rightEye: { x: 0.2, y: 0.5, sx: 0.6, sy: 0.6, minX: -0.5, maxX: -0.2, minY: -0.5, maxY: 0 }, upperLip: { x: 0.25, y: 0.05, sx: 0.9, sy: 0.9, minX: -1, maxX: 1, minY: 0, maxY: 0.42 }, lowerLip: { x: 0.25, y: 0.06, sx: 0.9, sy: 0.9, minX: -1, maxX: 0.25, minY: 0, maxY: 0.9 } }, { head: { x: 0, y: 0, sx: 1, sy: 1 }, leftEye: { x: 0, y: 0.5, sx: 0.6, sy: 0.6, minX: 0.2, maxX: 0.7, minY: 0, maxY: 0.6 }, rightEye: { x: 0.1, y: 0.3, sx: 0.6, sy: 0.6, minX: -0.7, maxX: 0.1, minY: 0, maxY: 0.4 }, upperLip: { x: -0.2, y: 0.1, sx: 0.5, sy: 0.5, minX: -0.3, maxX: 0, minY: -1, maxY: 1 }, lowerLip: { x: -0.10, y: 0.12, sx: 0.5, sy: 0.5, minX: -0.2, maxX: 0, minY: 0, maxY: 0.8 } }, { head: { x: 0, y: 0, sx: 1, sy: 1 }, leftEye: { x: 0.13, y: -0.02, sx: 0.8, sy: 0.8, minX: 0.6, maxX: 0.72, minY: -0.75, maxY: -0.6 }, rightEye: { x: 0.15, y: -0.02, sx: 0.8, sy: 0.8, minX: -0.6, maxX: -0.3, minY: -0.75, maxY: -0.6 }, upperLip: { x: 0.1, y: -0.3, sx: 1, sy: 1, minX: -0.5, maxX: 0.5, minY: -1.0, maxY: 1.0 }, lowerLip: { x: 0.15, y: -0.10, sx: 0.93, sy: 0.93, minX: -0.5, maxX: 0.5, minY: -1.0, maxY: 0.55 } }, { head: { x: 0, y: 0, sx: 1, sy: 1 }, leftEye: { x: -0.27, y: 0.325, sx: 0.4, sy: 0.4, minX: -0.3, maxX: 0.4, minY: -0.3, maxY: 0 }, rightEye: { x: -0.1, y: 0.5, sx: 0.4, sy: 0.4, minX: -0.6, maxX: -0.3, minY: 0, maxY: 0.2 }, upperLip: { x: 0, y: 0, sx: 1, sy: 1, minX: -1, maxX: 1, minY: -1, maxY: 1 }, lowerLip: { x: 0, y: 0.25, sx: 0.6, sy: 0.6, minX: -0.2, maxX: 0.2, minY: 0, maxY: 0.8 } }, { head: { x: 0, y: 0, sx: 1, sy: 1 }, leftEye: { x: 0.45, y: 0.35, sx: 0.3, sy: 0.3, minX: 0.9, maxX: 0.95, minY: -0.3, maxY: -0.2 }, rightEye: { x: 0.85, y: 0.16, sx: 0.6, sy: 0.6, minX: 0.3, maxX: 0.5, minY: -0.3, maxY: -0.2 }, upperLip: { x: 0.46, y: 0.125, sx: 0.45, sy: 0.45, minX: 0, maxX: 0.5, minY: -0.1, maxY: 0.5 }, lowerLip: { x: 0.45, y: 0.125, sx: 0.45, sy: 0.45, minX: -0.45, maxX: 0.45, minY: 0.6, maxY: 0.8 } }]; // Define fakeCamera globally with fixed positions for testing var fakeCamera = { leftEye: { x: 1380, y: 958 }, rightEye: { x: 673, y: 1050 }, upperLip: { x: 1027, y: 1610 }, lowerLip: { x: 1030, y: 1713 }, mouthOpen: false, noseTip: { x: 1024, y: 1366 } }; // Global object for bypass tracking mode function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } /**** * Game Functions ****/ // Handle tap anywhere on the screen to change face game.down = function (x, y, obj) { log("Game Down:", obj); // Check if the tap is within the settings button coordinates if (settingsPopup.visible || x >= settingsButtonBounds.x && x <= settingsButtonBounds.x + settingsButtonBounds.width && y >= settingsButtonBounds.y && y <= settingsButtonBounds.y + settingsButtonBounds.height) { return; } // Face style changing is now handled by the faceContainer's down event }; // Update function called every frame game.update = function () { if (bypassTracking) { // When bypassing tracking, position the troll face at the center and update its elements trollFace.updateFacePosition(); return; } if (!facekit || !facekit.noseTip) { return; } // Update tracking status and get smoothed face data var isCurrentlyTracking = facekitMgr.updateTrackingStatus(facekit); // Update tracking state UI if changed if (isCurrentlyTracking !== isTrackingFace) { isTrackingFace = isCurrentlyTracking; instructionText.setText(isCurrentlyTracking ? "Tracking..." : "No Face found"); } // Update troll face position to match real face if (isTrackingFace) { // Reset isRecentering flag when face tracking is detected trollFace.isRecentering = false; trollFace.isFirstPhaseCentering = false; // Clear stored positions to prevent any lingering effects trollFace.relativePositions = {}; // Reset head position to default (0,0) relative to container if (trollFace.elements && trollFace.elements.head) { trollFace.elements.head.x = 0; trollFace.elements.head.y = 0; } // Use the smoothed nose tip position trollFace.x = facekitMgr.currentFacekit.noseTip.x; trollFace.y = facekitMgr.currentFacekit.noseTip.y; // Use the original updateFacePosition with smoothed data trollFace.updateFacePosition(); trollFace.isCentered = false; trollFace.isCentering = false; } else { // If face is not detected, return the face to the center if (!trollFace.isCentered) { if (trollFace.isCentering) { // Don't exit the update function, just skip starting a new tween // This allows other updates to continue // Continue updating face elements during centering trollFace.updateFacePosition(); } else { trollFace.isCentering = true; // First phase: Capture current positions and maintain them during animation trollFace.captureCurrentPositions(); trollFace.isFirstPhaseCentering = true; trollFace.isRecentering = false; //LK.effects.flashScreen(0xFFFFFF, 300); // Flash screen tween(trollFace, { x: 2048 / 2, y: 2732 / 2 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { // Second phase: Switch to default positions trollFace.isFirstPhaseCentering = false; trollFace.isRecentering = true; trollFace.isCentered = true; trollFace.isCentering = false; // First call updateFacePosition to calculate proper scales // This won't affect positions yet since we'll animate them trollFace.updateFacePosition(); // Get positions directly from the updateAllFaceElements function var fakePositions = {}; // Store the original updateFaceElement function var originalUpdateFaceElement = trollFace.updateFaceElement; // Override the function temporarily to capture positions trollFace.updateFaceElement = function (name, x, y, scaleX, scaleY) { // Store the position and scale that would be applied fakePositions[name] = { x: x, y: y, scaleX: scaleX, scaleY: scaleY }; }; // Call the function to calculate positions without applying them trollFace.updateAllFaceElements(fakeCamera, false, false); // Restore the original function trollFace.updateFaceElement = originalUpdateFaceElement; // Animate each element independently to the correct positions for (var name in trollFace.elements) { if (trollFace.elements[name] && fakePositions[name]) { var element = trollFace.elements[name]; var target = fakePositions[name]; // Create tween for this element tween(element, { x: target.x, y: target.y, scaleX: target.scaleX, scaleY: target.scaleY }, { duration: 500, easing: tween.easeOut }); } } } }); } } // Update face tracking state if (isTrackingFace) { isTrackingFace = false; instructionText.setText("No Face found"); } } // If in debug mode, display the face elements positions if (debugMode) { // Clear existing debug points if (debugPoints) { for (var i = 0; i < debugPoints.children.length; i++) { debugPoints.children[i].alpha = 0; } } // Draw debug points for each face element if (facekit) { var pointIndex = 0; // Draw a point for each facial feature for (var key in facekit) { if (facekit[key] && typeof facekit[key].x !== 'undefined') { var point = debugPoints.children[pointIndex++]; if (point) { point.x = facekit[key].x; point.y = facekit[key].y; point.alpha = 1; } } } // Display rotation in degrees instructionText.setText("Rotation: " + Math.round(trollFace.rotation * 180 / Math.PI) + "°"); } } }; // Function to play confetti effect at specified position function playConfettiEffect(x, y) { if (confettiEnabled) { var confetti; // Check if a confetti class is specified var selectedConfetti = punchlineConfettis[currentPunchlineConfetti]; if (selectedConfetti && selectedConfetti["class"]) { // Create the appropriate confetti effect based on the selected type switch (selectedConfetti["class"]) { case "ConfettisParty": confetti = new ConfettisParty(); break; case "ConfettisHaha": confetti = new ConfettisHaha(); break; case "ConfettisSmiley": confetti = new ConfettisSmiley(); break; default: // Should not reach here with our current configuration return; } // Initialize the confetti effect confetti.createParticles(x, y); foregroundContainer.addChild(confetti); // Update confetti particles var updateInterval = LK.setInterval(function () { if (confetti.active) { confetti.update(); } else { LK.clearInterval(updateInterval); } }, 16); } // If no class specified (None option), do nothing } } // Function to handle face container tap function handleFaceContainerTap() { // Prevent action if settings popup is visible if (settingsPopup && settingsPopup.visible) { return; } // Switch to the next troll face style var newStyle = trollFace.nextStyle(); // Save the current style to storage storage.lastTrollStyle = newStyle; // Update the style text with fade animation updateStyleText(newStyle); // Play switch sound LK.getSound('switchTroll').play(); // If no face tracking, ensure the face is displayed if (!facekit || !facekit.noseTip || !facekitMgr.isTracking) { // Set bypass tracking temporarily to ensure face is displayed var originalBypass = bypassTracking; bypassTracking = true; // Update face position to show the face trollFace.updateFacePosition(); // Restore original bypass value bypassTracking = originalBypass; } } // Function to update punchline button visibility function updatePunchlineButtonVisibility() { // For debugging log("Updating button visibility. Sound: " + currentPunchlineSound + ", Confetti: " + currentPunchlineConfetti); // Hide button if both sound and confetti are set to "None" (index 0) if (currentPunchlineSound === 0 && currentPunchlineConfetti === 0) { buttonPunchline.visible = false; log("Hiding punchline button"); } else { buttonPunchline.visible = true; log("Showing punchline button"); } } // Function to update style text with fade animation function updateStyleText(newStyle) { // Update text immediately styleText.setText('Face ' + newStyle); // Reset to starting state styleText.alpha = 0; // First phase: Fade in with slight scale up tween(styleText, { alpha: 1 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { // Second phase: Fade out with scale down tween(styleText, { alpha: 0 }, { duration: 1000, easing: tween.easeIn, onFinish: function onFinish() {} }); } }); } function log() { if (debugMode) { console.log.apply(console, arguments); } } function initializeGame() { // Initialize game // Create containers for layering backgroundContainer = new Container(); middlegroundContainer = new Container(); foregroundContainer = new Container(); // Add containers to game game.addChild(backgroundContainer); game.addChild(middlegroundContainer); game.addChild(foregroundContainer); // Initialize FacekitManager facekitMgr = new FacekitManager(); facekitMgr.initialize(fakeCamera); // Global target position for the troll face targetPosition = { x: 2048 / 2, y: 2732 / 2 }; // Setup background background = LK.getAsset('whiteBackground', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, visible: true }); backgroundContainer.addChild(background); // Setup UI text instructionText = new Text2('Tap anywhere to change troll face', { size: 50, fill: 0xFF0000 }); instructionText.anchor.set(0.5, 0); LK.gui.top.addChild(instructionText); instructionText.y = 1800; instructionText.x = -500; instructionText.visible = debugMode; // Load the last used style from storage var lastStyle = storage.lastTrollStyle || 3; // Create a container for the face var faceContainer = new Container(); faceContainerBg = LK.getAsset('whiteBackground', { anchorX: 0, anchorY: 0, x: 0, y: 230, width: 2048, height: 2732 - 230 - 430, visible: backgroundVisible }); faceContainer.addChild(faceContainerBg); middlegroundContainer.addChild(faceContainer); // Create the troll face trollFace = new TrollFace(); trollFace.currentStyle = lastStyle; trollFace.initialize(); trollFace.nextStyle(lastStyle); // Apply zoom scale to the entire troll face container trollFace.scale.x = currentScaleRatio; trollFace.scale.y = currentScaleRatio; faceContainer.addChild(trollFace); // Add tap handler to the face container faceContainer.down = function (x, y, obj) { handleFaceContainerTap(); }; // Initialize tracking state isTrackingFace = false; // Add ButtonPunchline to the foreground at the bottom center buttonPunchline = new ButtonPunchline(); buttonPunchline.x = 2048 / 2; // Center horizontally buttonPunchline.y = 2732 - 300; // Position at the bottom foregroundContainer.addChild(buttonPunchline); // Set initial visibility based on current settings updatePunchlineButtonVisibility(); // Add ButtonSettings to the foreground at the bottom right buttonSettings = new ButtonSettings(); buttonSettings.x = 2048 - 200; // Position at the bottom right buttonSettings.y = 140; // Position at the bottom foregroundContainer.addChild(buttonSettings); settingsButtonBounds = { x: buttonSettings.x - buttonSettings.width / 2, y: buttonSettings.y - buttonSettings.height / 2, width: buttonSettings.width, height: buttonSettings.height }; // Initialize the settings popup and add it to foreground settingsPopup = new SettingsPopup(); settingsPopup.x = 2048 / 2; // Center horizontally settingsPopup.y = 2732 / 2; // Center vertically settingsPopup.visible = false; // Initially not visible foregroundContainer.addChild(settingsPopup); // Add style text styleText = new Text2('Face ' + lastStyle, { size: 80, fill: 0x1d3242, align: "left", fontWeight: "bold" }); styleText.anchor.set(0.5, 0); LK.gui.top.addChild(styleText); styleText.y = 40; styleText.alpha = 1; // Ensure initial alpha is set to 1 updateStyleText(lastStyle); // Debug mode (turn on for development, off for production) debugPoints = null; if (debugMode) { debugPoints = new DebugPoints(); foregroundContainer.addChild(debugPoints); // Log facekit to console every second LK.setInterval(function () { log(facekit); if (facekit.lowerLip) { var elementOffset = trollFace.currentOffsets.lowerLip; var eyeDistance = trollFace.getEyeDistance(facekit); log("lowerLip y:", facekit.lowerLip.y, "minY:", elementOffset.minY * eyeDistance, "maxY:", elementOffset.maxY * eyeDistance); } // Display rotation angle in degrees for debugging var rotationDegrees = Math.round(trollFace.rotation * (180 / Math.PI)); instructionText.setText("le:".concat(Math.round(facekit.leftEye.x), ",").concat(Math.round(facekit.leftEye.y), " / ") + "re:".concat(Math.round(facekit.rightEye.x), ",").concat(Math.round(facekit.rightEye.y), " / ") + "ul:".concat(Math.round(facekit.upperLip.x), ",").concat(Math.round(facekit.upperLip.y), " / ") + "ll:".concat(Math.round(facekit.lowerLip.x), ",").concat(Math.round(facekit.lowerLip.y), " / ") + "rot:".concat(rotationDegrees, "°")); }, 1000); } // Load preferences from storage soundEnabled = storage.soundEnabled !== undefined ? storage.soundEnabled : true; confettiEnabled = storage.confettiEnabled !== undefined ? storage.confettiEnabled : true; backgroundVisible = storage.backgroundVisible !== undefined ? storage.backgroundVisible : true; currentZoomLevel = storage.currentZoomLevel !== undefined ? storage.currentZoomLevel : 0.5; // Calculate initial scale ratio based on zoom level currentScaleRatio = minScaleRatio + (maxScaleRatio - minScaleRatio) * currentZoomLevel; // Apply background visibility setting if (background) { background.visible = backgroundVisible; } if (faceContainerBg) { faceContainerBg.visible = backgroundVisible; } } // Initialize the game initializeGame(); // Function to toggle background visibility function toggleBackground() { // Toggle the visibility state backgroundVisible = !backgroundVisible; // Save to storage storage.backgroundVisible = backgroundVisible; // Update visibility of both backgrounds if (faceContainerBg) { faceContainerBg.visible = backgroundVisible; } if (background) { background.visible = backgroundVisible; } // Update text in the settings popup if it exists and is visible if (settingsPopup && settingsPopup.visible) { settingsPopup.updateBackgroundToggleLabel(); } // Play beep sound when background is toggled LK.getSound('beep').play(); }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
var ButtonPunchline = Container.expand(function () {
var self = Container.call(this);
// Attach the ButtonPunchline asset
var buttonGraphics = self.attachAsset('ButtonPunchline', {
anchorX: 0.5,
anchorY: 0.5
});
// Add down event handler for punchline button
self.down = function (x, y, obj) {
// Play sound if enabled
if (soundEnabled) {
// Play the currently selected punchline sound
if (currentPunchlineSound >= 0 && currentPunchlineSound < punchlineSounds.length && punchlineSounds[currentPunchlineSound].asset) {
LK.getSound(punchlineSounds[currentPunchlineSound].asset).play();
}
}
// Create confetti effect if enabled
playConfettiEffect(self.x, self.y - 100);
};
return self;
});
var ButtonSettings = Container.expand(function () {
var self = Container.call(this);
// Attach the ButtonSettings asset
var buttonGraphics = self.attachAsset('ButtonSettingsBackground', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x000000,
alpha: 0.5
});
var buttonGraphics = self.attachAsset('ButtonSettings', {
anchorX: 0.5,
anchorY: 0.5
});
// Add down event handler to show settings popup
self.down = function (x, y, obj) {
// Toggle settings popup visibility
if (settingsPopup) {
settingsPopup.visible = !settingsPopup.visible;
bypassTracking = settingsPopup.visible; // Toggle bypassTracking based on visibility
trollFace.visible = !settingsPopup.visible; // Hide trollface when settings popup is shown
// Call onShow when the popup becomes visible
if (settingsPopup.visible && settingsPopup.onShow) {
settingsPopup.onShow();
}
}
// Play beep sound when settings button is toggled
LK.getSound('beep').play();
// Stop event propagation to prevent changing the face style
if (obj.event && typeof obj.event.stopPropagation === 'function') {
obj.event.stopPropagation();
}
};
// Define any additional properties or methods for the ButtonSettings here
return self;
});
var Confetti = Container.expand(function () {
var self = Container.call(this);
// Properties
self.particles = [];
self.particleCount = 50;
self.colors = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF];
self.gravity = 0.5;
self.active = false;
// Create confetti particles
self.createParticles = function (x, y) {
// Clear existing particles
for (var i = 0; i < self.particles.length; i++) {
if (self.particles[i].parent) {
self.particles[i].parent.removeChild(self.particles[i]);
}
}
self.particles = [];
// Create new particles
for (var i = 0; i < self.particleCount; i++) {
var particle = LK.getAsset('debugFacePoints', {
anchorX: 0.5,
anchorY: 0.5
});
particle.tint = self.colors[Math.floor(Math.random() * self.colors.length)];
particle.scale.set(0.2, 0.2);
// Set initial position
particle.x = x;
particle.y = y;
// Set random velocity
particle.vx = (Math.random() - 0.5) * 20;
particle.vy = (Math.random() - 0.5) * 20 - 10;
// Add to container
self.addChild(particle);
self.particles.push(particle);
}
self.active = true;
};
// Update particles
self.update = function () {
if (!self.active) {
return;
}
var allSettled = true;
for (var i = 0; i < self.particles.length; i++) {
var particle = self.particles[i];
// Apply gravity
particle.vy += self.gravity;
// Update position
particle.x += particle.vx;
particle.y += particle.vy;
// Check if particle is still moving
if (particle.vy < 10) {
allSettled = false;
}
// Check if particle is off screen
if (particle.y > 2732) {
particle.vy = 0;
particle.vx = 0;
}
}
// If all particles have settled, stop updating
if (allSettled) {
self.active = false;
// Remove particles
for (var i = 0; i < self.particles.length; i++) {
if (self.particles[i].parent) {
self.particles[i].parent.removeChild(self.particles[i]);
}
}
self.particles = [];
}
};
return self;
});
var ConfettisHaha = Container.expand(function () {
var self = Container.call(this);
// Properties
self.particles = [];
self.particleCount = 10;
self.active = false;
self.screenWidth = 2048;
self.screenHeight = 2732;
// Configuration properties
self.initialScale = 0.1; // Initial scale when particles appear
self.minScale = 0.5; // Minimum final scale of particles
self.scaleVariation = 0.5; // Random variation added to scale
self.minDuration = 2; // Minimum animation duration in seconds
self.durationVariation = 2; // Random variation added to duration
self.maxDelaySeconds = 0.5; // Maximum random delay before animation starts
self.fadeInThreshold = 0.2; // Progress threshold for fade in (0-1)
self.fadeOutThreshold = 0.8; // Progress threshold for fade out (0-1)
self.scaleUpThreshold = 0.25; // Progress threshold for scale up (0-1)
self.settleThreshold = 0.35; // Progress threshold for settling to final scale (0-1)
self.overshootFactor = 1.15; // How much to overshoot during scale up
self.oscillationFrequency = 12; // Frequency of oscillation
self.oscillationAmplitude = 0.05; // Amplitude of oscillation
self.rotationRange = 30; // Range of random rotation in degrees (±30°)
// Create confetti particles
self.createParticles = function (x, y) {
// Clear existing particles
for (var i = 0; i < self.particles.length; i++) {
if (self.particles[i].parent) {
self.particles[i].parent.removeChild(self.particles[i]);
}
}
self.particles = [];
// Create new particles
for (var i = 0; i < self.particleCount; i++) {
// Randomly select one of the Haha assets
var assetIndex = Math.floor(Math.random() * 3) + 1;
var particle = LK.getAsset('ConfettisHaha' + assetIndex, {
anchorX: 0.5,
anchorY: 0.5
});
// Set initial position randomly on the screen
particle.x = Math.random() * self.screenWidth;
particle.y = Math.random() * self.screenHeight;
// Set initial scale and alpha
particle.scale.set(self.initialScale, self.initialScale);
particle.alpha = 0;
// Set random initial rotation (±15 degrees)
particle.rotation = (Math.random() * 60 - 30) * (Math.PI / 180); // Convert to radians
// Set animation parameters
particle.duration = self.minDuration + Math.random() * self.durationVariation; // 2-4 seconds (longer overall duration)
particle.age = 0;
particle.maxScale = self.minScale + Math.random() * self.scaleVariation; // 0.5-1.0
// Add a small random delay for each particle (slightly longer for less abrupt appearance)
particle.delay = Math.random() * self.maxDelaySeconds; // 0-0.5 seconds delay
// Add to container
self.addChild(particle);
self.particles.push(particle);
}
self.active = true;
};
// Update particles
self.update = function () {
if (!self.active) {
return;
}
var activeParticles = false;
for (var i = 0; i < self.particles.length; i++) {
var particle = self.particles[i];
// Handle delay
if (particle.delay > 0) {
particle.delay -= 1 / 60; // Assuming 60fps
activeParticles = true;
continue;
}
// Update age
particle.age += 1 / 60; // Assuming 60fps
// Calculate progress (0 to 1)
var progress = particle.age / particle.duration;
if (progress < 1) {
activeParticles = true;
// Fade in gradually, stay visible longer, fade out gradually
if (progress < self.fadeInThreshold) {
particle.alpha = progress * (1 / self.fadeInThreshold); // 0 to 1 in first 20% of time
} else if (progress > self.fadeOutThreshold) {
particle.alpha = (1 - progress) * (1 / (1 - self.fadeOutThreshold)); // 1 to 0 in last 20% of time
} else {
particle.alpha = 1; // Stay fully visible for 60% of time
}
// Explosive scale effect - slightly slower scale up, then stay at max
var scaleProgress;
if (progress < self.scaleUpThreshold) {
// Slightly slower elastic-like scaling in first 25% of time
// Overshoot slightly and then settle
scaleProgress = self.overshootFactor * Math.pow(progress / self.scaleUpThreshold, 0.8);
if (scaleProgress > 1.1) {
scaleProgress = 1.1;
}
} else if (progress < self.settleThreshold) {
// Slower settle to normal scale
scaleProgress = 1.1 - (progress - self.scaleUpThreshold) * (0.1 / (self.settleThreshold - self.scaleUpThreshold)); // 1.1 to 1.0
} else {
scaleProgress = 1; // Stay at max scale
}
var currentScale = particle.maxScale * scaleProgress;
// Add oscillation after reaching max scale to simulate laughing
if (progress >= self.settleThreshold) {
// Calculate oscillation frequency and amplitude
var oscillationFreq = self.oscillationFrequency; // Higher = faster oscillation
var oscillationAmplitude = self.oscillationAmplitude; // Higher = more intense oscillation
// Apply sine wave oscillation that decreases in amplitude over time
var oscillationFactor = Math.sin(progress * oscillationFreq * Math.PI) * oscillationAmplitude;
// Gradually reduce oscillation amplitude as we approach the end
var dampingFactor = 1 - (progress - self.settleThreshold) / (1 - self.settleThreshold);
oscillationFactor *= dampingFactor;
// Apply oscillation to scale
currentScale *= 1 + oscillationFactor;
}
particle.scale.set(currentScale, currentScale);
}
}
// If no active particles, stop updating
if (!activeParticles) {
self.active = false;
// Remove particles
for (var i = 0; i < self.particles.length; i++) {
if (self.particles[i].parent) {
self.particles[i].parent.removeChild(self.particles[i]);
}
}
self.particles = [];
}
};
return self;
});
var ConfettisParty = Container.expand(function () {
var self = Container.call(this);
// Properties
self.particles = [];
self.particleCount = 200;
self.colors = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF];
self.gravity = 0.25;
self.active = false;
self.screenWidth = 2048;
self.screenHeight = 2732;
self.spawnedParticles = 0; // Track how many particles have been spawned
self.spawnRate = 10; // Particles to spawn per frame
self.spawnComplete = false; // Flag to track if all particles have been spawned
// Configuration properties
self.minScale = 0.5; // Minimum scale of particles
self.scaleVariation = 1.0; // Random variation added to scale
self.yStartPosition = 600; // Starting Y position for particles
self.yVariation = self.screenHeight / 2; // Random variation in Y position
self.centerY = self.screenHeight / 8; // Target center Y position
self.minSpeedX = 5; // Minimum horizontal speed
self.speedXVariation = 5; // Random variation in X speed
self.minSpeedY = 15; // Minimum vertical speed
self.speedYVariation = 10; // Random variation in Y speed
self.spawnInterval = 50; // Milliseconds between spawning batches
// Create confetti particles
self.createParticles = function (x, y) {
// Clear existing particles
for (var i = 0; i < self.particles.length; i++) {
if (self.particles[i].parent) {
self.particles[i].parent.removeChild(self.particles[i]);
}
}
self.particles = [];
self.spawnedParticles = 0;
self.spawnComplete = false;
self.active = true;
// Initial spawn
self.spawnParticles();
// Set up interval for continued spawning
var spawnInterval = LK.setInterval(function () {
self.spawnParticles();
if (self.spawnComplete) {
LK.clearInterval(spawnInterval);
}
}, self.spawnInterval); // Spawn every 50ms for a more gradual effect
};
// Spawn a batch of particles
self.spawnParticles = function () {
if (self.spawnComplete) {
return;
}
var particlesToSpawn = Math.min(self.spawnRate, self.particleCount - self.spawnedParticles);
for (var i = 0; i < particlesToSpawn; i++) {
var particle = LK.getAsset('ConfettiPartyBase', {
anchorX: 0.5,
anchorY: 0.5
});
// Random color
particle.tint = self.colors[Math.floor(Math.random() * self.colors.length)];
// Random scale between 0.5 and 1.5
var scale = self.minScale + Math.random() * self.scaleVariation;
particle.scale.set(scale, scale);
// Random rotation
particle.rotation = Math.random() * Math.PI * 2;
// Set initial position at left or right border
var startFromLeft = Math.random() > 0.5;
particle.x = startFromLeft ? 0 : self.screenWidth;
particle.y = self.yStartPosition + Math.random() * self.yVariation;
// Set velocity toward upper center
var centerX = self.screenWidth / 2;
var centerY = self.centerY;
// Calculate angle to center
var dx = centerX - particle.x;
var dy = centerY - particle.y;
var angle = Math.atan2(dy, dx);
// Set velocity based on angle with some randomness
var speedX = self.minSpeedX + Math.random() * self.speedXVariation;
var speedY = self.minSpeedY + Math.random() * self.speedYVariation;
particle.vx = Math.cos(angle) * speedX;
particle.vy = Math.sin(angle) * speedY;
// Add to container
self.addChild(particle);
self.particles.push(particle);
}
self.spawnedParticles += particlesToSpawn;
if (self.spawnedParticles >= self.particleCount) {
self.spawnComplete = true;
}
};
// Update particles
self.update = function () {
if (!self.active) {
return;
}
var activeParticles = false;
for (var i = 0; i < self.particles.length; i++) {
var particle = self.particles[i];
// Apply gravity after particles reach their peak
if (particle.vy > 0) {
particle.vy += self.gravity;
} else {
// Slow down upward movement
particle.vy += self.gravity * 0.5;
}
// Update position
particle.x += particle.vx;
particle.y += particle.vy;
// Rotate particle
particle.rotation += 0.05;
// Check if particle is still active
if (particle.y < self.screenHeight) {
activeParticles = true;
}
}
// If no active particles, stop updating
if (!activeParticles) {
self.active = false;
// Remove particles
for (var i = 0; i < self.particles.length; i++) {
if (self.particles[i].parent) {
self.particles[i].parent.removeChild(self.particles[i]);
}
}
self.particles = [];
}
};
return self;
});
var ConfettisSmiley = Container.expand(function () {
var self = Container.call(this);
// Properties
self.particles = [];
self.particleCount = 10;
self.active = false;
self.screenWidth = 2048;
self.screenHeight = 2732;
// Configuration properties
self.baseOffsetX = 250; // How far offscreen particles start
self.minSpeed = 9; // Minimum horizontal speed
self.speedVariation = 3; // Random variation added to speed
self.rotationSpeedRatio = 0.005; // Ratio of rotation speed to horizontal speed
self.minScale = 0.7; // Minimum scale of particles
self.scaleVariation = 0.3; // Random variation added to scale
self.maxDelaySeconds = 2; // Maximum random delay before movement starts
self.yMargin = 200; // Margin from top and bottom of screen
// Create confetti particles
self.createParticles = function (x, y) {
// Clear existing particles
for (var i = 0; i < self.particles.length; i++) {
if (self.particles[i].parent) {
self.particles[i].parent.removeChild(self.particles[i]);
}
}
self.particles = [];
// Create new particles
for (var i = 0; i < self.particleCount; i++) {
// Randomly select one of the Smiley assets
var assetIndex = Math.floor(Math.random() * 3) + 1;
var particle = LK.getAsset('ConfettisSmiley' + assetIndex, {
anchorX: 0.5,
anchorY: 0.5
});
// Set initial position at left or right border
var startFromLeft = Math.random() > 0.5;
particle.x = startFromLeft ? -self.baseOffsetX : self.screenWidth + self.baseOffsetX;
particle.y = self.yMargin + Math.random() * (self.screenHeight - 2 * self.yMargin); // Random Y position with margin
// Set horizontal velocity
particle.vx = startFromLeft ? self.minSpeed + Math.random() * self.speedVariation : -(self.minSpeed + Math.random() * self.speedVariation);
// Set rotation speed (faster for faster moving particles)
particle.rotationSpeed = particle.vx * self.rotationSpeedRatio;
// Set scale
var scale = self.minScale + Math.random() * self.scaleVariation;
particle.scale.set(scale, scale);
// Add delay before starting movement
particle.delay = Math.random() * self.maxDelaySeconds; // 0-maxDelaySeconds seconds delay
// Add to container
self.addChild(particle);
self.particles.push(particle);
}
self.active = true;
};
// Update particles
self.update = function () {
if (!self.active) {
return;
}
var activeParticles = false;
for (var i = 0; i < self.particles.length; i++) {
var particle = self.particles[i];
// Handle delay
if (particle.delay > 0) {
particle.delay -= 1 / 60; // Assuming 60fps
activeParticles = true;
continue;
}
// Update position
particle.x += particle.vx;
// Rotate particle (rolling effect)
particle.rotation += particle.rotationSpeed;
// Check if particle is still on screen (with margin)
if (particle.vx > 0 && particle.x < self.screenWidth + self.baseOffsetX || particle.vx < 0 && particle.x > -self.baseOffsetX) {
activeParticles = true;
}
}
// If no active particles, stop updating
if (!activeParticles) {
self.active = false;
// Remove particles
for (var i = 0; i < self.particles.length; i++) {
if (self.particles[i].parent) {
self.particles[i].parent.removeChild(self.particles[i]);
}
}
self.particles = [];
}
};
return self;
});
var DebugPoints = Container.expand(function () {
var self = Container.call(this);
// Create points for face tracking debugging
self.points = {
leftEye: self.attachAsset('debugFacePoints', {
anchorX: 0.5,
anchorY: 0.5
}),
rightEye: self.attachAsset('debugFacePoints', {
anchorX: 0.5,
anchorY: 0.5
}),
noseTip: self.attachAsset('debugFacePoints', {
anchorX: 0.5,
anchorY: 0.5
}),
mouthCenter: self.attachAsset('debugFacePoints', {
anchorX: 0.5,
anchorY: 0.5
}),
upperLip: self.attachAsset('debugFacePoints', {
anchorX: 0.5,
anchorY: 0.5
}),
lowerLip: self.attachAsset('debugFacePoints', {
anchorX: 0.5,
anchorY: 0.5
}),
chin: self.attachAsset('debugFacePoints', {
anchorX: 0.5,
anchorY: 0.5
})
};
// Update debug points to match face points
self.update = function () {
if (!facekit) {
return;
}
if (facekit.leftEye) {
self.points.leftEye.x = facekit.leftEye.x;
self.points.leftEye.y = facekit.leftEye.y;
}
if (facekit.rightEye) {
self.points.rightEye.x = facekit.rightEye.x;
self.points.rightEye.y = facekit.rightEye.y;
}
if (facekit.noseTip) {
self.points.noseTip.x = facekit.noseTip.x;
self.points.noseTip.y = facekit.noseTip.y;
}
if (facekit.mouthCenter) {
self.points.mouthCenter.x = facekit.mouthCenter.x;
self.points.mouthCenter.y = facekit.mouthCenter.y;
}
if (facekit.upperLip) {
self.points.upperLip.x = facekit.upperLip.x;
self.points.upperLip.y = facekit.upperLip.y;
}
if (facekit.lowerLip) {
self.points.lowerLip.x = facekit.lowerLip.x;
self.points.lowerLip.y = facekit.lowerLip.y;
}
if (facekit.chin) {
self.points.chin.x = facekit.chin.x;
self.points.chin.y = facekit.chin.y;
}
};
return self;
});
var SettingsPopup = Container.expand(function () {
var self = Container.call(this);
var popupGraphics = self.attachAsset('FrameSettingsPopup', {
anchorX: 0.5,
anchorY: 0.5
});
var popupGraphics = self.attachAsset('FrameSettingsTitle', {
anchorX: 0.5,
anchorY: 0.5,
y: -1060,
blendMode: 0
});
// Create sound toggle container
self.soundToggle = new Container();
self.soundToggle.x = -450;
self.soundToggle.y = -700;
self.addChild(self.soundToggle);
// Add sound icon
var soundIcon = self.soundToggle.attachAsset('IconSound', {
anchorX: 0.5,
anchorY: 0.5,
x: -0
});
// Add click handler for sound icon
soundIcon.down = function (x, y, obj) {
// Play the current sound if it's not "None" and sound is enabled
if (soundEnabled && punchlineSounds[currentPunchlineSound].asset) {
LK.getSound(punchlineSounds[currentPunchlineSound].asset).play();
}
};
// Add left chevron button
self.soundLeftChevronButton = self.soundToggle.attachAsset('ButtonLeftChevron', {
anchorX: 0.5,
anchorY: 0.5,
x: 220,
y: 0
});
// Add click handler for left chevron button
self.soundLeftChevronButton.down = function (x, y, obj) {
self.changeSound(-1);
};
// Add sound text that displays "None" by default
self.soundText = new Text2("None", {
size: 80,
fill: 0x333344,
align: "left",
fontWeight: "bold"
});
self.soundText.x = 320;
self.soundText.y = -40;
self.soundToggle.addChild(self.soundText);
// Add right chevron button
self.soundRightChevronButton = self.soundToggle.attachAsset('ButtonRightChevron', {
anchorX: 0.5,
anchorY: 0.5,
x: 920,
y: 0
});
// Add click handler for right chevron button
self.soundRightChevronButton.down = function (x, y, obj) {
self.changeSound(1);
};
// Function to change the selected sound
self.changeSound = function (direction) {
// Update the current sound index
currentPunchlineSound += direction;
storage.currentPunchlineSound = currentPunchlineSound;
// Cycle through the sounds instead of clamping
if (currentPunchlineSound < 0) {
currentPunchlineSound = punchlineSounds.length - 1;
} else if (currentPunchlineSound >= punchlineSounds.length) {
currentPunchlineSound = 0;
}
// Ensure currentPunchlineSound is within bounds
if (currentPunchlineSound >= 0 && currentPunchlineSound < punchlineSounds.length) {
// Update the sound text
self.soundText.setText(punchlineSounds[currentPunchlineSound].name);
} else {
// Fallback to "None" if out of bounds
self.soundText.setText("None");
}
// Play the sound if it's not "None" and sound is enabled
if (soundEnabled && punchlineSounds[currentPunchlineSound].asset) {
LK.getSound(punchlineSounds[currentPunchlineSound].asset).play();
}
updatePunchlineButtonVisibility();
};
// Initialize sound text with current selection
if (currentPunchlineSound >= 0 && currentPunchlineSound < punchlineSounds.length) {
self.soundText.setText(punchlineSounds[currentPunchlineSound].name);
} else {
self.soundText.setText("None");
}
// Create confetti toggle container
self.confettiToggle = new Container();
self.confettiToggle.x = -450;
self.confettiToggle.y = -300;
self.addChild(self.confettiToggle);
// Add confetti icon
var confettiIcon = self.confettiToggle.attachAsset('IconConfetti', {
anchorX: 0.5,
anchorY: 0.5,
x: 0
});
// Add click handler for confetti icon
confettiIcon.down = function (x, y, obj) {
// Play the selected confetti effect when icon is tapped
playConfettiEffect(self.x, self.y);
// Play beep sound when confetti is toggled
LK.getSound('beep').play();
};
// Add left chevron button
self.confettiLeftChevronButton = self.confettiToggle.attachAsset('ButtonLeftChevron', {
anchorX: 0.5,
anchorY: 0.5,
x: 220,
y: 0
});
// Add click handler for left chevron button
self.confettiLeftChevronButton.down = function (x, y, obj) {
self.changeConfetti(-1);
};
// Add confetti text that displays "None" by default
self.confettiText = new Text2("None", {
size: 80,
fill: 0x333344,
align: "left",
fontWeight: "bold"
});
self.confettiText.x = 320;
self.confettiText.y = -40;
self.confettiToggle.addChild(self.confettiText);
// Add right chevron button
self.confettiRightChevronButton = self.confettiToggle.attachAsset('ButtonRightChevron', {
anchorX: 0.5,
anchorY: 0.5,
x: 920,
y: 0
});
// Add click handler for right chevron button
self.confettiRightChevronButton.down = function (x, y, obj) {
self.changeConfetti(1);
};
// Function to change the selected confetti effect
self.changeConfetti = function (direction) {
// Update the current confetti index
currentPunchlineConfetti += direction;
// Validate the index to ensure it's within bounds
if (currentPunchlineConfetti < 0) {
currentPunchlineConfetti = punchlineConfettis.length - 1;
} else if (currentPunchlineConfetti >= punchlineConfettis.length) {
currentPunchlineConfetti = 0;
}
// Store the validated index
storage.currentPunchlineConfetti = currentPunchlineConfetti;
// Update the confetti text
self.confettiText.setText(punchlineConfettis[currentPunchlineConfetti].name);
// Play the selected confetti effect when changing types
playConfettiEffect(self.x, self.y);
updatePunchlineButtonVisibility();
// Play beep sound when confetti is toggled
LK.getSound('beep').play();
};
// Initialize confetti text with current selection
self.confettiText.setText(punchlineConfettis[currentPunchlineConfetti].name);
// Create background toggle container
self.backgroundToggle = new Container();
self.backgroundToggle.x = -450;
self.backgroundToggle.y = 100;
self.addChild(self.backgroundToggle);
// Add background icon
var backgroundIcon = self.backgroundToggle.attachAsset('IconBackgroundToggle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0
});
// Add click handler for background icon
backgroundIcon.down = function (x, y, obj) {
// Toggle background visibility
toggleBackground();
};
// Add background text
self.backgroundText = new Text2(backgroundVisible ? "Blank" : "Camera", {
size: 80,
fill: 0x333344,
align: "left",
fontWeight: "bold"
});
self.backgroundText.x = 320;
self.backgroundText.y = -40;
self.backgroundToggle.addChild(self.backgroundText);
// Add left chevron button
self.backgroundLeftChevronButton = self.backgroundToggle.attachAsset('ButtonLeftChevron', {
anchorX: 0.5,
anchorY: 0.5,
x: 220,
y: 0
});
// Add click handler for left chevron button
self.backgroundLeftChevronButton.down = function (x, y, obj) {
self.changeBackground(-1);
};
// Add right chevron button
self.backgroundRightChevronButton = self.backgroundToggle.attachAsset('ButtonRightChevron', {
anchorX: 0.5,
anchorY: 0.5,
x: 920,
y: 0
});
// Add click handler for right chevron button
self.backgroundRightChevronButton.down = function (x, y, obj) {
self.changeBackground(1);
};
// Function to change the background visibility
self.changeBackground = function (direction) {
// Toggle background visibility using the central function
// which will also update the text in the settings popup
toggleBackground();
};
// Initialize background text with current selection
self.backgroundText.setText(backgroundVisible ? "Blank" : "Camera");
// Create zoom toggle container
self.zoomToggle = new Container();
self.zoomToggle.x = -450;
self.zoomToggle.y = 500; // Position below background toggle
self.addChild(self.zoomToggle);
// Add zoom icon
var zoomIcon = self.zoomToggle.attachAsset('IconZoom', {
anchorX: 0.5,
anchorY: 0.5,
x: 0
});
// Add click handler for zoom icon
zoomIcon.down = function (x, y, obj) {
// Play beep sound when zoom icon is clicked
LK.getSound('beep').play();
};
// Add zoom text
self.zoomText = new Text2("Zoom " + Math.round(currentZoomLevel * 100) + "%", {
size: 80,
fill: 0x333344,
align: "left",
fontWeight: "bold"
});
self.zoomText.x = 320;
self.zoomText.y = -40;
self.zoomToggle.addChild(self.zoomText);
// Add left chevron button
self.zoomLeftChevronButton = self.zoomToggle.attachAsset('ButtonLeftChevron', {
anchorX: 0.5,
anchorY: 0.5,
x: 220,
y: 0
});
// Add click handler for left chevron button
self.zoomLeftChevronButton.down = function (x, y, obj) {
self.changeZoom(-0.1); // Decrease zoom by 10%
};
// Add right chevron button
self.zoomRightChevronButton = self.zoomToggle.attachAsset('ButtonRightChevron', {
anchorX: 0.5,
anchorY: 0.5,
x: 920,
y: 0
});
// Add click handler for right chevron button
self.zoomRightChevronButton.down = function (x, y, obj) {
self.changeZoom(0.1); // Increase zoom by 10%
};
// Function to change the zoom level
self.changeZoom = function (delta) {
// Update zoom level with bounds checking (0-100%)
currentZoomLevel = Math.max(0, Math.min(1, currentZoomLevel + delta));
// Calculate the current scale ratio based on zoom level
// Linear interpolation between minScaleRatio and maxScaleRatio
currentScaleRatio = minScaleRatio + (maxScaleRatio - minScaleRatio) * currentZoomLevel;
// Apply the new scale ratio to the troll face container
if (trollFace) {
trollFace.scale.x = currentScaleRatio;
trollFace.scale.y = currentScaleRatio;
}
// Save to storage
storage.currentZoomLevel = currentZoomLevel;
// Update the zoom text
self.updateZoomText();
// Play beep sound when zoom is changed
LK.getSound('beep').play();
};
// Function to update zoom text
self.updateZoomText = function () {
self.zoomText.setText("Zoom " + Math.round(currentZoomLevel * 100) + "%");
};
// Initialize zoom text with current level
self.updateZoomText();
// Add ButtonOk at the bottom of the settingsPopup
var buttonOk = self.attachAsset('ButtonOk', {
anchorX: 0.5,
anchorY: 0.5,
y: 1060 // Position at the bottom of the popup
});
buttonOk.down = function (x, y, obj) {
// Hide the settings popup when ButtonOk is pressed
self.visible = false;
bypassTracking = false; // Reset bypassTracking when closing settings
trollFace.visible = true; // Show trollface when settings popup is hidden
// Play beep sound when settings popup is closed
LK.getSound('beep').play();
};
// Define any additional properties or methods for the SettingsPopup here
// Add a function to update the background toggle label
self.updateBackgroundToggleLabel = function () {
// Update the background text to reflect the current state
self.backgroundText.setText(backgroundVisible ? "Blank" : "Camera");
};
// Define onShow to call the update function
self.onShow = function () {
self.updateBackgroundToggleLabel();
self.updateZoomText();
};
return self;
});
var TrollFace = Container.expand(function () {
var self = Container.call(this);
self.scales = {
head: {
x: 1,
y: 1
},
leftEye: {
x: 1,
y: 1
},
rightEye: {
x: 1,
y: 1
},
upperLip: {
x: 1,
y: 1
},
lowerLip: {
x: 1,
y: 1
}
};
// Properties
self.currentStyle = 1;
self.currentOffsets = trollFaceOffsets[self.currentStyle - 1];
self.elements = {};
// Initialize cached position objects to reuse
self.positionCache = {
head: {
x: 0,
y: 0,
scaleX: 1,
scaleY: 1
},
leftEye: {
x: 0,
y: 0,
scaleX: 1,
scaleY: 1
},
rightEye: {
x: 0,
y: 0,
scaleX: 1,
scaleY: 1
},
upperLip: {
x: 0,
y: 0,
scaleX: 1,
scaleY: 1
},
lowerLip: {
x: 0,
y: 0,
scaleX: 1,
scaleY: 1
}
};
// Initialize the troll face elements
self.initialize = function (style) {
// Clear previous elements
self.removeAllElements();
// Create new elements for the selected style
self.createFaceElements(style || self.currentStyle);
};
// Create face elements for a specific style
self.createFaceElements = function (style) {
// Create head
self.elements.head = self.attachAsset('trollHead' + style, {
anchorX: 0.5,
anchorY: 0.5,
scale: 1,
alpha: 1 // DEBUG
});
// Create eyes
self.elements.leftEye = self.attachAsset('trollLeftEye' + style, {
anchorX: 0.5,
anchorY: 0.5,
scale: 1
});
self.elements.rightEye = self.attachAsset('trollRightEye' + style, {
anchorX: 0.5,
anchorY: 0.5,
scale: 1
});
// Create upper lip
self.elements.upperLip = self.attachAsset('trollUpperLip' + style, {
anchorX: 0.5,
anchorY: 0.5,
scale: 1
});
// Create lower lip
self.elements.lowerLip = self.attachAsset('trollLowerLip' + style, {
anchorX: 0.5,
anchorY: 0.5,
scale: 1
});
};
// Remove all face elements
self.removeAllElements = function () {
if (self.elements.head) {
self.removeChild(self.elements.head);
}
if (self.elements.leftEye) {
self.removeChild(self.elements.leftEye);
}
if (self.elements.rightEye) {
self.removeChild(self.elements.rightEye);
}
if (self.elements.upperLip) {
self.removeChild(self.elements.upperLip);
}
if (self.elements.lowerLip) {
self.removeChild(self.elements.lowerLip);
}
};
// Change to the next troll face style
self.nextStyle = function (forcedStyle) {
self.currentStyle = forcedStyle || self.currentStyle % trollFaceOffsets.length + 1;
self.initialize();
self.currentOffsets = trollFaceOffsets[self.currentStyle - 1];
// Calculate face scale once and reuse
var baseScale = self.calculateFaceScale(facekit);
// Calculate all scales upfront
self.scales = {
head: {
x: baseScale * self.currentOffsets.head.sx,
y: baseScale * self.currentOffsets.head.sy
},
leftEye: {
x: baseScale * self.currentOffsets.leftEye.sx,
y: baseScale * self.currentOffsets.leftEye.sy
},
rightEye: {
x: baseScale * self.currentOffsets.rightEye.sx,
y: baseScale * self.currentOffsets.rightEye.sy
},
upperLip: {
x: baseScale * self.currentOffsets.upperLip.sx,
y: baseScale * self.currentOffsets.upperLip.sy
},
lowerLip: {
x: baseScale * self.currentOffsets.lowerLip.sx,
y: baseScale * self.currentOffsets.lowerLip.sy
}
};
return self.currentStyle;
};
// Helper function to clamp a value between min and max
self.clampPosition = function (value, min, max) {
return Math.min(Math.max(value, min), max);
};
// Helper function to ensure scale is an object
self.ensureScaleIsObject = function (element) {
if (_typeof(element.scale) !== 'object') {
element.scale = {
x: 1,
y: 1
};
}
};
// Helper function to update a face element
self.updateFaceElement = function (elementName, x, y, scaleX, scaleY, makeVisible) {
var element = self.elements[elementName];
if (!element) {
return;
}
// Ensure scale is an object
self.ensureScaleIsObject(element);
// Apply position with clamping using scaled boundaries
var elementOffset = self.currentOffsets[elementName];
element.x = self.clampPosition(x, elementOffset.minX * currentEyeDistance, elementOffset.maxX * currentEyeDistance);
element.y = self.clampPosition(y, elementOffset.minY * currentEyeDistance, elementOffset.maxY * currentEyeDistance);
// Apply scale
element.scale.x = scaleX;
element.scale.y = scaleY;
// Set visibility if needed
if (makeVisible) {
element.visible = true;
}
};
// Helper function to update all face elements
self.updateAllFaceElements = function (kit, makeVisible, useDefaultScales) {
var elementNames = ['head', 'leftEye', 'rightEye', 'upperLip', 'lowerLip'];
var positions = self.positionCache;
// Calculate positions for all elements
positions.head.x = 0;
positions.head.y = 0;
positions.head.scaleX = useDefaultScales ? 1 : self.scales.head.x;
positions.head.scaleY = useDefaultScales ? 1 : self.scales.head.y;
// Get the rotation angle for constraint calculations
var rotationAngle = self.rotation;
var cosAngle = Math.cos(-rotationAngle); // Negative to counter-rotate
var sinAngle = Math.sin(-rotationAngle);
// For other elements, calculate based on kit positions
for (var i = 1; i < elementNames.length; i++) {
var name = elementNames[i];
var kitElement = kit[name];
if (kitElement) {
var scaleX, scaleY;
// Determine which scale to use based on element type and useDefaultScales flag
if (useDefaultScales) {
scaleX = 1 * self.currentOffsets[name].sx;
scaleY = 1 * self.currentOffsets[name].sy;
} else {
if (name === 'leftEye') {
scaleX = self.scales.leftEye.x;
scaleY = self.scales.leftEye.y;
} else if (name === 'rightEye') {
scaleX = self.scales.rightEye.x;
scaleY = self.scales.rightEye.y;
} else if (name === 'upperLip') {
scaleX = self.scales.upperLip.x;
scaleY = self.scales.upperLip.y;
} else if (name === 'lowerLip') {
scaleX = self.scales.lowerLip.x;
scaleY = self.scales.lowerLip.y;
}
}
// Calculate position using relative offsets scaled by eye distance
var rawX = kitElement.x - self.x + self.currentOffsets[name].x * currentEyeDistance;
var rawY = kitElement.y - self.y + self.currentOffsets[name].y * currentEyeDistance;
// Apply rotation constraints to maintain relative positions
// This counter-rotates the positions to keep elements in proper alignment
positions[name].x = rawX * cosAngle - rawY * sinAngle;
positions[name].y = rawX * sinAngle + rawY * cosAngle;
positions[name].scaleX = scaleX;
positions[name].scaleY = scaleY;
}
}
// Update each element with calculated positions
for (var j = 0; j < elementNames.length; j++) {
var elemName = elementNames[j];
if (self.elements[elemName] && positions[elemName]) {
var pos = positions[elemName];
self.updateFaceElement(elemName, pos.x, pos.y, pos.scaleX, pos.scaleY, makeVisible);
}
}
// Handle mouth open adjustment
if (kit.mouthOpen && self.elements.lowerLip) {
//self.elements.lowerLip.scale.y = self.scales.lip.y * 1.5;
}
};
// Update face elements to match real face
self.updateFacePosition = function () {
if (!facekit) {
return;
}
// If in first phase of centering, directly apply stored positions with translation
if (self.isFirstPhaseCentering && self.relativePositions) {
// Apply stored positions to each element
for (var name in self.elements) {
if (self.relativePositions[name]) {
var element = self.elements[name];
element.x = self.relativePositions[name].x;
element.y = self.relativePositions[name].y;
element.scale.x = self.relativePositions[name].scaleX;
element.scale.y = self.relativePositions[name].scaleY;
}
}
// Set rotation to stored value
self.rotation = self.relativePositions.rotation;
return; // Skip the normal update process
}
// Get kit based on tracking state
var kit = bypassTracking ? fakeCamera : facekitMgr.isTracking ? facekitMgr.currentFacekit : facekit;
// If re-centering, use fakeCamera data for positioning without changing bypassTracking
if (self.isRecentering) {
kit = fakeCamera;
}
// Update global eye distance once per frame
currentEyeDistance = self.getEyeDistance(kit);
var baseScale = self.calculateFaceScale(kit);
// Calculate face rotation - use FacekitManager if tracking, otherwise use local calculation
var rotation = facekitMgr.isTracking ? facekitMgr.currentRotation + Math.PI : self.calculateFaceRotation(kit);
// If re-centering or in first phase, reset rotation to 0
if (self.isRecentering || self.isFirstPhaseCentering) {
rotation = 0;
}
// Update scales
self.scales = {
head: {
x: baseScale * self.currentOffsets.head.sx,
y: baseScale * self.currentOffsets.head.sy
},
leftEye: {
x: baseScale * self.currentOffsets.leftEye.sx,
y: baseScale * self.currentOffsets.leftEye.sy
},
rightEye: {
x: baseScale * self.currentOffsets.rightEye.sx,
y: baseScale * self.currentOffsets.rightEye.sy
},
upperLip: {
x: baseScale * self.currentOffsets.upperLip.sx,
y: baseScale * self.currentOffsets.upperLip.sy
},
lowerLip: {
x: baseScale * self.currentOffsets.lowerLip.sx,
y: baseScale * self.currentOffsets.lowerLip.sy
}
};
if (bypassTracking) {
self.x = 2048 / 2;
self.y = 2732 / 2;
self.rotation = 0; // Reset rotation in bypass mode
// Use the global fakeCamera with dynamic scales
self.updateAllFaceElements(fakeCamera, true, false);
return;
}
// Apply rotation to the entire troll face container
self.rotation = rotation;
// Update all elements with kit
self.updateAllFaceElements(kit, true, false);
};
// Calculate scale based on eye distance
self.calculateFaceScale = function (kit) {
return currentEyeDistance * 0.005;
};
// Calculate face rotation angle based on eye positions
self.calculateFaceRotation = function (kit) {
if (kit && kit.leftEye && kit.rightEye) {
// Get eye positions
var leftEye = kit.leftEye;
var rightEye = kit.rightEye;
// Calculate angle - simpler approach
// This gives us the angle of the line connecting the eyes
var angle = Math.atan2(rightEye.y - leftEye.y, rightEye.x - leftEye.x);
// More efficient angle wrapping using modulo
var angleDiff = angle - currentRotation;
// Normalize the difference to [-PI, PI] range
// Using efficient modulo approach rather than while loops
angleDiff = (angleDiff + Math.PI) % (2 * Math.PI) - Math.PI;
// Apply smoothing to the rotation
currentRotation = currentRotation + angleDiff * 0.2;
return currentRotation + Math.PI;
}
return 0; // Default rotation (no rotation)
};
// Get eye distance from kit
self.getEyeDistance = function (kit) {
if (kit && kit.leftEye && kit.rightEye) {
var dx = kit.leftEye.x - kit.rightEye.x;
var dy = kit.leftEye.y - kit.rightEye.y;
return Math.sqrt(dx * dx + dy * dy);
}
return 200; // Default eye distance
};
// Add a property to track if we're in re-centering mode
self.isRecentering = false;
// Add a property to track if we're in the first phase of centering (maintaining relative positions)
self.isFirstPhaseCentering = false;
// Store relative positions of elements during centering
self.relativePositions = {};
// Capture current face positions for first phase centering
self.captureCurrentPositions = function () {
// Store the exact current positions and scales of all elements
if (!self.elements) {
return;
}
// Create a snapshot of the current state
self.relativePositions = {
// Store positions and scales for each element
head: self.elements.head ? {
x: self.elements.head.x,
y: self.elements.head.y,
scaleX: self.elements.head.scale.x,
scaleY: self.elements.head.scale.y
} : null,
leftEye: self.elements.leftEye ? {
x: self.elements.leftEye.x,
y: self.elements.leftEye.y,
scaleX: self.elements.leftEye.scale.x,
scaleY: self.elements.leftEye.scale.y
} : null,
rightEye: self.elements.rightEye ? {
x: self.elements.rightEye.x,
y: self.elements.rightEye.y,
scaleX: self.elements.rightEye.scale.x,
scaleY: self.elements.rightEye.scale.y
} : null,
upperLip: self.elements.upperLip ? {
x: self.elements.upperLip.x,
y: self.elements.upperLip.y,
scaleX: self.elements.upperLip.scale.x,
scaleY: self.elements.upperLip.scale.y
} : null,
lowerLip: self.elements.lowerLip ? {
x: self.elements.lowerLip.x,
y: self.elements.lowerLip.y,
scaleX: self.elements.lowerLip.scale.x,
scaleY: self.elements.lowerLip.scale.y
} : null,
// Store current rotation
rotation: self.rotation,
// Store current container position
containerX: self.x,
containerY: self.y
};
};
return self;
});
/****
* Initialize Game
****/
// FacekitManager: handles face tracking, smoothing, and fallback
var game = new LK.Game({
backgroundColor: 0xFFFFFF
});
/****
* Game Code
****/
// FacekitManager: handles face tracking, smoothing, and fallback
/****
* Global Variables
****/
var FacekitManager = function FacekitManager() {
var self = new Container();
// Public properties
self.currentFacekit = null; // Current smoothed face data
self.isTracking = false; // Current tracking state
self.currentRotation = 0; // Current smoothed rotation (accessible directly for performance)
self.smoothingFactor = 0.5; // Default smoothing factor for facial features
self.headSmoothingFactor = 0.2; // Separate smoothing factor for head position (noseTip)
self.lastLipPosition = null; // Last lower lip position for tracking detection
self.trackingStoppedCounter = 100; // Counter for tracking detection
self.trackingStoppedDelay = 100; // Delay threshold for tracking detection
// Default positions - defined inline
var defaultFacekit = {
leftEye: {
x: 1380,
y: 958
},
rightEye: {
x: 673,
y: 970
},
upperLip: {
x: 1027,
y: 1610
},
lowerLip: {
x: 1030,
y: 1613
},
noseTip: {
x: 1024,
y: 1366
},
mouthOpen: false
};
// Initialize manager
self.initialize = function (fakeFacekit) {
// Use provided fallback or default
self.fallbackFacekit = fakeFacekit || defaultFacekit;
// Initialize current data as reference to fallback
self.currentFacekit = self.fallbackFacekit;
// Create a reusable object for smoothing calculations
self.smoothFacekit = {
leftEye: {
x: 0,
y: 0
},
rightEye: {
x: 0,
y: 0
},
upperLip: {
x: 0,
y: 0
},
lowerLip: {
x: 0,
y: 0
},
noseTip: {
x: 0,
y: 0
},
mouthOpen: false
};
return self;
};
// Process new tracking data and update tracking status
self.updateTrackingStatus = function (rawFacekit) {
// Check if there's valid face data
var hasFaceData = !!(rawFacekit && rawFacekit.lowerLip);
if (!hasFaceData) {
self.isTracking = false;
return false;
}
// Check if lower lip position has changed using existing tracking mechanism
if (self.lastLipPosition === rawFacekit.lowerLip.y) {
self.trackingStoppedCounter--;
if (self.trackingStoppedCounter <= 0) {
self.isTracking = false;
self.trackingStoppedCounter = self.trackingStoppedDelay; // Reset delay
}
} else {
self.isTracking = true;
self.lastLipPosition = rawFacekit.lowerLip.y;
self.trackingStoppedCounter = self.trackingStoppedDelay; // Reset delay
}
// If tracking, process the face data
if (self.isTracking) {
// If tracking just started, initialize smooth values
if (self.currentFacekit === self.fallbackFacekit) {
self._initSmoothValues(rawFacekit);
}
// Apply smoothing to each facial feature
self._smoothValues(rawFacekit);
// Set currentFacekit to the smoothed values
self.currentFacekit = self.smoothFacekit;
} else {
// Use fallback when not tracking
self.currentFacekit = self.fallbackFacekit;
}
// Return tracking state
return self.isTracking;
};
// Initialize smooth values with raw data
self._initSmoothValues = function (rawFacekit) {
// Directly set initial values from raw data
for (var key in rawFacekit) {
if (rawFacekit[key] && self.smoothFacekit[key] && typeof rawFacekit[key].x !== 'undefined') {
self.smoothFacekit[key].x = rawFacekit[key].x;
self.smoothFacekit[key].y = rawFacekit[key].y;
}
}
// Initialize rotation directly
if (rawFacekit.leftEye && rawFacekit.rightEye) {
self.currentRotation = Math.atan2(rawFacekit.rightEye.y - rawFacekit.leftEye.y, rawFacekit.rightEye.x - rawFacekit.leftEye.x);
}
};
// Apply smoothing to all values
self._smoothValues = function (rawFacekit) {
// Smooth positions (reuse existing objects)
for (var key in rawFacekit) {
if (rawFacekit[key] && self.smoothFacekit[key] && typeof rawFacekit[key].x !== 'undefined') {
// Apply position smoothing directly with different factors for head vs. other elements
var factor = key === 'noseTip' ? self.headSmoothingFactor : self.smoothingFactor;
self.smoothFacekit[key].x += (rawFacekit[key].x - self.smoothFacekit[key].x) * factor;
self.smoothFacekit[key].y += (rawFacekit[key].y - self.smoothFacekit[key].y) * factor;
}
}
// Calculate and smooth rotation
if (rawFacekit.leftEye && rawFacekit.rightEye) {
var newAngle = Math.atan2(rawFacekit.rightEye.y - rawFacekit.leftEye.y, rawFacekit.rightEye.x - rawFacekit.leftEye.x);
// Improved angle normalization to prevent full rotations - no while loops
var angleDiff = newAngle - self.currentRotation;
// Efficiently handle -PI/PI boundary crossing (replaces the while loops)
if (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
} else if (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Apply safeguard against large changes
var maxRotationPerFrame = Math.PI / 10; // Limit rotation change
// Clamp the rotation change to prevent sudden large rotations
if (angleDiff > maxRotationPerFrame) {
angleDiff = maxRotationPerFrame;
}
if (angleDiff < -maxRotationPerFrame) {
angleDiff = -maxRotationPerFrame;
}
// Apply smoothing
self.currentRotation += angleDiff * self.smoothingFactor;
// Normalize the result angle to ensure it stays in -PI to PI range
// Using efficient modulo approach rather than while loops
self.currentRotation = (self.currentRotation + 3 * Math.PI) % (2 * Math.PI) - Math.PI;
}
// Copy mouth state
self.smoothFacekit.mouthOpen = rawFacekit.mouthOpen;
};
return self;
};
var debugMode = false; // DEBUG MODE DEBUG MODE DEBUG MODE
var bypassTracking = false; // Global variable to bypass face tracking
var currentEyeDistance = 200; // Global variable to store current eye distance
var currentRotation = 0; // Current smoothed rotation value
var settingsPopup; // Global variable for settings popup
var soundEnabled = true; // Global variable to track if sound is enabled
var confettiEnabled = true; // Global variable to track if confetti is enabled
var backgroundVisible = true; // Global variable to track if background is visible
var currentZoomLevel = 0.5; // Global variable to track zoom level (50%)
var minScaleRatio = 0.5; // Minimum scale ratio for zoom
var maxScaleRatio = 2; // Maximum scale ratio for zoom
var currentScaleRatio = 1.0; // Current scale ratio based on zoom level
var punchlineSounds = [{
name: "None",
asset: ""
}, {
name: "Rimshot",
asset: "punchlineSound1"
}, {
name: "Trombone 1",
asset: "punchlineSound2"
}, {
name: "Trombone 2",
asset: "punchlineSound3"
}, {
name: "Laugh",
asset: "punchlineSound4"
}, {
name: "Crowd Laugh",
asset: "punchlineSound5"
}]; // Array to store available punchline sounds
var punchlineConfettis = [{
name: "None"
}, {
name: "Confettis",
"class": "ConfettisParty"
}, {
name: "Ha ha ha!",
"class": "ConfettisHaha"
}, {
name: "Smileys",
"class": "ConfettisSmiley"
}]; // Array to store available punchline confetti effects
var currentPunchlineSound = 5; // Default to "Crowd Laugh"
var storedSoundIndex = storage.currentPunchlineSound;
// If there's a valid stored sound index, use it
if (typeof storedSoundIndex === 'number' && storedSoundIndex >= 0 && storedSoundIndex < punchlineSounds.length) {
currentPunchlineSound = storedSoundIndex;
}
var currentPunchlineConfetti = 2; // Default to "Ha ha ha"
var storedConfettiIndex = storage.currentPunchlineConfetti;
// If there's a valid stored confetti index, use it
if (typeof storedConfettiIndex === 'number' && storedConfettiIndex >= 0 && storedConfettiIndex < punchlineConfettis.length) {
currentPunchlineConfetti = storedConfettiIndex;
}
var facekitMgr;
var background;
var instructionText;
var styleText;
var trollFace;
var debugPoints;
var backgroundContainer;
var middlegroundContainer;
var foregroundContainer;
var isTrackingFace = false; // Global flag to track face detection state
var targetPosition;
var lastNosePosition = null; // Global variable to store last facekit.noseTip.x
var trackingStoppedDelay = 100;
var trackingStoppedCounter = trackingStoppedDelay;
var buttonSettings;
var buttonPunchline; // Global variable for punchline button
var faceContainerBg; // Global variable for face container background
var settingsButtonBounds;
var trollFaceOffsets = [{
head: {
x: 0,
y: 0,
sx: 1,
sy: 1
},
leftEye: {
x: 0.13,
y: 0.5,
sx: 0.4,
sy: 0.4,
minX: 0.7,
maxX: 0.9,
minY: -0.5,
maxY: 0
},
rightEye: {
x: 0.2,
y: 0.5,
sx: 0.6,
sy: 0.6,
minX: -0.5,
maxX: -0.2,
minY: -0.5,
maxY: 0
},
upperLip: {
x: 0.25,
y: 0.05,
sx: 0.9,
sy: 0.9,
minX: -1,
maxX: 1,
minY: 0,
maxY: 0.42
},
lowerLip: {
x: 0.25,
y: 0.06,
sx: 0.9,
sy: 0.9,
minX: -1,
maxX: 0.25,
minY: 0,
maxY: 0.9
}
}, {
head: {
x: 0,
y: 0,
sx: 1,
sy: 1
},
leftEye: {
x: 0,
y: 0.5,
sx: 0.6,
sy: 0.6,
minX: 0.2,
maxX: 0.7,
minY: 0,
maxY: 0.6
},
rightEye: {
x: 0.1,
y: 0.3,
sx: 0.6,
sy: 0.6,
minX: -0.7,
maxX: 0.1,
minY: 0,
maxY: 0.4
},
upperLip: {
x: -0.2,
y: 0.1,
sx: 0.5,
sy: 0.5,
minX: -0.3,
maxX: 0,
minY: -1,
maxY: 1
},
lowerLip: {
x: -0.10,
y: 0.12,
sx: 0.5,
sy: 0.5,
minX: -0.2,
maxX: 0,
minY: 0,
maxY: 0.8
}
}, {
head: {
x: 0,
y: 0,
sx: 1,
sy: 1
},
leftEye: {
x: 0.13,
y: -0.02,
sx: 0.8,
sy: 0.8,
minX: 0.6,
maxX: 0.72,
minY: -0.75,
maxY: -0.6
},
rightEye: {
x: 0.15,
y: -0.02,
sx: 0.8,
sy: 0.8,
minX: -0.6,
maxX: -0.3,
minY: -0.75,
maxY: -0.6
},
upperLip: {
x: 0.1,
y: -0.3,
sx: 1,
sy: 1,
minX: -0.5,
maxX: 0.5,
minY: -1.0,
maxY: 1.0
},
lowerLip: {
x: 0.15,
y: -0.10,
sx: 0.93,
sy: 0.93,
minX: -0.5,
maxX: 0.5,
minY: -1.0,
maxY: 0.55
}
}, {
head: {
x: 0,
y: 0,
sx: 1,
sy: 1
},
leftEye: {
x: -0.27,
y: 0.325,
sx: 0.4,
sy: 0.4,
minX: -0.3,
maxX: 0.4,
minY: -0.3,
maxY: 0
},
rightEye: {
x: -0.1,
y: 0.5,
sx: 0.4,
sy: 0.4,
minX: -0.6,
maxX: -0.3,
minY: 0,
maxY: 0.2
},
upperLip: {
x: 0,
y: 0,
sx: 1,
sy: 1,
minX: -1,
maxX: 1,
minY: -1,
maxY: 1
},
lowerLip: {
x: 0,
y: 0.25,
sx: 0.6,
sy: 0.6,
minX: -0.2,
maxX: 0.2,
minY: 0,
maxY: 0.8
}
}, {
head: {
x: 0,
y: 0,
sx: 1,
sy: 1
},
leftEye: {
x: 0.45,
y: 0.35,
sx: 0.3,
sy: 0.3,
minX: 0.9,
maxX: 0.95,
minY: -0.3,
maxY: -0.2
},
rightEye: {
x: 0.85,
y: 0.16,
sx: 0.6,
sy: 0.6,
minX: 0.3,
maxX: 0.5,
minY: -0.3,
maxY: -0.2
},
upperLip: {
x: 0.46,
y: 0.125,
sx: 0.45,
sy: 0.45,
minX: 0,
maxX: 0.5,
minY: -0.1,
maxY: 0.5
},
lowerLip: {
x: 0.45,
y: 0.125,
sx: 0.45,
sy: 0.45,
minX: -0.45,
maxX: 0.45,
minY: 0.6,
maxY: 0.8
}
}];
// Define fakeCamera globally with fixed positions for testing
var fakeCamera = {
leftEye: {
x: 1380,
y: 958
},
rightEye: {
x: 673,
y: 1050
},
upperLip: {
x: 1027,
y: 1610
},
lowerLip: {
x: 1030,
y: 1713
},
mouthOpen: false,
noseTip: {
x: 1024,
y: 1366
}
}; // Global object for bypass tracking mode
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
/****
* Game Functions
****/
// Handle tap anywhere on the screen to change face
game.down = function (x, y, obj) {
log("Game Down:", obj);
// Check if the tap is within the settings button coordinates
if (settingsPopup.visible || x >= settingsButtonBounds.x && x <= settingsButtonBounds.x + settingsButtonBounds.width && y >= settingsButtonBounds.y && y <= settingsButtonBounds.y + settingsButtonBounds.height) {
return;
}
// Face style changing is now handled by the faceContainer's down event
};
// Update function called every frame
game.update = function () {
if (bypassTracking) {
// When bypassing tracking, position the troll face at the center and update its elements
trollFace.updateFacePosition();
return;
}
if (!facekit || !facekit.noseTip) {
return;
}
// Update tracking status and get smoothed face data
var isCurrentlyTracking = facekitMgr.updateTrackingStatus(facekit);
// Update tracking state UI if changed
if (isCurrentlyTracking !== isTrackingFace) {
isTrackingFace = isCurrentlyTracking;
instructionText.setText(isCurrentlyTracking ? "Tracking..." : "No Face found");
}
// Update troll face position to match real face
if (isTrackingFace) {
// Reset isRecentering flag when face tracking is detected
trollFace.isRecentering = false;
trollFace.isFirstPhaseCentering = false;
// Clear stored positions to prevent any lingering effects
trollFace.relativePositions = {};
// Reset head position to default (0,0) relative to container
if (trollFace.elements && trollFace.elements.head) {
trollFace.elements.head.x = 0;
trollFace.elements.head.y = 0;
}
// Use the smoothed nose tip position
trollFace.x = facekitMgr.currentFacekit.noseTip.x;
trollFace.y = facekitMgr.currentFacekit.noseTip.y;
// Use the original updateFacePosition with smoothed data
trollFace.updateFacePosition();
trollFace.isCentered = false;
trollFace.isCentering = false;
} else {
// If face is not detected, return the face to the center
if (!trollFace.isCentered) {
if (trollFace.isCentering) {
// Don't exit the update function, just skip starting a new tween
// This allows other updates to continue
// Continue updating face elements during centering
trollFace.updateFacePosition();
} else {
trollFace.isCentering = true;
// First phase: Capture current positions and maintain them during animation
trollFace.captureCurrentPositions();
trollFace.isFirstPhaseCentering = true;
trollFace.isRecentering = false;
//LK.effects.flashScreen(0xFFFFFF, 300); // Flash screen
tween(trollFace, {
x: 2048 / 2,
y: 2732 / 2
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Second phase: Switch to default positions
trollFace.isFirstPhaseCentering = false;
trollFace.isRecentering = true;
trollFace.isCentered = true;
trollFace.isCentering = false;
// First call updateFacePosition to calculate proper scales
// This won't affect positions yet since we'll animate them
trollFace.updateFacePosition();
// Get positions directly from the updateAllFaceElements function
var fakePositions = {};
// Store the original updateFaceElement function
var originalUpdateFaceElement = trollFace.updateFaceElement;
// Override the function temporarily to capture positions
trollFace.updateFaceElement = function (name, x, y, scaleX, scaleY) {
// Store the position and scale that would be applied
fakePositions[name] = {
x: x,
y: y,
scaleX: scaleX,
scaleY: scaleY
};
};
// Call the function to calculate positions without applying them
trollFace.updateAllFaceElements(fakeCamera, false, false);
// Restore the original function
trollFace.updateFaceElement = originalUpdateFaceElement;
// Animate each element independently to the correct positions
for (var name in trollFace.elements) {
if (trollFace.elements[name] && fakePositions[name]) {
var element = trollFace.elements[name];
var target = fakePositions[name];
// Create tween for this element
tween(element, {
x: target.x,
y: target.y,
scaleX: target.scaleX,
scaleY: target.scaleY
}, {
duration: 500,
easing: tween.easeOut
});
}
}
}
});
}
}
// Update face tracking state
if (isTrackingFace) {
isTrackingFace = false;
instructionText.setText("No Face found");
}
}
// If in debug mode, display the face elements positions
if (debugMode) {
// Clear existing debug points
if (debugPoints) {
for (var i = 0; i < debugPoints.children.length; i++) {
debugPoints.children[i].alpha = 0;
}
}
// Draw debug points for each face element
if (facekit) {
var pointIndex = 0;
// Draw a point for each facial feature
for (var key in facekit) {
if (facekit[key] && typeof facekit[key].x !== 'undefined') {
var point = debugPoints.children[pointIndex++];
if (point) {
point.x = facekit[key].x;
point.y = facekit[key].y;
point.alpha = 1;
}
}
}
// Display rotation in degrees
instructionText.setText("Rotation: " + Math.round(trollFace.rotation * 180 / Math.PI) + "°");
}
}
};
// Function to play confetti effect at specified position
function playConfettiEffect(x, y) {
if (confettiEnabled) {
var confetti;
// Check if a confetti class is specified
var selectedConfetti = punchlineConfettis[currentPunchlineConfetti];
if (selectedConfetti && selectedConfetti["class"]) {
// Create the appropriate confetti effect based on the selected type
switch (selectedConfetti["class"]) {
case "ConfettisParty":
confetti = new ConfettisParty();
break;
case "ConfettisHaha":
confetti = new ConfettisHaha();
break;
case "ConfettisSmiley":
confetti = new ConfettisSmiley();
break;
default:
// Should not reach here with our current configuration
return;
}
// Initialize the confetti effect
confetti.createParticles(x, y);
foregroundContainer.addChild(confetti);
// Update confetti particles
var updateInterval = LK.setInterval(function () {
if (confetti.active) {
confetti.update();
} else {
LK.clearInterval(updateInterval);
}
}, 16);
}
// If no class specified (None option), do nothing
}
}
// Function to handle face container tap
function handleFaceContainerTap() {
// Prevent action if settings popup is visible
if (settingsPopup && settingsPopup.visible) {
return;
}
// Switch to the next troll face style
var newStyle = trollFace.nextStyle();
// Save the current style to storage
storage.lastTrollStyle = newStyle;
// Update the style text with fade animation
updateStyleText(newStyle);
// Play switch sound
LK.getSound('switchTroll').play();
// If no face tracking, ensure the face is displayed
if (!facekit || !facekit.noseTip || !facekitMgr.isTracking) {
// Set bypass tracking temporarily to ensure face is displayed
var originalBypass = bypassTracking;
bypassTracking = true;
// Update face position to show the face
trollFace.updateFacePosition();
// Restore original bypass value
bypassTracking = originalBypass;
}
}
// Function to update punchline button visibility
function updatePunchlineButtonVisibility() {
// For debugging
log("Updating button visibility. Sound: " + currentPunchlineSound + ", Confetti: " + currentPunchlineConfetti);
// Hide button if both sound and confetti are set to "None" (index 0)
if (currentPunchlineSound === 0 && currentPunchlineConfetti === 0) {
buttonPunchline.visible = false;
log("Hiding punchline button");
} else {
buttonPunchline.visible = true;
log("Showing punchline button");
}
}
// Function to update style text with fade animation
function updateStyleText(newStyle) {
// Update text immediately
styleText.setText('Face ' + newStyle);
// Reset to starting state
styleText.alpha = 0;
// First phase: Fade in with slight scale up
tween(styleText, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Second phase: Fade out with scale down
tween(styleText, {
alpha: 0
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: function onFinish() {}
});
}
});
}
function log() {
if (debugMode) {
console.log.apply(console, arguments);
}
}
function initializeGame() {
// Initialize game
// Create containers for layering
backgroundContainer = new Container();
middlegroundContainer = new Container();
foregroundContainer = new Container();
// Add containers to game
game.addChild(backgroundContainer);
game.addChild(middlegroundContainer);
game.addChild(foregroundContainer);
// Initialize FacekitManager
facekitMgr = new FacekitManager();
facekitMgr.initialize(fakeCamera);
// Global target position for the troll face
targetPosition = {
x: 2048 / 2,
y: 2732 / 2
};
// Setup background
background = LK.getAsset('whiteBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
visible: true
});
backgroundContainer.addChild(background);
// Setup UI text
instructionText = new Text2('Tap anywhere to change troll face', {
size: 50,
fill: 0xFF0000
});
instructionText.anchor.set(0.5, 0);
LK.gui.top.addChild(instructionText);
instructionText.y = 1800;
instructionText.x = -500;
instructionText.visible = debugMode;
// Load the last used style from storage
var lastStyle = storage.lastTrollStyle || 3;
// Create a container for the face
var faceContainer = new Container();
faceContainerBg = LK.getAsset('whiteBackground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 230,
width: 2048,
height: 2732 - 230 - 430,
visible: backgroundVisible
});
faceContainer.addChild(faceContainerBg);
middlegroundContainer.addChild(faceContainer);
// Create the troll face
trollFace = new TrollFace();
trollFace.currentStyle = lastStyle;
trollFace.initialize();
trollFace.nextStyle(lastStyle);
// Apply zoom scale to the entire troll face container
trollFace.scale.x = currentScaleRatio;
trollFace.scale.y = currentScaleRatio;
faceContainer.addChild(trollFace);
// Add tap handler to the face container
faceContainer.down = function (x, y, obj) {
handleFaceContainerTap();
};
// Initialize tracking state
isTrackingFace = false;
// Add ButtonPunchline to the foreground at the bottom center
buttonPunchline = new ButtonPunchline();
buttonPunchline.x = 2048 / 2; // Center horizontally
buttonPunchline.y = 2732 - 300; // Position at the bottom
foregroundContainer.addChild(buttonPunchline);
// Set initial visibility based on current settings
updatePunchlineButtonVisibility();
// Add ButtonSettings to the foreground at the bottom right
buttonSettings = new ButtonSettings();
buttonSettings.x = 2048 - 200; // Position at the bottom right
buttonSettings.y = 140; // Position at the bottom
foregroundContainer.addChild(buttonSettings);
settingsButtonBounds = {
x: buttonSettings.x - buttonSettings.width / 2,
y: buttonSettings.y - buttonSettings.height / 2,
width: buttonSettings.width,
height: buttonSettings.height
};
// Initialize the settings popup and add it to foreground
settingsPopup = new SettingsPopup();
settingsPopup.x = 2048 / 2; // Center horizontally
settingsPopup.y = 2732 / 2; // Center vertically
settingsPopup.visible = false; // Initially not visible
foregroundContainer.addChild(settingsPopup);
// Add style text
styleText = new Text2('Face ' + lastStyle, {
size: 80,
fill: 0x1d3242,
align: "left",
fontWeight: "bold"
});
styleText.anchor.set(0.5, 0);
LK.gui.top.addChild(styleText);
styleText.y = 40;
styleText.alpha = 1; // Ensure initial alpha is set to 1
updateStyleText(lastStyle);
// Debug mode (turn on for development, off for production)
debugPoints = null;
if (debugMode) {
debugPoints = new DebugPoints();
foregroundContainer.addChild(debugPoints);
// Log facekit to console every second
LK.setInterval(function () {
log(facekit);
if (facekit.lowerLip) {
var elementOffset = trollFace.currentOffsets.lowerLip;
var eyeDistance = trollFace.getEyeDistance(facekit);
log("lowerLip y:", facekit.lowerLip.y, "minY:", elementOffset.minY * eyeDistance, "maxY:", elementOffset.maxY * eyeDistance);
}
// Display rotation angle in degrees for debugging
var rotationDegrees = Math.round(trollFace.rotation * (180 / Math.PI));
instructionText.setText("le:".concat(Math.round(facekit.leftEye.x), ",").concat(Math.round(facekit.leftEye.y), " / ") + "re:".concat(Math.round(facekit.rightEye.x), ",").concat(Math.round(facekit.rightEye.y), " / ") + "ul:".concat(Math.round(facekit.upperLip.x), ",").concat(Math.round(facekit.upperLip.y), " / ") + "ll:".concat(Math.round(facekit.lowerLip.x), ",").concat(Math.round(facekit.lowerLip.y), " / ") + "rot:".concat(rotationDegrees, "°"));
}, 1000);
}
// Load preferences from storage
soundEnabled = storage.soundEnabled !== undefined ? storage.soundEnabled : true;
confettiEnabled = storage.confettiEnabled !== undefined ? storage.confettiEnabled : true;
backgroundVisible = storage.backgroundVisible !== undefined ? storage.backgroundVisible : true;
currentZoomLevel = storage.currentZoomLevel !== undefined ? storage.currentZoomLevel : 0.5;
// Calculate initial scale ratio based on zoom level
currentScaleRatio = minScaleRatio + (maxScaleRatio - minScaleRatio) * currentZoomLevel;
// Apply background visibility setting
if (background) {
background.visible = backgroundVisible;
}
if (faceContainerBg) {
faceContainerBg.visible = backgroundVisible;
}
}
// Initialize the game
initializeGame();
// Function to toggle background visibility
function toggleBackground() {
// Toggle the visibility state
backgroundVisible = !backgroundVisible;
// Save to storage
storage.backgroundVisible = backgroundVisible;
// Update visibility of both backgrounds
if (faceContainerBg) {
faceContainerBg.visible = backgroundVisible;
}
if (background) {
background.visible = backgroundVisible;
}
// Update text in the settings popup if it exists and is visible
if (settingsPopup && settingsPopup.visible) {
settingsPopup.updateBackgroundToggleLabel();
}
// Play beep sound when background is toggled
LK.getSound('beep').play();
}