Code edit (1 edits merged)
Please save this source code
User prompt
Call update face after switching
User prompt
Call updatefaceposition once at game start
User prompt
Call updatefaceposition once then only when not tweening
User prompt
Call updatefaceposition only when not tweening
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: target is not an Object. (evaluating 'key in target')' in or related to this line: 'tween(self.elements.head.scale, {' Line Number: 215
User prompt
Apply tween for head scale too āŖš” Consider importing and using the following plugins: @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
User prompt
Add a system to wait for tween anim to end before starting the next āŖš” Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add a global target position and use easing to make face movement less jerky āŖš” Consider importing and using the following plugins: @upit/tween.v1
User prompt
Use nose tip position for face
User prompt
Add a new background asset
User prompt
Remove switchButton (but keep troll face switch feature when player taps screen
User prompt
Add a white background
User prompt
Add separate assets for left and right eyes
User prompt
Please fix the bug: 'TypeError: self.elements.mouth.scale.set is not a function. (In 'self.elements.mouth.scale.set(self.elements.head.scale.x * 0.7, self.elements.head.scale.y * 0.7)', 'self.elements.mouth.scale.set' is undefined)' in or related to this line: 'self.elements.mouth.scale.set(self.elements.head.scale.x * 0.7, self.elements.head.scale.y * 0.7);' Line Number: 246
User prompt
Please fix the bug: 'TypeError: self.elements.rightEye.scale.set is not a function. (In 'self.elements.rightEye.scale.set(self.elements.head.scale.x * 0.5, self.elements.head.scale.y * 0.5)', 'self.elements.rightEye.scale.set' is undefined)' in or related to this line: 'self.elements.rightEye.scale.set(self.elements.head.scale.x * 0.5, self.elements.head.scale.y * 0.5);' Line Number: 234
User prompt
Please fix the bug: 'TypeError: self.elements.leftEye.scale.set is not a function. (In 'self.elements.leftEye.scale.set(self.elements.head.scale.x * 0.5, self.elements.head.scale.y * 0.5)', 'self.elements.leftEye.scale.set' is undefined)' in or related to this line: 'self.elements.leftEye.scale.set(self.elements.head.scale.x * 0.5, self.elements.head.scale.y * 0.5);' Line Number: 223
User prompt
Please fix the bug: 'TypeError: self.elements.head.scale.set is not a function. (In 'self.elements.head.scale.set(scale * 1.5, scale * 1.8)', 'self.elements.head.scale.set' is undefined)' in or related to this line: 'self.elements.head.scale.set(scale * 1.5, scale * 1.8);' Line Number: 211
Code edit (1 edits merged)
Please save this source code
User prompt
TrollFace Takeover
Initial prompt
Facekit funny tool where you face is replaced by a troll face. You can choose between 3 troll faces. Each face has assets for each part, i.e. head_0, leftEye_0, rightEye_0, upperLip_0, lowerLip_0. Player tap screen to switch between faces.
/**** * 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();
}