/****
* 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();
}