User prompt
Use the eyes to track the mask height instead of the nose. ↪💡 Consider importing and using the following plugins: @upit/facekit.v1
Code edit (1 edits merged)
Please save this source code
User prompt
Update with: var Mask = Container.expand(function () { var self = Container.call(this); // Store maskGraphics on self to access its properties like height if needed later self.maskGraphics = self.attachAsset('maskImage', { anchorX: 0.5, anchorY: 0.5 }); self.animationFinished = false; // Flag to track if the intro animation is done // Rotation variables - EXACTLY from working example var targetTilt = 0; var tiltSmoothingFactor = 0.11; var tiltScaleFactor = 0.09; self.show = function (targetX, targetY, duration) { self.x = targetX; self.y = -self.maskGraphics.height / 2 - 50; tween(self, { y: targetY }, { duration: duration, easing: tween.easeOutSine, onFinish: function onFinish() { self.animationFinished = true; } }); }; // Function copied EXACTLY from working DragonHead example function calculateFaceTilt() { if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) { // Calculate midpoint between eyes var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2; var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2; // Calculate angle between eye midpoint and mouth, negated to fix direction var dx = facekit.mouthCenter.x - eyeMidX; var dy = facekit.mouthCenter.y - eyeMidY; var angle = -(Math.atan2(dx, dy) * (180 / Math.PI)); // Reduced angle impact return Math.max(-15, Math.min(15, angle * 0.15)); } return 0; // Default to straight when face points aren't available } // Add update method with rotation - EXACTLY like DragonHead self.update = function () { if (!self.animationFinished) { return; } // Rotation tracking - COPIED EXACTLY from working example if (facekit.leftEye && facekit.rightEye) { targetTilt = calculateFaceTilt() * tiltScaleFactor; // Limit rotation to ±15 degrees - DON'T convert to radians here targetTilt = Math.max(-15, Math.min(15, targetTilt)); self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor; } }; return self; }); var MouthAnimator = Container.expand(function () { var self = Container.call(this); // Visemes array for easier management self.visemes = [self.attachAsset('mouthVisemeClosed', { anchorX: 0.5, anchorY: 0.5, visible: false }), self.attachAsset('mouthVisemeOpenS', { anchorX: 0.5, anchorY: 0.5, visible: false }), self.attachAsset('mouthVisemeOpenM', { anchorX: 0.5, anchorY: 0.5, visible: false }), self.attachAsset('mouthVisemeOpenL', { anchorX: 0.5, anchorY: 0.5, visible: false })]; self.currentVisemeIndex = 0; if (self.visemes.length > 0) { self.visemes[self.currentVisemeIndex].visible = true; } // Rotation variables - EXACTLY from working example var targetTilt = 0; var tiltSmoothingFactor = 0.11; var tiltScaleFactor = 0.09; // Function copied EXACTLY from working DragonHead example function calculateFaceTilt() { if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) { // Calculate midpoint between eyes var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2; var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2; // Calculate angle between eye midpoint and mouth, negated to fix direction var dx = facekit.mouthCenter.x - eyeMidX; var dy = facekit.mouthCenter.y - eyeMidY; var angle = -(Math.atan2(dx, dy) * (180 / Math.PI)); // Reduced angle impact return Math.max(-15, Math.min(15, angle * 0.15)); } return 0; // Default to straight when face points aren't available } self.updateViseme = function (volume) { if (self.visemes.length === 0) { return; } var newIndex = 0; // Default to closed mouth if (volume > 0.6) { // Loud newIndex = 3; // mouthVisemeOpenL } else if (volume > 0.3) { // Moderate newIndex = 2; // mouthVisemeOpenM } else if (volume > 0.05) { // Quiet sound, slightly open newIndex = 1; // mouthVisemeOpenS } // else, newIndex remains 0 for mouthVisemeClosed if (self.currentVisemeIndex !== newIndex) { self.visemes[self.currentVisemeIndex].visible = false; // Ensure newIndex is valid before accessing if (newIndex >= 0 && newIndex < self.visemes.length) { self.visemes[newIndex].visible = true; self.currentVisemeIndex = newIndex; } } }; // Add update method with rotation - EXACTLY like DragonHead self.update = function () { // Rotation tracking - COPIED EXACTLY from working example if (facekit.leftEye && facekit.rightEye) { targetTilt = calculateFaceTilt() * tiltScaleFactor; // Limit rotation to ±15 degrees - DON'T convert to radians here targetTilt = Math.max(-15, Math.min(15, targetTilt)); self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor; } }; return self; }); ``` And update your game loop to call the `update` methods: ```javascript // Game update loop game.update = function () { if (isGameActive) { // Update mouth animator position and viseme if (mouthAnimatorInstance) { if (facekit.mouthCenter && (facekit.mouthCenter.x !== 0 || facekit.mouthCenter.y !== 0)) { mouthAnimatorInstance.x = facekit.mouthCenter.x; mouthAnimatorInstance.y = facekit.mouthCenter.y; } else { // Fallback if face data is lost mid-game, keep it centered-ish mouthAnimatorInstance.x = GAME_WIDTH / 2; mouthAnimatorInstance.y = GAME_HEIGHT / 2 + 200; } mouthAnimatorInstance.updateViseme(facekit.volume); mouthAnimatorInstance.update(); // Call the update method for rotation } // Update mask position based on face tracking if (maskInstance) { // ... existing position code ... maskInstance.update(); // Call the update method for rotation } } };
User prompt
Update with: var Mask = Container.expand(function () { var self = Container.call(this); // Store maskGraphics on self to access its properties like height if needed later self.maskGraphics = self.attachAsset('maskImage', { anchorX: 0.5, anchorY: 0.5 }); self.animationFinished = false; // Flag to track if the intro animation is done // Rotation variables - copied exactly from working example var targetTilt = 0; var tiltSmoothingFactor = 0.11; var tiltScaleFactor = 0.09; // Function copied exactly from working DragonHead example function calculateFaceTilt() { if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) { // Calculate midpoint between eyes var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2; var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2; // Calculate angle between eye midpoint and mouth, negated to fix direction var dx = facekit.mouthCenter.x - eyeMidX; var dy = facekit.mouthCenter.y - eyeMidY; var angle = -(Math.atan2(dx, dy) * (180 / Math.PI)); // Reduced angle impact return Math.max(-15, Math.min(15, angle * 0.15)); } return 0; // Default to straight when face points aren't available } self.show = function (targetX, targetY, duration) { self.x = targetX; // Start off-screen (above the visible area) // Considering anchorY is 0.5, initial Y should be -half height relative to screen top self.y = -self.maskGraphics.height / 2 - 50; // 50 pixels buffer to be surely off-screen tween(self, { y: targetY }, { duration: duration, easing: tween.easeOutSine, // A smooth easing function onFinish: function onFinish() { // Optional: anything to do after mask finishes animating self.animationFinished = true; // Mark animation as finished } }); }; // Add rotation update method self.updateRotation = function() { if (!self.animationFinished) { return; } // Rotation tracking - copied exactly from working example if (facekit.leftEye && facekit.rightEye) { targetTilt = calculateFaceTilt() * tiltScaleFactor; // Limit rotation to ±15 degrees - DON'T convert to radians here targetTilt = Math.max(-15, Math.min(15, targetTilt)); self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor; } }; return self; }); var MouthAnimator = Container.expand(function () { var self = Container.call(this); // Visemes array for easier management self.visemes = [self.attachAsset('mouthVisemeClosed', { anchorX: 0.5, anchorY: 0.5, visible: false }), self.attachAsset('mouthVisemeOpenS', { anchorX: 0.5, anchorY: 0.5, visible: false }), self.attachAsset('mouthVisemeOpenM', { anchorX: 0.5, anchorY: 0.5, visible: false }), self.attachAsset('mouthVisemeOpenL', { anchorX: 0.5, anchorY: 0.5, visible: false })]; self.currentVisemeIndex = 0; if (self.visemes.length > 0) { self.visemes[self.currentVisemeIndex].visible = true; } // Rotation variables - copied exactly from working example var targetTilt = 0; var tiltSmoothingFactor = 0.11; var tiltScaleFactor = 0.09; // Function copied exactly from working DragonHead example function calculateFaceTilt() { if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) { // Calculate midpoint between eyes var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2; var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2; // Calculate angle between eye midpoint and mouth, negated to fix direction var dx = facekit.mouthCenter.x - eyeMidX; var dy = facekit.mouthCenter.y - eyeMidY; var angle = -(Math.atan2(dx, dy) * (180 / Math.PI)); // Reduced angle impact return Math.max(-15, Math.min(15, angle * 0.15)); } return 0; // Default to straight when face points aren't available } self.updateViseme = function (volume) { if (self.visemes.length === 0) { return; } var newIndex = 0; // Default to closed mouth if (volume > 0.6) { // Loud newIndex = 3; // mouthVisemeOpenL } else if (volume > 0.3) { // Moderate newIndex = 2; // mouthVisemeOpenM } else if (volume > 0.05) { // Quiet sound, slightly open newIndex = 1; // mouthVisemeOpenS } // else, newIndex remains 0 for mouthVisemeClosed if (self.currentVisemeIndex !== newIndex) { self.visemes[self.currentVisemeIndex].visible = false; // Ensure newIndex is valid before accessing if (newIndex >= 0 && newIndex < self.visemes.length) { self.visemes[newIndex].visible = true; self.currentVisemeIndex = newIndex; } } }; // Add rotation update method self.updateRotation = function() { // Rotation tracking - copied exactly from working example if (facekit.leftEye && facekit.rightEye) { targetTilt = calculateFaceTilt() * tiltScaleFactor; // Limit rotation to ±15 degrees - DON'T convert to radians here targetTilt = Math.max(-15, Math.min(15, targetTilt)); self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor; } }; return self; }); ``` And update your game loop to call the rotation methods: ```javascript // Game update loop game.update = function () { if (isGameActive) { // Update mouth animator position and viseme if (mouthAnimatorInstance) { if (facekit.mouthCenter && (facekit.mouthCenter.x !== 0 || facekit.mouthCenter.y !== 0)) { mouthAnimatorInstance.x = facekit.mouthCenter.x; mouthAnimatorInstance.y = facekit.mouthCenter.y; } else { // Fallback if face data is lost mid-game, keep it centered-ish mouthAnimatorInstance.x = GAME_WIDTH / 2; mouthAnimatorInstance.y = GAME_HEIGHT / 2 + 200; } mouthAnimatorInstance.updateViseme(facekit.volume); mouthAnimatorInstance.updateRotation(); // Add rotation update } // Update mask position based on face tracking if (maskInstance) { // Ensure maskGraphics is available to get its height // This is safe as maskGraphics is attached in Mask constructor var maskGraphicHeight = maskInstance.maskGraphics.height; // Determine target X for the mask var targetMaskX; if (facekit.noseTip && facekit.noseTip.x !== 0) { targetMaskX = facekit.noseTip.x; } else { // Fallback: Use current X if valid (not 0 or undefined), otherwise center. // This handles cases where noseTip data might be temporarily unavailable or zero. targetMaskX = maskInstance.x !== undefined && maskInstance.x !== 0 ? maskInstance.x : GAME_WIDTH / 2; } maskInstance.x = targetMaskX; // Continuously update X position // For Y position, only update after the initial lowering animation is finished if (maskInstance.animationFinished) { var targetMaskY; var yOffset = -50; // Negative Y offset if (facekit.noseTip && facekit.noseTip.y !== 0) { targetMaskY = facekit.noseTip.y - maskGraphicHeight * 0.20 + yOffset; } else { // Fallback for Y: Use current Y if valid and not the initial off-screen Y, otherwise a default. // The initialOffScreenYPos is the value set in Mask.show when animation starts. var initialOffScreenYPos = -maskGraphicHeight / 2 - 50; // This is the original start position, not the target // Apply offset to fallback as well, ensuring consistency targetMaskY = maskInstance.y !== undefined && maskInstance.y !== initialOffScreenYPos + yOffset ? maskInstance.y : GAME_HEIGHT / 3 + yOffset; } maskInstance.y = targetMaskY; } maskInstance.updateRotation(); // Add rotation update // Else (during animation, i.e., !maskInstance.animationFinished): // Y position is controlled by the tween started in maskInstance.show(). // X position is updated live (as done above), making it follow the face horizontally during the drop. } } }; ↪💡 Consider importing and using the following plugins: @upit/facekit.v1, @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
User prompt
update with: /**** * Assets ****/ LK.init.shape('mouthVisemeClosed', {width:250, height:120, color:0xaf1bdb, shape:'box'}) LK.init.shape('mouthVisemeOpenL', {width:250, height:180, color:0x7607f6, shape:'box'}) LK.init.shape('mouthVisemeOpenM', {width:250, height:160, color:0x90bfa0, shape:'box'}) LK.init.shape('mouthVisemeOpenS', {width:250, height:140, color:0xa2610d, shape:'box'}) LK.init.shape('startButtonImage', {width:400, height:150, color:0xadef39, shape:'box'}) LK.init.image('maskImage', {width:1500, height:1500, id:'683e614fee80fa316a84a8f0'}) LK.init.music('karaokeSong') /**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var facekit = LK.import("@upit/facekit.v1"); /**** * Classes ****/ var Mask = Container.expand(function () { var self = Container.call(this); // Store maskGraphics on self to access its properties like height if needed later self.maskGraphics = self.attachAsset('maskImage', { anchorX: 0.5, anchorY: 0.5 }); self.animationFinished = false; // Flag to track if the intro animation is done // Face tracking variables for smooth movement var targetX = GAME_WIDTH / 2; var targetY = GAME_HEIGHT / 3; var smoothingFactor = 0.12; var prevX = null; var prevY = null; // Rotation tracking variables var targetTilt = 0; var tiltSmoothingFactor = 0.11; var tiltScaleFactor = 0.05; // Reduced for mask subtlety // Scale tracking variables var scaleHistory = new Array(5).fill(1); // Start with default scale of 1 var scaleIndex = 0; // Function to calculate face tilt (adapted from face tracking example) function calculateFaceTilt() { if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) { // Calculate midpoint between eyes var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2; var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2; // Calculate angle between eye midpoint and mouth, negated to fix direction var dx = facekit.mouthCenter.x - eyeMidX; var dy = facekit.mouthCenter.y - eyeMidY; var angle = -(Math.atan2(dx, dy) * (180 / Math.PI)); // Reduced angle impact for mask return Math.max(-10, Math.min(10, angle * 0.1)); } return 0; // Default to straight when face points aren't available } self.show = function (targetX, targetY, duration) { self.x = targetX; // Start off-screen (above the visible area) // Considering anchorY is 0.5, initial Y should be -half height relative to screen top self.y = -self.maskGraphics.height / 2 - 50; // 50 pixels buffer to be surely off-screen tween(self, { y: targetY }, { duration: duration, easing: tween.easeOutSine, // A smooth easing function onFinish: function onFinish() { // Optional: anything to do after mask finishes animating self.animationFinished = true; // Mark animation as finished } }); }; // New smooth update method for face tracking self.updateFaceTracking = function() { if (!self.animationFinished) { return; // Don't track until animation is finished } // Smooth position tracking if (facekit.noseTip && facekit.noseTip.x !== 0) { targetX = facekit.noseTip.x; var maskGraphicHeight = self.maskGraphics.height; var yOffset = -50; // Keep the existing Y offset targetY = facekit.noseTip.y - maskGraphicHeight * 0.20 + yOffset; // Initialize previous positions if not set if (prevX === null) { prevX = targetX; prevY = targetY; } // Weighted average between previous and target position for smooth movement var newX = prevX * (1 - smoothingFactor) + targetX * smoothingFactor; var newY = prevY * (1 - smoothingFactor) + targetY * smoothingFactor; self.x = newX; self.y = newY; // Update previous positions prevX = newX; prevY = newY; } else { // Fallback positioning if face data is lost if (prevX !== null) { self.x = prevX; self.y = prevY; } } // Rotation tracking if (facekit.leftEye && facekit.rightEye) { targetTilt = calculateFaceTilt() * tiltScaleFactor; // Limit rotation to ±10 degrees for mask targetTilt = Math.max(-10, Math.min(10, targetTilt)); // Convert to radians for rotation var targetTiltRad = targetTilt * (Math.PI / 180); self.rotation += (targetTiltRad - self.rotation) * tiltSmoothingFactor; } // Scale tracking if (facekit.leftEye && facekit.rightEye) { var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x); var newScale = eyeDistance / 300; // Adjusted divisor for mask // Update rolling average scaleHistory[scaleIndex] = newScale; scaleIndex = (scaleIndex + 1) % scaleHistory.length; // Calculate average scale var avgScale = scaleHistory.reduce(function (a, b) { return a + b; }, 0) / scaleHistory.length; // Apply with gentle smoothing (limited range for mask) var targetScale = Math.max(0.8, Math.min(1.3, avgScale)); self.maskGraphics.scaleX = self.maskGraphics.scaleX * 0.85 + targetScale * 0.15; self.maskGraphics.scaleY = self.maskGraphics.scaleY * 0.85 + targetScale * 0.15; } }; return self; }); var MouthAnimator = Container.expand(function () { var self = Container.call(this); // Visemes array for easier management self.visemes = [self.attachAsset('mouthVisemeClosed', { anchorX: 0.5, anchorY: 0.5, visible: false }), self.attachAsset('mouthVisemeOpenS', { anchorX: 0.5, anchorY: 0.5, visible: false }), self.attachAsset('mouthVisemeOpenM', { anchorX: 0.5, anchorY: 0.5, visible: false }), self.attachAsset('mouthVisemeOpenL', { anchorX: 0.5, anchorY: 0.5, visible: false })]; self.currentVisemeIndex = 0; if (self.visemes.length > 0) { self.visemes[self.currentVisemeIndex].visible = true; } // Smooth mouth tracking variables (adapted from face tracking example) self.mouthOpenness = 0; // Track mouth state from 0 (closed) to 1 (open) self.mouthSmoothingSpeed = 0.15; // Adjust this value to change transition speed // Position tracking variables for smooth movement var targetX = GAME_WIDTH / 2; var targetY = GAME_HEIGHT / 2 + 200; var positionSmoothingFactor = 0.12; var prevX = null; var prevY = null; // Scale tracking variables var scaleHistory = new Array(5).fill(1); var scaleIndex = 0; self.updateViseme = function (volume) { // Use face tracking for mouth state instead of volume var targetMouthState = 0; if (facekit && facekit.mouthOpen) { targetMouthState = 1; } // Smoothly transition mouth state self.mouthOpenness += (targetMouthState - self.mouthOpenness) * self.mouthSmoothingSpeed; // Convert smooth mouth openness to viseme index (0-3) var newIndex = Math.floor(self.mouthOpenness * 3.99); // Ensures we stay in 0-3 range newIndex = Math.max(0, Math.min(3, newIndex)); // Clamp to valid range // Update viseme visibility if changed if (self.currentVisemeIndex !== newIndex) { self.visemes[self.currentVisemeIndex].visible = false; if (newIndex >= 0 && newIndex < self.visemes.length) { self.visemes[newIndex].visible = true; self.currentVisemeIndex = newIndex; } } }; // New smooth update method for position and scale tracking self.updateFaceTracking = function() { // Smooth position tracking if (facekit.mouthCenter && (facekit.mouthCenter.x !== 0 || facekit.mouthCenter.y !== 0)) { targetX = facekit.mouthCenter.x; targetY = facekit.mouthCenter.y; // Initialize previous positions if not set if (prevX === null) { prevX = targetX; prevY = targetY; } // Weighted average between previous and target position for smooth movement var newX = prevX * (1 - positionSmoothingFactor) + targetX * positionSmoothingFactor; var newY = prevY * (1 - positionSmoothingFactor) + targetY * positionSmoothingFactor; self.x = newX; self.y = newY; // Update previous positions prevX = newX; prevY = newY; } else { // Fallback if face data is lost mid-game if (prevX !== null) { self.x = prevX; self.y = prevY; } else { self.x = GAME_WIDTH / 2; self.y = GAME_HEIGHT / 2 + 200; } } // Scale tracking based on face size if (facekit.leftEye && facekit.rightEye) { var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x); var newScale = eyeDistance / 250; // Adjusted divisor for mouth // Update rolling average scaleHistory[scaleIndex] = newScale; scaleIndex = (scaleIndex + 1) % scaleHistory.length; // Calculate average scale var avgScale = scaleHistory.reduce(function (a, b) { return a + b; }, 0) / scaleHistory.length; // Apply with gentle smoothing (limited range) var targetScale = Math.max(0.7, Math.min(1.4, avgScale)); // Apply scale to all visemes for (var i = 0; i < self.visemes.length; i++) { self.visemes[i].scaleX = self.visemes[i].scaleX * 0.85 + targetScale * 0.15; self.visemes[i].scaleY = self.visemes[i].scaleY * 0.85 + targetScale * 0.15; } } // Update viseme based on face tracking (call this after position/scale updates) self.updateViseme(facekit.volume); }; return self; }); var StartButton = Container.expand(function () { var self = Container.call(this); self.buttonGraphics = self.attachAsset('startButtonImage', { anchorX: 0.5, anchorY: 0.5 }); self.down = function () { // This will call the globally defined startGame function // when the button is pressed. if (typeof handleStartButtonPressed === 'function') { handleStartButtonPressed(); } }; return self; }); /**** * Initialize Game ****/ // Facekit provides the camera feed as background, so no explicit backgroundColor needed. var game = new LK.Game({}); /**** * Game Code ****/ // Game state variables var isGameActive = false; var startButtonInstance; var maskInstance; var mouthAnimatorInstance; // Game constants var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; function handleStartButtonPressed() { if (isGameActive) { return; } // Game already started isGameActive = true; // Remove start button if (startButtonInstance && startButtonInstance.parent) { startButtonInstance.destroy(); startButtonInstance = null; } // Create and show mask maskInstance = new Mask(); game.addChild(maskInstance); // Target X for mask: use noseTip.x if available, otherwise screen center var maskTargetX = facekit.noseTip && facekit.noseTip.x !== 0 ? facekit.noseTip.x : GAME_WIDTH / 2; // Target Y for mask: position it over the upper face. // Anchor is center (0.5,0.5). If mask height is H, noseTip.y - H*0.2 might work. // This positions the center of the mask slightly above the nose tip. var maskGraphicHeight = maskInstance.maskGraphics.height; // actual height of the mask graphic var yOffset = -50; // Negative Y offset var maskTargetY = facekit.noseTip && facekit.noseTip.y !== 0 ? facekit.noseTip.y - maskGraphicHeight * 0.20 + yOffset : GAME_HEIGHT / 3 + yOffset; maskInstance.show(maskTargetX, maskTargetY, 1200); // 1.2 seconds animation duration // Create mouth animator mouthAnimatorInstance = new MouthAnimator(); // Initial position, will be updated continuously in game.update var mouthInitialX = facekit.mouthCenter && facekit.mouthCenter.x !== 0 ? facekit.mouthCenter.x : GAME_WIDTH / 2; var mouthInitialY = facekit.mouthCenter && facekit.mouthCenter.y !== 0 ? facekit.mouthCenter.y : GAME_HEIGHT / 2 + 200; mouthAnimatorInstance.x = mouthInitialX; mouthAnimatorInstance.y = mouthInitialY; game.addChild(mouthAnimatorInstance); // Play music LK.playMusic('karaokeSong'); } // Initial game setup function initializeGameScreen() { isGameActive = false; // Reset game state // Create and position the start button startButtonInstance = new StartButton(); // Center the button horizontally startButtonInstance.x = GAME_WIDTH / 2; // Position it towards the bottom of the screen // Anchor is 0.5, 0.5, so take half of button height into account for precise padding from bottom. var buttonHeight = startButtonInstance.buttonGraphics.height; startButtonInstance.y = GAME_HEIGHT - buttonHeight / 2 - 100; // 100px padding from bottom game.addChild(startButtonInstance); } // Call initial setup initializeGameScreen(); // Game update loop game.update = function () { if (isGameActive) { // Update mouth animator with enhanced face tracking if (mouthAnimatorInstance) { mouthAnimatorInstance.updateFaceTracking(); } // Update mask with enhanced face tracking if (maskInstance) { maskInstance.updateFaceTracking(); } } }; // LK engine handles game pause, game over, etc. // No explicit win/loss conditions in this game's description.
Code edit (2 edits merged)
Please save this source code
User prompt
Dynamically change mask scale depending on size of face.
User prompt
Add a negative Y offset of 50 pixels to the mask tracking.
User prompt
Update the players movement continuously as the mask is lowering to make sure it lands on the players upper face smoothly and doesn’t just warp over.
User prompt
When the button is pressed begin to track the mouth right away, but animate the mask smoothly in to place on the players upper face before starting the full tracking. ↪💡 Consider importing and using the following plugins: @upit/facekit.v1, @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
User prompt
FaceKit Karaoke Mask
Initial prompt
Use FaceKit. When the player presses on an on screen button in the lower center screen that says “Press Me”, the button disappears and a mask lowers from off screen top and over the players upper face. Then through the use of an overlay of mouth animation visemes, the player is made to appear to sing a song.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var facekit = LK.import("@upit/facekit.v1"); /**** * Classes ****/ var Mask = Container.expand(function () { var self = Container.call(this); // Store maskGraphics on self to access its properties like height if needed later self.maskGraphics = self.attachAsset('maskImage', { anchorX: 0.5, anchorY: 0.4 }); self.animationFinished = false; // Flag to track if the intro animation is done var targetX = GAME_WIDTH / 2; var targetY = GAME_HEIGHT / 3; var smoothingFactor = 0.15; // Higher value for more responsive tracking var prevX = null; var prevY = null; // Scale tracking variables (from dragon example) var scaleHistory = new Array(5).fill(1); // Start with default scale of 1 var scaleIndex = 0; var currentScale = 1; // Rotation variables - EXACTLY from working example var targetTilt = 0; var tiltSmoothingFactor = 0.11; var tiltScaleFactor = 0.09; self.show = function (targetX, targetY, duration) { self.x = targetX; self.y = -self.maskGraphics.height / 2 - 50; tween(self, { y: targetY }, { duration: duration, easing: tween.easeOutSine, onFinish: function onFinish() { self.animationFinished = true; } }); }; // Function copied EXACTLY from working DragonHead example function calculateFaceTilt() { if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) { // Calculate midpoint between eyes var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2; var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2; // Calculate angle between eye midpoint and mouth, negated to fix direction var dx = facekit.mouthCenter.x - eyeMidX; var dy = facekit.mouthCenter.y - eyeMidY; var angle = -(Math.atan2(dx, dy) * (180 / Math.PI)); // Reduced angle impact return Math.max(-15, Math.min(15, angle * 0.15)); } return 0; // Default to straight when face points aren't available } // Enhanced update method with smooth position tracking and dynamic scaling self.update = function () { if (!self.animationFinished) { return; } // Smooth position tracking (from dragon example) if (facekit.leftEye && facekit.rightEye) { // --- SCALE CALCULATION --- var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x); var newScaleFactor = eyeDistance / 300; // Base scale factor from eye distance scaleHistory[scaleIndex] = newScaleFactor; scaleIndex = (scaleIndex + 1) % scaleHistory.length; var avgScale = scaleHistory.reduce(function (a, b) { return a + b; }, 0) / scaleHistory.length; var targetScaleValue = avgScale; // Smoothed target scale currentScale = currentScale * 0.85 + targetScaleValue * 0.15; // Final smoothed currentScale // --- POSITION CALCULATION WITH Y-COMPENSATION --- var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2; var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2; targetX = eyeMidX; // Target X is the midpoint between eyes // Base target Y positions the anchor (0.4 from top of graphic) relative to eyes var baseTargetY = eyeMidY - self.maskGraphics.height * 0.20 - 50; // Y-compensation: lower the mask when below standard scale (< 1.0), raise it slightly when above (> 1.0) var yOffsetCompensation = 0; if (currentScale < 1) { yOffsetCompensation = self.maskGraphics.height * 0.4 * (1 - currentScale); } else if (currentScale > 1) { // Raise mask slightly as it grows above base scale (gentle effect) yOffsetCompensation = -self.maskGraphics.height * 0.08 * (currentScale - 1); } targetY = baseTargetY + yOffsetCompensation; // Final targetY including compensation // Initialize previous positions if not set if (prevX === null) { prevX = targetX; prevY = targetY; } // Weighted average for smooth tracking var newX = prevX * (1 - smoothingFactor) + targetX * smoothingFactor; var newY = prevY * (1 - smoothingFactor) + targetY * smoothingFactor; self.x = newX; self.y = newY; // Update previous positions prevX = newX; prevY = newY; // --- APPLY SCALE TO MASK GRAPHICS --- self.maskGraphics.scaleX = currentScale; self.maskGraphics.scaleY = currentScale; } // Rotation tracking - COPIED EXACTLY from working example if (facekit.leftEye && facekit.rightEye) { targetTilt = calculateFaceTilt() * tiltScaleFactor; // Limit rotation to ±15 degrees - DON'T convert to radians here targetTilt = Math.max(-15, Math.min(15, targetTilt)); self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor; } }; return self; }); var MouthAnimator = Container.expand(function () { var self = Container.call(this); // Load all viseme assets (keeping your existing setup) self.visemes = { closed: self.attachAsset('visemeClosed', { anchorX: 0.5, anchorY: 0.5, visible: false }), AEI: self.attachAsset('visemeAEI', { anchorX: 0.5, anchorY: 0.5, visible: false }), BMP: self.attachAsset('visemeBMP', { anchorX: 0.5, anchorY: 0.5, visible: false }), CDGKNRSTXYZ: self.attachAsset('visemeCDGKNRSTXYZ', { anchorX: 0.5, anchorY: 0.5, visible: false }), CHJSH: self.attachAsset('visemeCHJSH', { anchorX: 0.5, anchorY: 0.5, visible: false }), EE: self.attachAsset('visemeEE', { anchorX: 0.5, anchorY: 0.5, visible: false }), FV: self.attachAsset('visemeFV', { anchorX: 0.5, anchorY: 0.5, visible: false }), L: self.attachAsset('visemeL', { anchorX: 0.5, anchorY: 0.5, visible: false }), N: self.attachAsset('visemeN', { anchorX: 0.5, anchorY: 0.5, visible: false }), O: self.attachAsset('visemeO', { anchorX: 0.5, anchorY: 0.5, visible: false }), TH: self.attachAsset('visemeTH', { anchorX: 0.5, anchorY: 0.5, visible: false }), U: self.attachAsset('visemeU', { anchorX: 0.5, anchorY: 0.5, visible: false }), QW: self.attachAsset('visimeQW', { anchorX: 0.5, anchorY: 0.5, visible: false }) }; self.currentViseme = 'closed'; self.visemes.closed.visible = true; // Automated lip sync data (converted from your JSON) self.automatedLipSync = [{ start: 0.00, end: 5.03, value: "X" }, { start: 5.03, end: 5.16, value: "B" }, { start: 5.16, end: 5.23, value: "E" }, { start: 5.23, end: 5.30, value: "F" }, { start: 5.30, end: 5.40, value: "A" }, { start: 5.40, end: 5.54, value: "C" }, { start: 5.54, end: 5.61, value: "B" }, { start: 5.61, end: 5.82, value: "C" }, { start: 5.82, end: 5.96, value: "B" }, { start: 5.96, end: 6.03, value: "C" }, { start: 6.03, end: 6.31, value: "B" }, { start: 6.31, end: 6.52, value: "F" }, { start: 6.52, end: 6.63, value: "G" }, { start: 6.63, end: 7.43, value: "E" }, { start: 7.43, end: 9.85, value: "X" }, { start: 9.85, end: 10.03, value: "E" }, { start: 10.03, end: 10.24, value: "C" }, { start: 10.24, end: 10.38, value: "A" }, { start: 10.38, end: 10.52, value: "F" }, { start: 10.52, end: 10.59, value: "D" }, { start: 10.59, end: 11.15, value: "C" }, { start: 11.15, end: 11.23, value: "A" }, { start: 11.23, end: 11.54, value: "D" }, { start: 11.54, end: 11.62, value: "C" }, { start: 11.62, end: 12.77, value: "G" }, { start: 12.77, end: 13.77, value: "X" }, { start: 13.77, end: 13.83, value: "N" }, { start: 13.83, end: 14.44, value: "O" }, { start: 14.44, end: 14.79, value: "C" }, { start: 14.79, end: 15.01, value: "F" }, { start: 15.01, end: 15.09, value: "A" }, { start: 15.09, end: 15.13, value: "B" }, { start: 15.13, end: 15.38, value: "F" }, { start: 15.38, end: 15.45, value: "B" }, { start: 15.45, end: 16.08, value: "E" }, { start: 16.08, end: 16.29, value: "B" }, { start: 16.29, end: 17.16, value: "X" }, { start: 17.16, end: 17.23, value: "C" }, { start: 17.23, end: 17.29, value: "B" }, { start: 17.29, end: 17.79, value: "N" }, { start: 17.79, end: 18.99, value: 'E' }, { start: 18.99, end: 19.53, value: "G" }, { start: 19.53, end: 22.08, value: "X" }, { start: 22.08, end: 22.42, value: "E" }, { start: 22.42, end: 22.77, value: "B" }, { start: 22.77, end: 22.85, value: "A" }, { start: 22.85, end: 23.02, value: "N" }, { start: 23.02, end: 23.30, value: "C" }, { start: 23.30, end: 23.65, value: "C" }, { start: 23.65, end: 23.86, value: "B" }, { start: 23.86, end: 24.49, value: "E" }, { start: 24.49, end: 24.91, value: "L" }, { start: 24.91, end: 24.98, value: "E" }, { start: 24.98, end: 25.05, value: "N" }, { start: 25.05, end: 25.33, value: "E" }, { start: 25.33, end: 25.75, value: "B" }, { start: 25.75, end: 25.82, value: "D" }, { start: 25.82, end: 25.96, value: "B" }, { start: 25.96, end: 26.17, value: "F" }, { start: 26.17, end: 26.45, value: "E" }, { start: 26.45, end: 27.08, value: "B" }, { start: 27.08, end: 27.43, value: "E" }, { start: 27.43, end: 27.57, value: "F" }, { start: 27.57, end: 27.71, value: "C" }, { start: 27.71, end: 28.48, value: "O" }, { start: 28.48, end: 32.70, value: "X" }, { start: 32.70, end: 32.82, value: "B" }, { start: 32.82, end: 33.03, value: "O" }, { start: 33.03, end: 33.45, value: "E" }, { start: 33.45, end: 33.66, value: "E" }, { start: 33.66, end: 33.73, value: "C" }, { start: 33.73, end: 33.87, value: "B" }, { start: 33.87, end: 34.01, value: "E" }, { start: 34.01, end: 34.29, value: "C" }, { start: 34.29, end: 34.36, value: "B" }, { start: 34.36, end: 34.50, value: "E" }, { start: 34.50, end: 34.71, value: "B" }, { start: 34.71, end: 34.85, value: "E" }, { start: 34.85, end: 35.13, value: "C" }, { start: 35.13, end: 35.20, value: "C" }, { start: 35.20, end: 35.48, value: "B" }, { start: 35.48, end: 35.55, value: "C" }, { start: 35.55, end: 35.76, value: "E" }, { start: 35.76, end: 35.90, value: "F" }, { start: 35.90, end: 36.60, value: "B" }, { start: 36.60, end: 36.71, value: "A" }, { start: 36.71, end: 36.89, value: "O" }, { start: 36.89, end: 36.94, value: "N" }, { start: 36.94, end: 37.47, value: "B" }, { start: 37.47, end: 37.55, value: "A" }, { start: 37.55, end: 37.65, value: "E" }, { start: 37.65, end: 37.72, value: "B" }, { start: 37.72, end: 37.92, value: "F" }, { start: 37.92, end: 38.00, value: "E" }, { start: 38.00, end: 40.10, value: "O" }, { start: 40.10, end: 43.42, value: "X" }, { start: 43.42, end: 43.67, value: "C" }, { start: 43.67, end: 43.74, value: "O" }, { start: 43.74, end: 43.88, value: "F" }, { start: 43.88, end: 44.23, value: "A" }, { start: 44.23, end: 44.44, value: "C" }, { start: 44.44, end: 44.58, value: "B" }, { start: 44.58, end: 44.79, value: "C" }, { start: 44.79, end: 45.07, value: "O" }, { start: 45.07, end: 45.35, value: "L" }, { start: 45.35, end: 45.63, value: "C" }, { start: 45.63, end: 45.70, value: "B" }, { start: 45.70, end: 46.12, value: "E" }, { start: 46.12, end: 46.40, value: "E" }, { start: 46.40, end: 46.45, value: "B" }, { start: 46.45, end: 47.13, value: "F" }, { start: 47.13, end: 47.27, value: "C" }, { start: 47.27, end: 47.34, value: "B" }, { start: 47.34, end: 47.55, value: "E" }, { start: 47.55, end: 47.76, value: "C" }, { start: 47.76, end: 47.83, value: "B" }, { start: 47.83, end: 48.04, value: "D" }, { start: 48.04, end: 48.11, value: "B" }, { start: 48.11, end: 48.81, value: "F" }, { start: 48.81, end: 48.88, value: "A" }, { start: 48.88, end: 48.95, value: "L" }, { start: 48.95, end: 49.02, value: "E" }, { start: 49.02, end: 50.28, value: "B" }, { start: 50.28, end: 50.61, value: "A" }, { start: 50.61, end: 53.74, value: "X" }, { start: 53.74, end: 54.23, value: "C" }, { start: 54.23, end: 54.31, value: "A" }, { start: 54.31, end: 54.51, value: "B" }, { start: 54.51, end: 54.79, value: "E" }, { start: 54.79, end: 54.90, value: "A" }, { start: 54.90, end: 55.01, value: "B" }, { start: 55.01, end: 55.12, value: "A" }, { start: 55.12, end: 55.18, value: "C" }, { start: 55.18, end: 55.52, value: "B" }, { start: 55.52, end: 55.59, value: "C" }, { start: 55.59, end: 55.87, value: "B" }, { start: 55.87, end: 56.22, value: "F" }, { start: 56.22, end: 56.36, value: "B" }, { start: 56.36, end: 56.66, value: "F" }, { start: 56.66, end: 56.74, value: "E" }, { start: 56.74, end: 56.89, value: "C" }, { start: 56.89, end: 57.17, value: "A" }, { start: 57.17, end: 57.69, value: "N" }, { start: 57.69, end: 57.92, value: "C" }, { start: 57.92, end: 58.20, value: "B" }, { start: 58.20, end: 59.04, value: "F" }, { start: 59.04, end: 59.18, value: "B" }, { start: 59.18, end: 59.39, value: "E" }, { start: 59.39, end: 59.53, value: "C" }, { start: 59.53, end: 59.98, value: "O" }, { start: 59.98, end: 60.06, value: "N" }, { start: 60.06, end: 60.37, value: "B" }, { start: 60.37, end: 60.51, value: "C" }, { start: 60.51, end: 60.65, value: "E" }, { start: 60.65, end: 62.19, value: "N" }, { start: 62.19, end: 62.33, value: "X" }, { start: 62.33, end: 62.55, value: "B" }, { start: 62.55, end: 63.24, value: "X" }, { start: 63.24, end: 63.38, value: "B" }, { start: 63.38, end: 63.73, value: "E" }, { start: 63.73, end: 63.87, value: "C" }, { start: 63.87, end: 63.99, value: "A" }, { start: 63.99, end: 64.11, value: "C" }, { start: 64.11, end: 64.46, value: "B" }, { start: 64.46, end: 64.60, value: "F" }, { start: 64.60, end: 64.67, value: "B" }, { start: 64.67, end: 64.88, value: "F" }, { start: 64.88, end: 64.95, value: "C" }, { start: 64.95, end: 65.88, value: "B" }, { start: 65.88, end: 66.43, value: "X" }, { start: 66.43, end: 66.62, value: "D" }, { start: 66.62, end: 66.81, value: "C" }, { start: 66.81, end: 66.86, value: "E" }, { start: 66.86, end: 67.11, value: "C" }, { start: 67.11, end: 68.93, value: "B" }, { start: 68.93, end: 69.14, value: "X" }, { start: 69.14, end: 70.99, value: "A" }, { start: 70.99, end: 71.13, value: "F" }, { start: 71.13, end: 72.63, value: "A" }, { start: 72.63, end: 72.71, value: "C" }, { start: 72.71, end: 73.16, value: "B" }, { start: 73.16, end: 75.30, value: "X" }, { start: 75.30, end: 75.57, value: "B" }, { start: 75.57, end: 75.71, value: "E" }, { start: 75.71, end: 75.99, value: "B" }, { start: 75.99, end: 76.27, value: "C" }, { start: 76.27, end: 76.34, value: "B" }, { start: 76.34, end: 76.83, value: "F" }, { start: 76.83, end: 77.04, value: "E" }, { start: 77.04, end: 77.11, value: "C" }, { start: 77.11, end: 77.18, value: "H" }, { start: 77.18, end: 77.37, value: "E" }, { start: 77.37, end: 77.42, value: "C" }, { start: 77.42, end: 77.81, value: "B" }, { start: 77.81, end: 79.14, value: "F" }, { start: 79.14, end: 79.48, value: "B" }, { start: 79.48, end: 80.18, value: "E" }, { start: 80.18, end: 80.46, value: "C" }, { start: 80.46, end: 83.12, value: "B" }, { start: 83.12, end: 84.24, value: "E" }, { start: 84.24, end: 84.45, value: "B" }, { start: 84.45, end: 85.37, value: "X" }, { start: 85.37, end: 85.55, value: "F" }, { start: 85.55, end: 86.08, value: "B" }, { start: 86.08, end: 86.62, value: "F" }, { start: 86.62, end: 86.67, value: "C" }, { start: 86.67, end: 87.00, value: "E" }, { start: 87.00, end: 87.28, value: "F" }, { start: 87.28, end: 87.42, value: "D" }, { start: 87.42, end: 87.70, value: "C" }, { start: 87.70, end: 88.54, value: "B" }, { start: 88.54, end: 89.42, value: "D" }, { start: 89.42, end: 89.49, value: "C" }, { start: 89.49, end: 89.73, value: "B" }, { start: 89.73, end: 90.46, value: "X" }, { start: 90.46, end: 90.81, value: "E" }, { start: 90.81, end: 91.27, value: "F" }, { start: 91.27, end: 91.34, value: "E" }, { start: 91.34, end: 91.41, value: "F" }, { start: 91.41, end: 92.11, value: "B" }, { start: 92.11, end: 93.07, value: "X" }, { start: 93.07, end: 93.33, value: "C" }, { start: 93.33, end: 93.40, value: "E" }, { start: 93.40, end: 93.78, value: "F" }, { start: 93.78, end: 93.85, value: "B" }, { start: 93.85, end: 94.16, value: "D" }, { start: 94.16, end: 94.24, value: "C" }, { start: 94.24, end: 95.04, value: "B" }, { start: 95.04, end: 95.79, value: "X" }, { start: 95.79, end: 95.89, value: "C" }, { start: 95.89, end: 95.96, value: "B" }, { start: 95.96, end: 96.05, value: "A" }, { start: 96.05, end: 96.31, value: "C" }, { start: 96.31, end: 96.49, value: "B" }, { start: 96.49, end: 97.18, value: "D" }, { start: 97.18, end: 97.26, value: "C" }, { start: 97.26, end: 98.43, value: "X" }, { start: 98.43, end: 98.54, value: "E" }, { start: 98.54, end: 98.61, value: "F" }, { start: 98.61, end: 98.68, value: "C" }, { start: 98.68, end: 98.75, value: "B" }, { start: 98.75, end: 98.82, value: "F" }, { start: 98.82, end: 99.10, value: "C" }, { start: 99.10, end: 99.18, value: "A" }, { start: 99.18, end: 99.64, value: "C" }, { start: 99.64, end: 99.85, value: "B" }, { start: 99.85, end: 101.13, value: "X" }, { start: 101.13, end: 101.26, value: "D" }, { start: 101.26, end: 101.33, value: "B" }, { start: 101.33, end: 101.45, value: "A" }, { start: 101.45, end: 101.86, value: "C" }, { start: 101.86, end: 102.38, value: "B" }, { start: 102.38, end: 102.46, value: "A" }, { start: 102.46, end: 103.19, value: "N" }, { start: 103.19, end: 103.47, value: "B" }, { start: 103.47, end: 104.52, value: "A" }, { start: 104.52, end: 105.15, value: "C" }, { start: 105.15, end: 105.57, value: "B" }, { start: 105.57, end: 108.41, value: "X" }, { start: 108.41, end: 108.56, value: "A" }, { start: 108.56, end: 108.63, value: "C" }, { start: 108.63, end: 108.84, value: "B" }, { start: 108.84, end: 108.98, value: "A" }, { start: 108.98, end: 109.05, value: "L" }, { start: 109.05, end: 109.33, value: "A" }, { start: 109.33, end: 109.68, value: "B" }, { start: 109.68, end: 109.82, value: "E" }, { start: 109.82, end: 109.89, value: "B" }, { start: 109.89, end: 110.03, value: "O" }, { start: 110.03, end: 110.17, value: "F" }, { start: 110.17, end: 110.24, value: "B" }, { start: 110.24, end: 110.52, value: "A" }, { start: 110.52, end: 110.59, value: "D" }, { start: 110.59, end: 111.01, value: "N" }, { start: 111.01, end: 111.08, value: "E" }, { start: 111.08, end: 111.57, value: "C" }, { start: 111.57, end: 111.64, value: "B" }, { start: 111.64, end: 111.99, value: "E" }, { start: 111.99, end: 113.77, value: "X" }, { start: 113.77, end: 114.16, value: "B" }, { start: 114.16, end: 114.65, value: "E" }, { start: 114.65, end: 114.93, value: "C" }, { start: 114.93, end: 115.00, value: "C" }, { start: 115.00, end: 115.24, value: "A" }, { start: 115.24, end: 115.46, value: "N" }, { start: 115.46, end: 115.60, value: "C" }, { start: 115.60, end: 115.74, value: "B" }, { start: 115.74, end: 115.88, value: "E" }, { start: 115.88, end: 116.09, value: "A" }, { start: 116.09, end: 116.17, value: "B" }, { start: 116.17, end: 116.24, value: "E" }, { start: 116.24, end: 116.73, value: "B" }, { start: 116.73, end: 117.57, value: "X" }, { start: 117.57, end: 117.80, value: "B" }, { start: 117.80, end: 118.81, value: "X" }, { start: 118.81, end: 119.28, value: "B" }, { start: 119.28, end: 119.35, value: "F" }, { start: 119.35, end: 119.49, value: "E" }, { start: 119.49, end: 119.65, value: "A" }, { start: 119.65, end: 119.88, value: "C" }, { start: 119.88, end: 119.95, value: "E" }, { start: 119.95, end: 120.16, value: "B" }, { start: 120.16, end: 120.30, value: "E" }, { start: 120.30, end: 120.37, value: "B" }, { start: 120.37, end: 120.65, value: "E" }, { start: 120.65, end: 120.79, value: "F" }, { start: 120.79, end: 120.93, value: "B" }, { start: 120.93, end: 121.00, value: "E" }, { start: 121.00, end: 121.28, value: "L" }, { start: 121.28, end: 121.49, value: "O" }, { start: 121.49, end: 121.56, value: "F" }, { start: 121.56, end: 121.70, value: "B" }, { start: 121.70, end: 121.98, value: "A" }, { start: 121.98, end: 122.19, value: "E" }, { start: 122.19, end: 122.61, value: "C" }, { start: 122.61, end: 123.56, value: "B" }, { start: 123.56, end: 123.77, value: "X" }, { start: 123.77, end: 123.87, value: "B" }, { start: 123.87, end: 124.02, value: "A" }, { start: 124.02, end: 124.16, value: "L" }, { start: 124.16, end: 124.30, value: "F" }, { start: 124.30, end: 124.72, value: "B" }, { start: 124.72, end: 125.00, value: "E" }, { start: 125.00, end: 125.99, value: "D" }, { start: 125.99, end: 126.06, value: "C" }, { start: 126.06, end: 126.34, value: "I" }, { start: 126.34, end: 126.48, value: "B" }, { start: 126.48, end: 126.62, value: "E" }, { start: 126.62, end: 126.69, value: "F" }, { start: 126.69, end: 126.83, value: "B" }, { start: 126.83, end: 128.26, value: "A" }, { start: 128.26, end: 128.43, value: "C" }, { start: 128.43, end: 129.70, value: "X" }, { start: 129.70, end: 129.85, value: "A" }, { start: 129.85, end: 129.96, value: "L" }, { start: 129.96, end: 130.03, value: "F" }, { start: 130.03, end: 130.17, value: "B" }, { start: 130.17, end: 130.38, value: "E" }, { start: 130.38, end: 130.52, value: "D" }, { start: 130.52, end: 130.59, value: "B" }, { start: 130.59, end: 130.87, value: "I" }, { start: 130.87, end: 131.08, value: "B" }, { start: 131.08, end: 131.15, value: "D" }, { start: 131.15, end: 131.29, value: "O" }, { start: 131.29, end: 131.36, value: "B" }, { start: 131.36, end: 131.50, value: "B" }, { start: 131.50, end: 131.81, value: "A" }, { start: 131.81, end: 131.89, value: "C" }, { start: 131.89, end: 132.20, value: "B" }, { start: 132.20, end: 132.48, value: "E" }, { start: 132.48, end: 132.62, value: "X" }, { start: 132.62, end: 132.69, value: "B" }, { start: 132.69, end: 132.90, value: "U" }, { start: 132.90, end: 133.25, value: "B" }, { start: 133.25, end: 133.45, value: "F" }, { start: 133.45, end: 133.53, value: "E" }, { start: 133.53, end: 133.88, value: "B" }, { start: 133.88, end: 134.02, value: "U" }, { start: 134.02, end: 134.14, value: "X" }, { start: 134.14, end: 134.29, value: "B" }, { start: 134.29, end: 134.36, value: "A" }, { start: 134.36, end: 134.50, value: "B" }, { start: 134.50, end: 134.71, value: "D" }, { start: 134.71, end: 134.85, value: "E" }, { start: 134.85, end: 135.06, value: "N" }, { start: 135.06, end: 135.20, value: "C" }, { start: 135.20, end: 135.27, value: "B" }, { start: 135.27, end: 135.69, value: "A" }, { start: 135.69, end: 135.83, value: "B" }, { start: 135.83, end: 136.53, value: "E" }, { start: 136.53, end: 136.67, value: "B" }, { start: 136.67, end: 140.08, value: "X" }, { start: 140.08, end: 140.22, value: "D" }, { start: 140.22, end: 140.29, value: "B" }, { start: 140.29, end: 140.57, value: "E" }, { start: 140.57, end: 141.20, value: "F" }, { start: 141.20, end: 141.41, value: "B" }, { start: 141.41, end: 141.76, value: "A" }, { start: 141.76, end: 142.11, value: "C" }, { start: 142.11, end: 142.18, value: "N" }, { start: 142.18, end: 142.25, value: "E" }, { start: 142.25, end: 142.74, value: "C" }, { start: 142.74, end: 142.93, value: "B" }, { start: 142.93, end: 142.98, value: "A" }, { start: 142.98, end: 143.21, value: "C" }, { start: 143.21, end: 143.97, value: "D" }, { start: 143.97, end: 144.11, value: "B" }, { start: 144.11, end: 144.27, value: "X" }, { start: 144.27, end: 144.40, value: "B" }, { start: 144.40, end: 145.11, value: "X" }, { start: 145.11, end: 145.58, value: "B" }, { start: 145.58, end: 145.72, value: "F" }, { start: 145.72, end: 145.80, value: "A" }, { start: 145.80, end: 145.87, value: "C" }, { start: 145.87, end: 146.18, value: "D" }, { start: 146.18, end: 146.25, value: "C" }, { start: 146.25, end: 146.49, value: "B" }, { start: 146.49, end: 146.56, value: "A" }, { start: 146.56, end: 146.77, value: "L" }, { start: 146.77, end: 146.85, value: "A" }, { start: 146.85, end: 147.26, value: "E" }, { start: 147.26, end: 147.47, value: "B" }, { start: 147.47, end: 147.75, value: "F" }, { start: 147.75, end: 147.82, value: "E" }, { start: 147.82, end: 148.15, value: "D" }, { start: 148.15, end: 148.20, value: "C" }, { start: 148.20, end: 148.38, value: "B" }, { start: 148.38, end: 148.45, value: "F" }, { start: 148.45, end: 150.48, value: "E" }, { start: 150.48, end: 150.90, value: "B" }, { start: 150.90, end: 151.41, value: "X" }, { start: 151.41, end: 151.76, value: "E" }, { start: 151.76, end: 151.90, value: "F" }, { start: 151.90, end: 152.15, value: "A" }, { start: 152.15, end: 152.40, value: "E" }, { start: 152.40, end: 152.54, value: "F" }, { start: 152.54, end: 152.61, value: "B" }, { start: 152.61, end: 152.89, value: "E" }, { start: 152.89, end: 153.13, value: "F" }, { start: 153.13, end: 153.19, value: "C" }, { start: 153.19, end: 153.38, value: "E" }, { start: 153.38, end: 153.45, value: "F" }, { start: 153.45, end: 154.69, value: "X" }, { start: 154.69, end: 154.96, value: "C" }, { start: 154.96, end: 155.16, value: "A" }, { start: 155.16, end: 155.54, value: "F" }, { start: 155.54, end: 155.62, value: "B" }, { start: 155.62, end: 156.49, value: "E" }, { start: 156.49, end: 156.63, value: "B" }, { start: 156.63, end: 157.19, value: "E" }, { start: 157.19, end: 157.47, value: "L" }, { start: 157.47, end: 157.68, value: "O" }, { start: 157.68, end: 158.18, value: "B" }, { start: 158.18, end: 158.22, value: "B" }, { start: 158.22, end: 159.21, value: "A" }, { start: 159.21, end: 159.83, value: "E" }, { start: 159.83, end: 160.25, value: "C" }, { start: 160.25, end: 160.81, value: "B" }, { start: 160.81, end: 163.32, value: "X" }, { start: 163.32, end: 163.41, value: "B" }, { start: 163.41, end: 163.49, value: "A" }, { start: 163.49, end: 163.68, value: "L" }, { start: 163.68, end: 163.73, value: "F" }, { start: 163.73, end: 163.91, value: "B" }, { start: 163.91, end: 164.12, value: "E" }, { start: 164.12, end: 164.71, value: "D" }, { start: 164.71, end: 164.92, value: "C" }, { start: 164.92, end: 165.13, value: "I" }, { start: 165.13, end: 165.48, value: "D" }, { start: 165.48, end: 165.62, value: "O" }, { start: 165.62, end: 165.90, value: "M" }, { start: 165.90, end: 165.97, value: "C" }, { start: 165.97, end: 167.23, value: "A" }, { start: 167.23, end: 167.68, value: "X" }, { start: 167.68, end: 168.61, value: "A" }, { start: 168.61, end: 168.91, value: "L" }, { start: 168.91, end: 168.98, value: "F" }, { start: 168.98, end: 169.40, value: "B" }, { start: 169.40, end: 169.73, value: "E" }, { start: 169.73, end: 172.56, value: "D" }, { start: 172.56, end: 172.64, value: "C" }, { start: 172.64, end: 173.40, value: "X" }, { start: 173.40, end: 173.53, value: "B" }, { start: 173.53, end: 174.56, value: "U" }, { start: 174.56, end: 174.63, value: "F" }, { start: 174.63, end: 174.70, value: "G" }, { start: 174.70, end: 174.98, value: "E" }, { start: 174.98, end: 175.26, value: "H" }, { start: 175.26, end: 175.68, value: "C" }, { start: 175.68, end: 176.10, value: "B" }, { start: 176.10, end: 176.24, value: "E" }, { start: 176.24, end: 176.45, value: "F" }, { start: 176.45, end: 176.52, value: "B" }, { start: 176.52, end: 177.64, value: "O" }, { start: 177.64, end: 177.85, value: "X" }, { start: 177.85, end: 177.97, value: "C" }, { start: 177.97, end: 178.27, value: "X" }, { start: 178.27, end: 178.40, value: "B" }, { start: 178.40, end: 178.54, value: "A" }, { start: 178.54, end: 178.68, value: "R" }, { start: 178.68, end: 179.17, value: "E" }, { start: 179.17, end: 179.94, value: "B" }, { start: 179.94, end: 181.13, value: "X" }, { start: 181.13, end: 181.25, value: "D" }, { start: 181.25, end: 181.39, value: "B" }, { start: 181.39, end: 181.94, value: "E" }, { start: 181.94, end: 182.33, value: "A" }, { start: 182.33, end: 182.41, value: "B" }, { start: 182.41, end: 182.86, value: "B" }, { start: 182.86, end: 182.98, value: "X" }, { start: 182.98, end: 183.17, value: "B" }, { start: 183.17, end: 183.81, value: "X" }, { start: 183.81, end: 183.96, value: "C" }, { start: 183.96, end: 184.18, value: "A" }, { start: 184.18, end: 184.63, value: "N" }, { start: 184.63, end: 184.71, value: "A" }, { start: 184.71, end: 185.44, value: "B" }, { start: 185.44, end: 186.32, value: "X" }, { start: 186.32, end: 186.49, value: "B" }, { start: 186.49, end: 186.56, value: "A" }, { start: 186.56, end: 186.63, value: "T" }, { start: 186.63, end: 186.78, value: "B" }, { start: 186.78, end: 187.00, value: "A" }, { start: 187.00, end: 188.12, value: "N" }, { start: 188.12, end: 189.10, value: "X" }, { start: 189.10, end: 189.33, value: "N" }, { start: 189.33, end: 189.47, value: "A" }, { start: 189.47, end: 189.66, value: "N" }, { start: 189.66, end: 189.71, value: "A" }, { start: 189.71, end: 190.80, value: "N" }, { start: 190.80, end: 191.22, value: "A" }, { start: 191.22, end: 191.46, value: "N" }, { start: 191.46, end: 191.50, value: "A" }, { start: 191.50, end: 191.54, value: "N" }, { start: 191.54, end: 192.52, value: "A" }, { start: 192.52, end: 192.69, value: "N" }, { start: 192.69, end: 192.83, value: "A" }, { start: 192.83, end: 193.25, value: "B" }, { start: 193.25, end: 193.50, value: "A" }, { start: 193.50, end: 193.71, value: "T" }, { start: 193.71, end: 193.82, value: "B" }, { start: 193.82, end: 194.03, value: "A" }, { start: 194.03, end: 194.09, value: "N" }, { start: 194.09, end: 195.53, value: "B" }, { start: 195.53, end: 195.77, value: "X" }, { start: 195.77, end: 195.95, value: "N" }, { start: 195.95, end: 196.65, value: "A" }, { start: 196.65, end: 198.41, value: "N" }, { start: 198.41, end: 198.66, value: "A" }, { start: 198.66, end: 198.80, value: "N" }, { start: 198.80, end: 199.08, value: "A" }, { start: 199.08, end: 199.15, value: "N" }, { start: 199.15, end: 199.43, value: "A" }, { start: 199.43, end: 199.85, value: "N" }, { start: 199.85, end: 199.99, value: "A" }, { start: 199.99, end: 200.20, value: "N" }, { start: 200.20, end: 200.41, value: "A" }, { start: 200.41, end: 201.15, value: "B" }, { start: 201.15, end: 202.25, value: "A" }, { start: 202.25, end: 202.33, value: "T" }, { start: 202.33, end: 203.09, value: "B" }, { start: 203.09, end: 203.19, value: "A" }, { start: 203.19, end: 203.26, value: "N" }, { start: 203.26, end: 203.33, value: "B" }, { start: 203.33, end: 203.89, value: "A" }, { start: 203.89, end: 206.72, value: "X" }]; // Viseme mapping self.visemeMapping = { 'X': 'closed', 'A': 'AEI', 'B': 'BMP', 'C': 'CDGKNRSTXYZ', 'D': 'AEI', 'E': 'EE', 'F': 'FV', 'G': 'CDGKNRSTXYZ', 'H': 'TH', // Added TH viseme 'L': 'L', // Added L viseme 'N': 'N', // Added N viseme 'O': 'O', // Added O viseme 'Q': 'QW', // Added QW viseme 'U': 'U' // Added U viseme }; self.songStartTime = null; self.isLipSyncActive = false; self.currentLipSyncIndex = 0; // Position tracking variables (keeping your existing setup) var targetX = GAME_WIDTH / 2; var targetY = GAME_HEIGHT / 2 + 200; var smoothingFactor = 0.18; var prevX = null; var prevY = null; // Scale tracking variables (keeping your existing setup) var scaleHistory = new Array(5).fill(1); var scaleIndex = 0; var currentScale = 1; // Rotation variables (keeping your existing setup) var targetTilt = 0; var tiltSmoothingFactor = 0.11; var tiltScaleFactor = 0.09; function calculateFaceTilt() { if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) { var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2; var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2; var dx = facekit.mouthCenter.x - eyeMidX; var dy = facekit.mouthCenter.y - eyeMidY; var angle = -(Math.atan2(dx, dy) * (180 / Math.PI)); return Math.max(-15, Math.min(15, angle * 0.15)); } return 0; } self.startLipSync = function () { self.songStartTime = Date.now(); self.isLipSyncActive = true; self.currentLipSyncIndex = 0; }; self.setViseme = function (visemeName) { if (self.currentViseme !== visemeName && self.visemes[visemeName]) { if (self.visemes[self.currentViseme]) { self.visemes[self.currentViseme].visible = false; } self.visemes[visemeName].visible = true; self.currentViseme = visemeName; } }; self.updateAutomatedLipSync = function () { if (!self.isLipSyncActive || !self.songStartTime || self.automatedLipSync.length === 0) { return; } var currentTimeSeconds = (Date.now() - self.songStartTime) / 1000.0; var targetVisemeKey = 'closed'; // Start searching from currentLipSyncIndex for optimization for (var i = self.currentLipSyncIndex; i < self.automatedLipSync.length; i++) { var cue = self.automatedLipSync[i]; if (currentTimeSeconds >= cue.start && currentTimeSeconds < cue.end) { var automatedCode = cue.value; targetVisemeKey = self.visemeMapping[automatedCode] || 'closed'; self.currentLipSyncIndex = i; break; } if (currentTimeSeconds >= cue.end) { if (i === self.automatedLipSync.length - 1) { var automatedCode = cue.value; targetVisemeKey = self.visemeMapping[automatedCode] || 'closed'; } } else if (currentTimeSeconds < cue.start) { break; } } if (self.currentLipSyncIndex === self.automatedLipSync.length - 1 && currentTimeSeconds >= self.automatedLipSync[self.automatedLipSync.length - 1].end) { targetVisemeKey = 'closed'; } self.setViseme(targetVisemeKey); }; self.update = function () { // Update automated lip sync self.updateAutomatedLipSync(); // Smooth position tracking if (facekit.mouthCenter) { targetX = facekit.mouthCenter.x; targetY = facekit.mouthCenter.y; if (prevX === null) { prevX = targetX; prevY = targetY; } var newX = prevX * (1 - smoothingFactor) + targetX * smoothingFactor; var newY = prevY * (1 - smoothingFactor) + targetY * smoothingFactor; self.x = newX; self.y = newY; prevX = newX; prevY = newY; } // Dynamic scale adjustment if (facekit.leftEye && facekit.rightEye) { var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x); var newScale = eyeDistance / 250; scaleHistory[scaleIndex] = newScale; scaleIndex = (scaleIndex + 1) % scaleHistory.length; var avgScale = scaleHistory.reduce(function (a, b) { return a + b; }, 0) / scaleHistory.length; var targetScale = avgScale; currentScale = currentScale * 0.85 + targetScale * 0.15; Object.keys(self.visemes).forEach(function (key) { self.visemes[key].scaleX = currentScale; self.visemes[key].scaleY = currentScale; }); } // Rotation tracking if (facekit.leftEye && facekit.rightEye) { targetTilt = calculateFaceTilt() * tiltScaleFactor; targetTilt = Math.max(-15, Math.min(15, targetTilt)); self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor; } }; return self; }); var StartButton = Container.expand(function () { var self = Container.call(this); self.buttonGraphics = self.attachAsset('startButtonImage', { anchorX: 0.5, anchorY: 0.5 }); self.down = function () { // This will call the globally defined startGame function // when the button is pressed. if (typeof handleStartButtonPressed === 'function') { handleStartButtonPressed(); } }; return self; }); var SubtitleDisplay = Container.expand(function () { var self = Container.call(this); // Create text display properties self.currentText = ""; self.textDisplay = null; self.subtitleData = [{ start: 4.8, end: 7.5, text: "From the ashes of tragedy" }, { start: 10.0, end: 12.5, text: "A guardian rises" }, { start: 14.0, end: 16.0, text: "Not the hero they deserve" }, { start: 17.5, end: 19.5, text: "But the one they need" }, { start: 22.5, end: 24.5, text: "Parents fallen in an alley cold" }, { start: 25.0, end: 29.0, text: "A boy's heart turned to stone" }, { start: 33.0, end: 35.0, text: "Fortune built on others' pain" }, { start: 36.0, end: 40.0, text: "Now I claim the night as my own" }, { start: 43.5, end: 46.0, text: "Gotham calls to me in whispers dire" }, { start: 47.0, end: 51.0, text: "Criminals scatter at my arrival" }, { start: 53.5, end: 56.0, text: "The cape becomes my second skin" }, { start: 57.0, end: 62.0, text: "This mask - my true survival" }, // Chorus { start: 63.5, end: 69.0, text: "From the darkness I emerge" }, { start: 75.0, end: 79.0, text: "Justice is what I serve" }, { start: 85.5, end: 90.0, text: "When evil plagues these streets" }, { start: 90.5, end: 92.0, text: "I'll be there" }, { start: 93.5, end: 94.5, text: "I'll be there" }, { start: 96.0, end: 97.0, text: "I'm Batman" }, { start: 98.5, end: 100.0, text: "I'm Batman" }, { start: 101.5, end: 102.5, text: "I'm Batman" }, { start: 103.0, end: 105.0, text: "I'm Batman" }, // Verse 2 { start: 109.0, end: 112.5, text: "A gallery of madness I've faced" }, { start: 114.0, end: 116.5, text: "The Joker with his twisted grin" }, { start: 120.0, end: 123.8, text: "Riddler's puzzles, Penguin's schemes" }, { start: 124.0, end: 128.5, text: "Two-Face's coin, the battle within" }, { start: 130.0, end: 132.0, text: "My allies few, my burden great" }, { start: 133.0, end: 137.0, text: "The cave below, my sanctuary" }, { start: 140.5, end: 144.5, text: "Alfred's wisdom guides my path" }, { start: 145.0, end: 150.5, text: "Through this life so solitary" }, // Chorus repeat { start: 151.5, end: 157.0, text: "From the darkness I emerge" }, { start: 163.5, end: 167.5, text: "Justice is what I serve" }, { start: 173.5, end: 178.0, text: "When evil plagues these streets" }, { start: 178.5, end: 180.0, text: "I'll be there" }, { start: 181.0, end: 182.5, text: "I'll be there" }, { start: 184.0, end: 185.0, text: "I'm Batman" }, { start: 186.5, end: 188.0, text: "I'm Batman" }, { start: 189.0, end: 190.5, text: "I'm Batman" }, { start: 191.5, end: 193.0, text: "I'm Batman" }, // Outro { start: 194.0, end: 197.0, text: "Na na na na na na na na" }, { start: 197.0, end: 200.0, text: "Na na na na na na na na" }, { start: 198.0, end: 198.5, text: "Batman" }, { start: 199.0, end: 202.0, text: "Na na na na na na na na" }, { start: 202.0, end: 205.0, text: "Na na na na na na na na" }, { start: 203.0, end: 204.5, text: "Batman!" }]; self.songStartTime = null; self.isActive = false; self.currentSubtitleIndex = 0; // Text styling properties self.textStyle = { fontFamily: 'Arial', // Font family for Text2 fontSize: 96, // Font size (doubled) // fontWeight: 'bold', // fontWeight is part of font string in Text2 if specific bold font is used color: '#FFFFFF', // Fill color stroke: '#000000', // Stroke color strokeWidth: 3, // Mapped to strokeThickness in Text2 textAlign: 'center' // Mapped to align in Text2 // textBaseline: 'middle', // Handled by anchor in Text2 // shadowColor: '#000000', // Shadow not directly supported by LK Text2 // shadowBlur: 8, // shadowOffsetX: 2, // shadowOffsetY: 2 }; self.createTextGraphics = function (text) { if (self.textDisplay) { // If a previous text object exists self.textDisplay.destroy(); // Destroy it self.textDisplay = null; // Clear the reference } if (!text || text.trim() === "") { // If new text is empty, do nothing further return; } // Create new Text2 object self.textDisplay = new Text2(text, { font: self.textStyle.fontFamily, size: self.textStyle.fontSize, fill: self.textStyle.color, stroke: self.textStyle.stroke, strokeThickness: self.textStyle.strokeWidth, align: self.textStyle.textAlign }); self.textDisplay.anchor.set(0.5, 0.5); // Center the text within its bounds self.addChild(self.textDisplay); // Add the new text display to the container }; self.startSubtitles = function () { self.songStartTime = Date.now(); self.isActive = true; self.currentSubtitleIndex = 0; }; self.updateSubtitles = function () { if (!self.isActive || !self.songStartTime || self.subtitleData.length === 0) { return; } var currentTimeSeconds = (Date.now() - self.songStartTime) / 1000.0; var newText = ""; var foundSubtitle = false; // Find current subtitle // Iterate from the current index for efficiency or from start if reset needed for (var i = self.currentSubtitleIndex; i < self.subtitleData.length; i++) { var subtitle = self.subtitleData[i]; if (currentTimeSeconds >= subtitle.start && currentTimeSeconds < subtitle.end) { newText = subtitle.text; self.currentSubtitleIndex = i; // Keep track of the current subtitle index foundSubtitle = true; break; } // If current time is less than the start of this subtitle, means no future subtitle will match yet. if (currentTimeSeconds < subtitle.start) { break; } // If time has passed this subtitle's end, it might be the one if nothing else matches if (currentTimeSeconds >= subtitle.end) { // If it's the last subtitle and we've passed its start time, it might still be active until next change or song end // This logic clears the subtitle if we've passed its end time and it's not picked up by another. } } // If no subtitle is active for the current time (e.g., between cues), clear the text. if (!foundSubtitle && currentTimeSeconds > 0) { // Check currentTimeSeconds > 0 to avoid clearing at very start var shouldClear = true; // Check if we are before the first subtitle or after the last one if (self.subtitleData.length > 0) { if (currentTimeSeconds < self.subtitleData[0].start) { shouldClear = true; } else { // Check if we are between subtitles for (var k = 0; k < self.subtitleData.length; k++) { if (currentTimeSeconds >= self.subtitleData[k].start && currentTimeSeconds < self.subtitleData[k].end) { shouldClear = false; // We are in a subtitle, don't clear break; } } } } if (shouldClear) { newText = ""; } } // Update display if text changed if (newText !== self.currentText) { self.currentText = newText; self.createTextGraphics(newText); } }; self.setPosition = function (x, y) { self.x = x; self.y = y; }; self.update = function () { self.updateSubtitles(); }; return self; }); /**** * Initialize Game ****/ // Facekit provides the camera feed as background, so no explicit backgroundColor needed. var game = new LK.Game({}); /**** * Game Code ****/ // Game state variables var isGameActive = false; var startButtonInstance; var maskInstance; var mouthAnimatorInstance; var subtitleInstance; var songDuration = 206.72; // Song duration in seconds based on lip sync data var musicStartTime = null; var songCompleted = false; // Game constants var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; var AUTOMATED_VISEME_MAPPING = { 'X': 'closed', // Silence/rest position 'A': 'AEI', // Open vowels (father, palm) 'B': 'BMP', // Lip closure (b, m, p) 'C': 'CDGKNRSTXYZ', // Consonants (c, d, g, k, n, r, s, t, x, y, z) 'D': 'AEI', // Mid vowels (day, face) 'E': 'EE', // High front vowels (see, fleece) 'F': 'FV', // Lip-teeth contact (f, v) 'G': 'CDGKNRSTXYZ', // Back consonants (g, ng) 'H': 'CHJSH' // Fricatives (h, ch, j, sh) }; function handleStartButtonPressed() { if (isGameActive) { return; } isGameActive = true; // Remove start button if (startButtonInstance && startButtonInstance.parent) { startButtonInstance.destroy(); startButtonInstance = null; } // Create and show mask maskInstance = new Mask(); game.addChild(maskInstance); var maskGraphicHeight = maskInstance.maskGraphics.height; var yOffset = -50; var maskTargetX, maskTargetY; if (facekit.leftEye && facekit.rightEye && facekit.leftEye.x !== 0 && facekit.rightEye.x !== 0) { maskTargetX = (facekit.leftEye.x + facekit.rightEye.x) / 2; maskTargetY = (facekit.leftEye.y + facekit.rightEye.y) / 2 - maskGraphicHeight * 0.20 + yOffset; } else { maskTargetX = GAME_WIDTH / 2; maskTargetY = GAME_HEIGHT / 3 + yOffset; } // Create mouth animator mouthAnimatorInstance = new MouthAnimator(); var mouthInitialX = facekit.mouthCenter && facekit.mouthCenter.x !== 0 ? facekit.mouthCenter.x : GAME_WIDTH / 2; var mouthInitialY = facekit.mouthCenter && facekit.mouthCenter.y !== 0 ? facekit.mouthCenter.y : GAME_HEIGHT / 2 + 200; mouthAnimatorInstance.x = mouthInitialX; mouthAnimatorInstance.y = mouthInitialY; game.addChild(mouthAnimatorInstance); // Create subtitle display subtitleInstance = new SubtitleDisplay(); subtitleInstance.setPosition(GAME_WIDTH / 2, GAME_HEIGHT - 124 - GAME_HEIGHT * 0.1); // Position moved up 10% from previous position game.addChild(subtitleInstance); // Show mask with callback to start music and lip-sync when animation completes maskInstance.show(maskTargetX, maskTargetY, 1200); // Start music, lip-sync, and subtitles after mask animation completes (1.2 seconds) LK.setTimeout(function () { LK.playMusic('batmansong'); musicStartTime = Date.now(); // Track when music started songCompleted = false; // Reset completion flag if (mouthAnimatorInstance) { // Ensure mouthAnimatorInstance still exists mouthAnimatorInstance.startLipSync(); } if (subtitleInstance) { // Ensure subtitleInstance still exists subtitleInstance.startSubtitles(); } }, 1200); } // Initial game setup function initializeGameScreen() { isGameActive = false; // Reset game state // Create and position the start button startButtonInstance = new StartButton(); // Center the button horizontally startButtonInstance.x = GAME_WIDTH / 2; // Position it towards the bottom of the screen var buttonHeight = startButtonInstance.buttonGraphics.height; startButtonInstance.y = GAME_HEIGHT - buttonHeight / 2 - 100; // 100px padding from bottom game.addChild(startButtonInstance); } // Call initial setup initializeGameScreen(); // Game update loop game.update = function () { if (isGameActive) { // Check if song has completed and stop music if (musicStartTime && !songCompleted) { var currentMusicTime = (Date.now() - musicStartTime) / 1000.0; if (currentMusicTime >= songDuration) { LK.stopMusic(); songCompleted = true; } } // Update mouth animator with enhanced tracking if (mouthAnimatorInstance) { // mouthAnimatorInstance.updateViseme(facekit.volume); // Viseme update is now handled by updateLipSync within MouthAnimator's update mouthAnimatorInstance.update(); // This now includes smooth position tracking, scaling, and lip-sync } // Update mask with enhanced tracking if (maskInstance) { maskInstance.update(); // This now includes smooth position tracking and scaling } if (subtitleInstance) { subtitleInstance.update(); } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
var Mask = Container.expand(function () {
var self = Container.call(this);
// Store maskGraphics on self to access its properties like height if needed later
self.maskGraphics = self.attachAsset('maskImage', {
anchorX: 0.5,
anchorY: 0.4
});
self.animationFinished = false; // Flag to track if the intro animation is done
var targetX = GAME_WIDTH / 2;
var targetY = GAME_HEIGHT / 3;
var smoothingFactor = 0.15; // Higher value for more responsive tracking
var prevX = null;
var prevY = null;
// Scale tracking variables (from dragon example)
var scaleHistory = new Array(5).fill(1); // Start with default scale of 1
var scaleIndex = 0;
var currentScale = 1;
// Rotation variables - EXACTLY from working example
var targetTilt = 0;
var tiltSmoothingFactor = 0.11;
var tiltScaleFactor = 0.09;
self.show = function (targetX, targetY, duration) {
self.x = targetX;
self.y = -self.maskGraphics.height / 2 - 50;
tween(self, {
y: targetY
}, {
duration: duration,
easing: tween.easeOutSine,
onFinish: function onFinish() {
self.animationFinished = true;
}
});
};
// Function copied EXACTLY from working DragonHead example
function calculateFaceTilt() {
if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) {
// Calculate midpoint between eyes
var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2;
var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2;
// Calculate angle between eye midpoint and mouth, negated to fix direction
var dx = facekit.mouthCenter.x - eyeMidX;
var dy = facekit.mouthCenter.y - eyeMidY;
var angle = -(Math.atan2(dx, dy) * (180 / Math.PI));
// Reduced angle impact
return Math.max(-15, Math.min(15, angle * 0.15));
}
return 0; // Default to straight when face points aren't available
}
// Enhanced update method with smooth position tracking and dynamic scaling
self.update = function () {
if (!self.animationFinished) {
return;
}
// Smooth position tracking (from dragon example)
if (facekit.leftEye && facekit.rightEye) {
// --- SCALE CALCULATION ---
var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x);
var newScaleFactor = eyeDistance / 300; // Base scale factor from eye distance
scaleHistory[scaleIndex] = newScaleFactor;
scaleIndex = (scaleIndex + 1) % scaleHistory.length;
var avgScale = scaleHistory.reduce(function (a, b) {
return a + b;
}, 0) / scaleHistory.length;
var targetScaleValue = avgScale; // Smoothed target scale
currentScale = currentScale * 0.85 + targetScaleValue * 0.15; // Final smoothed currentScale
// --- POSITION CALCULATION WITH Y-COMPENSATION ---
var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2;
var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2;
targetX = eyeMidX; // Target X is the midpoint between eyes
// Base target Y positions the anchor (0.4 from top of graphic) relative to eyes
var baseTargetY = eyeMidY - self.maskGraphics.height * 0.20 - 50;
// Y-compensation: lower the mask when below standard scale (< 1.0), raise it slightly when above (> 1.0)
var yOffsetCompensation = 0;
if (currentScale < 1) {
yOffsetCompensation = self.maskGraphics.height * 0.4 * (1 - currentScale);
} else if (currentScale > 1) {
// Raise mask slightly as it grows above base scale (gentle effect)
yOffsetCompensation = -self.maskGraphics.height * 0.08 * (currentScale - 1);
}
targetY = baseTargetY + yOffsetCompensation; // Final targetY including compensation
// Initialize previous positions if not set
if (prevX === null) {
prevX = targetX;
prevY = targetY;
}
// Weighted average for smooth tracking
var newX = prevX * (1 - smoothingFactor) + targetX * smoothingFactor;
var newY = prevY * (1 - smoothingFactor) + targetY * smoothingFactor;
self.x = newX;
self.y = newY;
// Update previous positions
prevX = newX;
prevY = newY;
// --- APPLY SCALE TO MASK GRAPHICS ---
self.maskGraphics.scaleX = currentScale;
self.maskGraphics.scaleY = currentScale;
}
// Rotation tracking - COPIED EXACTLY from working example
if (facekit.leftEye && facekit.rightEye) {
targetTilt = calculateFaceTilt() * tiltScaleFactor;
// Limit rotation to ±15 degrees - DON'T convert to radians here
targetTilt = Math.max(-15, Math.min(15, targetTilt));
self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor;
}
};
return self;
});
var MouthAnimator = Container.expand(function () {
var self = Container.call(this);
// Load all viseme assets (keeping your existing setup)
self.visemes = {
closed: self.attachAsset('visemeClosed', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
AEI: self.attachAsset('visemeAEI', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
BMP: self.attachAsset('visemeBMP', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
CDGKNRSTXYZ: self.attachAsset('visemeCDGKNRSTXYZ', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
CHJSH: self.attachAsset('visemeCHJSH', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
EE: self.attachAsset('visemeEE', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
FV: self.attachAsset('visemeFV', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
L: self.attachAsset('visemeL', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
N: self.attachAsset('visemeN', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
O: self.attachAsset('visemeO', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
TH: self.attachAsset('visemeTH', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
U: self.attachAsset('visemeU', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
}),
QW: self.attachAsset('visimeQW', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
})
};
self.currentViseme = 'closed';
self.visemes.closed.visible = true;
// Automated lip sync data (converted from your JSON)
self.automatedLipSync = [{
start: 0.00,
end: 5.03,
value: "X"
}, {
start: 5.03,
end: 5.16,
value: "B"
}, {
start: 5.16,
end: 5.23,
value: "E"
}, {
start: 5.23,
end: 5.30,
value: "F"
}, {
start: 5.30,
end: 5.40,
value: "A"
}, {
start: 5.40,
end: 5.54,
value: "C"
}, {
start: 5.54,
end: 5.61,
value: "B"
}, {
start: 5.61,
end: 5.82,
value: "C"
}, {
start: 5.82,
end: 5.96,
value: "B"
}, {
start: 5.96,
end: 6.03,
value: "C"
}, {
start: 6.03,
end: 6.31,
value: "B"
}, {
start: 6.31,
end: 6.52,
value: "F"
}, {
start: 6.52,
end: 6.63,
value: "G"
}, {
start: 6.63,
end: 7.43,
value: "E"
}, {
start: 7.43,
end: 9.85,
value: "X"
}, {
start: 9.85,
end: 10.03,
value: "E"
}, {
start: 10.03,
end: 10.24,
value: "C"
}, {
start: 10.24,
end: 10.38,
value: "A"
}, {
start: 10.38,
end: 10.52,
value: "F"
}, {
start: 10.52,
end: 10.59,
value: "D"
}, {
start: 10.59,
end: 11.15,
value: "C"
}, {
start: 11.15,
end: 11.23,
value: "A"
}, {
start: 11.23,
end: 11.54,
value: "D"
}, {
start: 11.54,
end: 11.62,
value: "C"
}, {
start: 11.62,
end: 12.77,
value: "G"
}, {
start: 12.77,
end: 13.77,
value: "X"
}, {
start: 13.77,
end: 13.83,
value: "N"
}, {
start: 13.83,
end: 14.44,
value: "O"
}, {
start: 14.44,
end: 14.79,
value: "C"
}, {
start: 14.79,
end: 15.01,
value: "F"
}, {
start: 15.01,
end: 15.09,
value: "A"
}, {
start: 15.09,
end: 15.13,
value: "B"
}, {
start: 15.13,
end: 15.38,
value: "F"
}, {
start: 15.38,
end: 15.45,
value: "B"
}, {
start: 15.45,
end: 16.08,
value: "E"
}, {
start: 16.08,
end: 16.29,
value: "B"
}, {
start: 16.29,
end: 17.16,
value: "X"
}, {
start: 17.16,
end: 17.23,
value: "C"
}, {
start: 17.23,
end: 17.29,
value: "B"
}, {
start: 17.29,
end: 17.79,
value: "N"
}, {
start: 17.79,
end: 18.99,
value: 'E'
}, {
start: 18.99,
end: 19.53,
value: "G"
}, {
start: 19.53,
end: 22.08,
value: "X"
}, {
start: 22.08,
end: 22.42,
value: "E"
}, {
start: 22.42,
end: 22.77,
value: "B"
}, {
start: 22.77,
end: 22.85,
value: "A"
}, {
start: 22.85,
end: 23.02,
value: "N"
}, {
start: 23.02,
end: 23.30,
value: "C"
}, {
start: 23.30,
end: 23.65,
value: "C"
}, {
start: 23.65,
end: 23.86,
value: "B"
}, {
start: 23.86,
end: 24.49,
value: "E"
}, {
start: 24.49,
end: 24.91,
value: "L"
}, {
start: 24.91,
end: 24.98,
value: "E"
}, {
start: 24.98,
end: 25.05,
value: "N"
}, {
start: 25.05,
end: 25.33,
value: "E"
}, {
start: 25.33,
end: 25.75,
value: "B"
}, {
start: 25.75,
end: 25.82,
value: "D"
}, {
start: 25.82,
end: 25.96,
value: "B"
}, {
start: 25.96,
end: 26.17,
value: "F"
}, {
start: 26.17,
end: 26.45,
value: "E"
}, {
start: 26.45,
end: 27.08,
value: "B"
}, {
start: 27.08,
end: 27.43,
value: "E"
}, {
start: 27.43,
end: 27.57,
value: "F"
}, {
start: 27.57,
end: 27.71,
value: "C"
}, {
start: 27.71,
end: 28.48,
value: "O"
}, {
start: 28.48,
end: 32.70,
value: "X"
}, {
start: 32.70,
end: 32.82,
value: "B"
}, {
start: 32.82,
end: 33.03,
value: "O"
}, {
start: 33.03,
end: 33.45,
value: "E"
}, {
start: 33.45,
end: 33.66,
value: "E"
}, {
start: 33.66,
end: 33.73,
value: "C"
}, {
start: 33.73,
end: 33.87,
value: "B"
}, {
start: 33.87,
end: 34.01,
value: "E"
}, {
start: 34.01,
end: 34.29,
value: "C"
}, {
start: 34.29,
end: 34.36,
value: "B"
}, {
start: 34.36,
end: 34.50,
value: "E"
}, {
start: 34.50,
end: 34.71,
value: "B"
}, {
start: 34.71,
end: 34.85,
value: "E"
}, {
start: 34.85,
end: 35.13,
value: "C"
}, {
start: 35.13,
end: 35.20,
value: "C"
}, {
start: 35.20,
end: 35.48,
value: "B"
}, {
start: 35.48,
end: 35.55,
value: "C"
}, {
start: 35.55,
end: 35.76,
value: "E"
}, {
start: 35.76,
end: 35.90,
value: "F"
}, {
start: 35.90,
end: 36.60,
value: "B"
}, {
start: 36.60,
end: 36.71,
value: "A"
}, {
start: 36.71,
end: 36.89,
value: "O"
}, {
start: 36.89,
end: 36.94,
value: "N"
}, {
start: 36.94,
end: 37.47,
value: "B"
}, {
start: 37.47,
end: 37.55,
value: "A"
}, {
start: 37.55,
end: 37.65,
value: "E"
}, {
start: 37.65,
end: 37.72,
value: "B"
}, {
start: 37.72,
end: 37.92,
value: "F"
}, {
start: 37.92,
end: 38.00,
value: "E"
}, {
start: 38.00,
end: 40.10,
value: "O"
}, {
start: 40.10,
end: 43.42,
value: "X"
}, {
start: 43.42,
end: 43.67,
value: "C"
}, {
start: 43.67,
end: 43.74,
value: "O"
}, {
start: 43.74,
end: 43.88,
value: "F"
}, {
start: 43.88,
end: 44.23,
value: "A"
}, {
start: 44.23,
end: 44.44,
value: "C"
}, {
start: 44.44,
end: 44.58,
value: "B"
}, {
start: 44.58,
end: 44.79,
value: "C"
}, {
start: 44.79,
end: 45.07,
value: "O"
}, {
start: 45.07,
end: 45.35,
value: "L"
}, {
start: 45.35,
end: 45.63,
value: "C"
}, {
start: 45.63,
end: 45.70,
value: "B"
}, {
start: 45.70,
end: 46.12,
value: "E"
}, {
start: 46.12,
end: 46.40,
value: "E"
}, {
start: 46.40,
end: 46.45,
value: "B"
}, {
start: 46.45,
end: 47.13,
value: "F"
}, {
start: 47.13,
end: 47.27,
value: "C"
}, {
start: 47.27,
end: 47.34,
value: "B"
}, {
start: 47.34,
end: 47.55,
value: "E"
}, {
start: 47.55,
end: 47.76,
value: "C"
}, {
start: 47.76,
end: 47.83,
value: "B"
}, {
start: 47.83,
end: 48.04,
value: "D"
}, {
start: 48.04,
end: 48.11,
value: "B"
}, {
start: 48.11,
end: 48.81,
value: "F"
}, {
start: 48.81,
end: 48.88,
value: "A"
}, {
start: 48.88,
end: 48.95,
value: "L"
}, {
start: 48.95,
end: 49.02,
value: "E"
}, {
start: 49.02,
end: 50.28,
value: "B"
}, {
start: 50.28,
end: 50.61,
value: "A"
}, {
start: 50.61,
end: 53.74,
value: "X"
}, {
start: 53.74,
end: 54.23,
value: "C"
}, {
start: 54.23,
end: 54.31,
value: "A"
}, {
start: 54.31,
end: 54.51,
value: "B"
}, {
start: 54.51,
end: 54.79,
value: "E"
}, {
start: 54.79,
end: 54.90,
value: "A"
}, {
start: 54.90,
end: 55.01,
value: "B"
}, {
start: 55.01,
end: 55.12,
value: "A"
}, {
start: 55.12,
end: 55.18,
value: "C"
}, {
start: 55.18,
end: 55.52,
value: "B"
}, {
start: 55.52,
end: 55.59,
value: "C"
}, {
start: 55.59,
end: 55.87,
value: "B"
}, {
start: 55.87,
end: 56.22,
value: "F"
}, {
start: 56.22,
end: 56.36,
value: "B"
}, {
start: 56.36,
end: 56.66,
value: "F"
}, {
start: 56.66,
end: 56.74,
value: "E"
}, {
start: 56.74,
end: 56.89,
value: "C"
}, {
start: 56.89,
end: 57.17,
value: "A"
}, {
start: 57.17,
end: 57.69,
value: "N"
}, {
start: 57.69,
end: 57.92,
value: "C"
}, {
start: 57.92,
end: 58.20,
value: "B"
}, {
start: 58.20,
end: 59.04,
value: "F"
}, {
start: 59.04,
end: 59.18,
value: "B"
}, {
start: 59.18,
end: 59.39,
value: "E"
}, {
start: 59.39,
end: 59.53,
value: "C"
}, {
start: 59.53,
end: 59.98,
value: "O"
}, {
start: 59.98,
end: 60.06,
value: "N"
}, {
start: 60.06,
end: 60.37,
value: "B"
}, {
start: 60.37,
end: 60.51,
value: "C"
}, {
start: 60.51,
end: 60.65,
value: "E"
}, {
start: 60.65,
end: 62.19,
value: "N"
}, {
start: 62.19,
end: 62.33,
value: "X"
}, {
start: 62.33,
end: 62.55,
value: "B"
}, {
start: 62.55,
end: 63.24,
value: "X"
}, {
start: 63.24,
end: 63.38,
value: "B"
}, {
start: 63.38,
end: 63.73,
value: "E"
}, {
start: 63.73,
end: 63.87,
value: "C"
}, {
start: 63.87,
end: 63.99,
value: "A"
}, {
start: 63.99,
end: 64.11,
value: "C"
}, {
start: 64.11,
end: 64.46,
value: "B"
}, {
start: 64.46,
end: 64.60,
value: "F"
}, {
start: 64.60,
end: 64.67,
value: "B"
}, {
start: 64.67,
end: 64.88,
value: "F"
}, {
start: 64.88,
end: 64.95,
value: "C"
}, {
start: 64.95,
end: 65.88,
value: "B"
}, {
start: 65.88,
end: 66.43,
value: "X"
}, {
start: 66.43,
end: 66.62,
value: "D"
}, {
start: 66.62,
end: 66.81,
value: "C"
}, {
start: 66.81,
end: 66.86,
value: "E"
}, {
start: 66.86,
end: 67.11,
value: "C"
}, {
start: 67.11,
end: 68.93,
value: "B"
}, {
start: 68.93,
end: 69.14,
value: "X"
}, {
start: 69.14,
end: 70.99,
value: "A"
}, {
start: 70.99,
end: 71.13,
value: "F"
}, {
start: 71.13,
end: 72.63,
value: "A"
}, {
start: 72.63,
end: 72.71,
value: "C"
}, {
start: 72.71,
end: 73.16,
value: "B"
}, {
start: 73.16,
end: 75.30,
value: "X"
}, {
start: 75.30,
end: 75.57,
value: "B"
}, {
start: 75.57,
end: 75.71,
value: "E"
}, {
start: 75.71,
end: 75.99,
value: "B"
}, {
start: 75.99,
end: 76.27,
value: "C"
}, {
start: 76.27,
end: 76.34,
value: "B"
}, {
start: 76.34,
end: 76.83,
value: "F"
}, {
start: 76.83,
end: 77.04,
value: "E"
}, {
start: 77.04,
end: 77.11,
value: "C"
}, {
start: 77.11,
end: 77.18,
value: "H"
}, {
start: 77.18,
end: 77.37,
value: "E"
}, {
start: 77.37,
end: 77.42,
value: "C"
}, {
start: 77.42,
end: 77.81,
value: "B"
}, {
start: 77.81,
end: 79.14,
value: "F"
}, {
start: 79.14,
end: 79.48,
value: "B"
}, {
start: 79.48,
end: 80.18,
value: "E"
}, {
start: 80.18,
end: 80.46,
value: "C"
}, {
start: 80.46,
end: 83.12,
value: "B"
}, {
start: 83.12,
end: 84.24,
value: "E"
}, {
start: 84.24,
end: 84.45,
value: "B"
}, {
start: 84.45,
end: 85.37,
value: "X"
}, {
start: 85.37,
end: 85.55,
value: "F"
}, {
start: 85.55,
end: 86.08,
value: "B"
}, {
start: 86.08,
end: 86.62,
value: "F"
}, {
start: 86.62,
end: 86.67,
value: "C"
}, {
start: 86.67,
end: 87.00,
value: "E"
}, {
start: 87.00,
end: 87.28,
value: "F"
}, {
start: 87.28,
end: 87.42,
value: "D"
}, {
start: 87.42,
end: 87.70,
value: "C"
}, {
start: 87.70,
end: 88.54,
value: "B"
}, {
start: 88.54,
end: 89.42,
value: "D"
}, {
start: 89.42,
end: 89.49,
value: "C"
}, {
start: 89.49,
end: 89.73,
value: "B"
}, {
start: 89.73,
end: 90.46,
value: "X"
}, {
start: 90.46,
end: 90.81,
value: "E"
}, {
start: 90.81,
end: 91.27,
value: "F"
}, {
start: 91.27,
end: 91.34,
value: "E"
}, {
start: 91.34,
end: 91.41,
value: "F"
}, {
start: 91.41,
end: 92.11,
value: "B"
}, {
start: 92.11,
end: 93.07,
value: "X"
}, {
start: 93.07,
end: 93.33,
value: "C"
}, {
start: 93.33,
end: 93.40,
value: "E"
}, {
start: 93.40,
end: 93.78,
value: "F"
}, {
start: 93.78,
end: 93.85,
value: "B"
}, {
start: 93.85,
end: 94.16,
value: "D"
}, {
start: 94.16,
end: 94.24,
value: "C"
}, {
start: 94.24,
end: 95.04,
value: "B"
}, {
start: 95.04,
end: 95.79,
value: "X"
}, {
start: 95.79,
end: 95.89,
value: "C"
}, {
start: 95.89,
end: 95.96,
value: "B"
}, {
start: 95.96,
end: 96.05,
value: "A"
}, {
start: 96.05,
end: 96.31,
value: "C"
}, {
start: 96.31,
end: 96.49,
value: "B"
}, {
start: 96.49,
end: 97.18,
value: "D"
}, {
start: 97.18,
end: 97.26,
value: "C"
}, {
start: 97.26,
end: 98.43,
value: "X"
}, {
start: 98.43,
end: 98.54,
value: "E"
}, {
start: 98.54,
end: 98.61,
value: "F"
}, {
start: 98.61,
end: 98.68,
value: "C"
}, {
start: 98.68,
end: 98.75,
value: "B"
}, {
start: 98.75,
end: 98.82,
value: "F"
}, {
start: 98.82,
end: 99.10,
value: "C"
}, {
start: 99.10,
end: 99.18,
value: "A"
}, {
start: 99.18,
end: 99.64,
value: "C"
}, {
start: 99.64,
end: 99.85,
value: "B"
}, {
start: 99.85,
end: 101.13,
value: "X"
}, {
start: 101.13,
end: 101.26,
value: "D"
}, {
start: 101.26,
end: 101.33,
value: "B"
}, {
start: 101.33,
end: 101.45,
value: "A"
}, {
start: 101.45,
end: 101.86,
value: "C"
}, {
start: 101.86,
end: 102.38,
value: "B"
}, {
start: 102.38,
end: 102.46,
value: "A"
}, {
start: 102.46,
end: 103.19,
value: "N"
}, {
start: 103.19,
end: 103.47,
value: "B"
}, {
start: 103.47,
end: 104.52,
value: "A"
}, {
start: 104.52,
end: 105.15,
value: "C"
}, {
start: 105.15,
end: 105.57,
value: "B"
}, {
start: 105.57,
end: 108.41,
value: "X"
}, {
start: 108.41,
end: 108.56,
value: "A"
}, {
start: 108.56,
end: 108.63,
value: "C"
}, {
start: 108.63,
end: 108.84,
value: "B"
}, {
start: 108.84,
end: 108.98,
value: "A"
}, {
start: 108.98,
end: 109.05,
value: "L"
}, {
start: 109.05,
end: 109.33,
value: "A"
}, {
start: 109.33,
end: 109.68,
value: "B"
}, {
start: 109.68,
end: 109.82,
value: "E"
}, {
start: 109.82,
end: 109.89,
value: "B"
}, {
start: 109.89,
end: 110.03,
value: "O"
}, {
start: 110.03,
end: 110.17,
value: "F"
}, {
start: 110.17,
end: 110.24,
value: "B"
}, {
start: 110.24,
end: 110.52,
value: "A"
}, {
start: 110.52,
end: 110.59,
value: "D"
}, {
start: 110.59,
end: 111.01,
value: "N"
}, {
start: 111.01,
end: 111.08,
value: "E"
}, {
start: 111.08,
end: 111.57,
value: "C"
}, {
start: 111.57,
end: 111.64,
value: "B"
}, {
start: 111.64,
end: 111.99,
value: "E"
}, {
start: 111.99,
end: 113.77,
value: "X"
}, {
start: 113.77,
end: 114.16,
value: "B"
}, {
start: 114.16,
end: 114.65,
value: "E"
}, {
start: 114.65,
end: 114.93,
value: "C"
}, {
start: 114.93,
end: 115.00,
value: "C"
}, {
start: 115.00,
end: 115.24,
value: "A"
}, {
start: 115.24,
end: 115.46,
value: "N"
}, {
start: 115.46,
end: 115.60,
value: "C"
}, {
start: 115.60,
end: 115.74,
value: "B"
}, {
start: 115.74,
end: 115.88,
value: "E"
}, {
start: 115.88,
end: 116.09,
value: "A"
}, {
start: 116.09,
end: 116.17,
value: "B"
}, {
start: 116.17,
end: 116.24,
value: "E"
}, {
start: 116.24,
end: 116.73,
value: "B"
}, {
start: 116.73,
end: 117.57,
value: "X"
}, {
start: 117.57,
end: 117.80,
value: "B"
}, {
start: 117.80,
end: 118.81,
value: "X"
}, {
start: 118.81,
end: 119.28,
value: "B"
}, {
start: 119.28,
end: 119.35,
value: "F"
}, {
start: 119.35,
end: 119.49,
value: "E"
}, {
start: 119.49,
end: 119.65,
value: "A"
}, {
start: 119.65,
end: 119.88,
value: "C"
}, {
start: 119.88,
end: 119.95,
value: "E"
}, {
start: 119.95,
end: 120.16,
value: "B"
}, {
start: 120.16,
end: 120.30,
value: "E"
}, {
start: 120.30,
end: 120.37,
value: "B"
}, {
start: 120.37,
end: 120.65,
value: "E"
}, {
start: 120.65,
end: 120.79,
value: "F"
}, {
start: 120.79,
end: 120.93,
value: "B"
}, {
start: 120.93,
end: 121.00,
value: "E"
}, {
start: 121.00,
end: 121.28,
value: "L"
}, {
start: 121.28,
end: 121.49,
value: "O"
}, {
start: 121.49,
end: 121.56,
value: "F"
}, {
start: 121.56,
end: 121.70,
value: "B"
}, {
start: 121.70,
end: 121.98,
value: "A"
}, {
start: 121.98,
end: 122.19,
value: "E"
}, {
start: 122.19,
end: 122.61,
value: "C"
}, {
start: 122.61,
end: 123.56,
value: "B"
}, {
start: 123.56,
end: 123.77,
value: "X"
}, {
start: 123.77,
end: 123.87,
value: "B"
}, {
start: 123.87,
end: 124.02,
value: "A"
}, {
start: 124.02,
end: 124.16,
value: "L"
}, {
start: 124.16,
end: 124.30,
value: "F"
}, {
start: 124.30,
end: 124.72,
value: "B"
}, {
start: 124.72,
end: 125.00,
value: "E"
}, {
start: 125.00,
end: 125.99,
value: "D"
}, {
start: 125.99,
end: 126.06,
value: "C"
}, {
start: 126.06,
end: 126.34,
value: "I"
}, {
start: 126.34,
end: 126.48,
value: "B"
}, {
start: 126.48,
end: 126.62,
value: "E"
}, {
start: 126.62,
end: 126.69,
value: "F"
}, {
start: 126.69,
end: 126.83,
value: "B"
}, {
start: 126.83,
end: 128.26,
value: "A"
}, {
start: 128.26,
end: 128.43,
value: "C"
}, {
start: 128.43,
end: 129.70,
value: "X"
}, {
start: 129.70,
end: 129.85,
value: "A"
}, {
start: 129.85,
end: 129.96,
value: "L"
}, {
start: 129.96,
end: 130.03,
value: "F"
}, {
start: 130.03,
end: 130.17,
value: "B"
}, {
start: 130.17,
end: 130.38,
value: "E"
}, {
start: 130.38,
end: 130.52,
value: "D"
}, {
start: 130.52,
end: 130.59,
value: "B"
}, {
start: 130.59,
end: 130.87,
value: "I"
}, {
start: 130.87,
end: 131.08,
value: "B"
}, {
start: 131.08,
end: 131.15,
value: "D"
}, {
start: 131.15,
end: 131.29,
value: "O"
}, {
start: 131.29,
end: 131.36,
value: "B"
}, {
start: 131.36,
end: 131.50,
value: "B"
}, {
start: 131.50,
end: 131.81,
value: "A"
}, {
start: 131.81,
end: 131.89,
value: "C"
}, {
start: 131.89,
end: 132.20,
value: "B"
}, {
start: 132.20,
end: 132.48,
value: "E"
}, {
start: 132.48,
end: 132.62,
value: "X"
}, {
start: 132.62,
end: 132.69,
value: "B"
}, {
start: 132.69,
end: 132.90,
value: "U"
}, {
start: 132.90,
end: 133.25,
value: "B"
}, {
start: 133.25,
end: 133.45,
value: "F"
}, {
start: 133.45,
end: 133.53,
value: "E"
}, {
start: 133.53,
end: 133.88,
value: "B"
}, {
start: 133.88,
end: 134.02,
value: "U"
}, {
start: 134.02,
end: 134.14,
value: "X"
}, {
start: 134.14,
end: 134.29,
value: "B"
}, {
start: 134.29,
end: 134.36,
value: "A"
}, {
start: 134.36,
end: 134.50,
value: "B"
}, {
start: 134.50,
end: 134.71,
value: "D"
}, {
start: 134.71,
end: 134.85,
value: "E"
}, {
start: 134.85,
end: 135.06,
value: "N"
}, {
start: 135.06,
end: 135.20,
value: "C"
}, {
start: 135.20,
end: 135.27,
value: "B"
}, {
start: 135.27,
end: 135.69,
value: "A"
}, {
start: 135.69,
end: 135.83,
value: "B"
}, {
start: 135.83,
end: 136.53,
value: "E"
}, {
start: 136.53,
end: 136.67,
value: "B"
}, {
start: 136.67,
end: 140.08,
value: "X"
}, {
start: 140.08,
end: 140.22,
value: "D"
}, {
start: 140.22,
end: 140.29,
value: "B"
}, {
start: 140.29,
end: 140.57,
value: "E"
}, {
start: 140.57,
end: 141.20,
value: "F"
}, {
start: 141.20,
end: 141.41,
value: "B"
}, {
start: 141.41,
end: 141.76,
value: "A"
}, {
start: 141.76,
end: 142.11,
value: "C"
}, {
start: 142.11,
end: 142.18,
value: "N"
}, {
start: 142.18,
end: 142.25,
value: "E"
}, {
start: 142.25,
end: 142.74,
value: "C"
}, {
start: 142.74,
end: 142.93,
value: "B"
}, {
start: 142.93,
end: 142.98,
value: "A"
}, {
start: 142.98,
end: 143.21,
value: "C"
}, {
start: 143.21,
end: 143.97,
value: "D"
}, {
start: 143.97,
end: 144.11,
value: "B"
}, {
start: 144.11,
end: 144.27,
value: "X"
}, {
start: 144.27,
end: 144.40,
value: "B"
}, {
start: 144.40,
end: 145.11,
value: "X"
}, {
start: 145.11,
end: 145.58,
value: "B"
}, {
start: 145.58,
end: 145.72,
value: "F"
}, {
start: 145.72,
end: 145.80,
value: "A"
}, {
start: 145.80,
end: 145.87,
value: "C"
}, {
start: 145.87,
end: 146.18,
value: "D"
}, {
start: 146.18,
end: 146.25,
value: "C"
}, {
start: 146.25,
end: 146.49,
value: "B"
}, {
start: 146.49,
end: 146.56,
value: "A"
}, {
start: 146.56,
end: 146.77,
value: "L"
}, {
start: 146.77,
end: 146.85,
value: "A"
}, {
start: 146.85,
end: 147.26,
value: "E"
}, {
start: 147.26,
end: 147.47,
value: "B"
}, {
start: 147.47,
end: 147.75,
value: "F"
}, {
start: 147.75,
end: 147.82,
value: "E"
}, {
start: 147.82,
end: 148.15,
value: "D"
}, {
start: 148.15,
end: 148.20,
value: "C"
}, {
start: 148.20,
end: 148.38,
value: "B"
}, {
start: 148.38,
end: 148.45,
value: "F"
}, {
start: 148.45,
end: 150.48,
value: "E"
}, {
start: 150.48,
end: 150.90,
value: "B"
}, {
start: 150.90,
end: 151.41,
value: "X"
}, {
start: 151.41,
end: 151.76,
value: "E"
}, {
start: 151.76,
end: 151.90,
value: "F"
}, {
start: 151.90,
end: 152.15,
value: "A"
}, {
start: 152.15,
end: 152.40,
value: "E"
}, {
start: 152.40,
end: 152.54,
value: "F"
}, {
start: 152.54,
end: 152.61,
value: "B"
}, {
start: 152.61,
end: 152.89,
value: "E"
}, {
start: 152.89,
end: 153.13,
value: "F"
}, {
start: 153.13,
end: 153.19,
value: "C"
}, {
start: 153.19,
end: 153.38,
value: "E"
}, {
start: 153.38,
end: 153.45,
value: "F"
}, {
start: 153.45,
end: 154.69,
value: "X"
}, {
start: 154.69,
end: 154.96,
value: "C"
}, {
start: 154.96,
end: 155.16,
value: "A"
}, {
start: 155.16,
end: 155.54,
value: "F"
}, {
start: 155.54,
end: 155.62,
value: "B"
}, {
start: 155.62,
end: 156.49,
value: "E"
}, {
start: 156.49,
end: 156.63,
value: "B"
}, {
start: 156.63,
end: 157.19,
value: "E"
}, {
start: 157.19,
end: 157.47,
value: "L"
}, {
start: 157.47,
end: 157.68,
value: "O"
}, {
start: 157.68,
end: 158.18,
value: "B"
}, {
start: 158.18,
end: 158.22,
value: "B"
}, {
start: 158.22,
end: 159.21,
value: "A"
}, {
start: 159.21,
end: 159.83,
value: "E"
}, {
start: 159.83,
end: 160.25,
value: "C"
}, {
start: 160.25,
end: 160.81,
value: "B"
}, {
start: 160.81,
end: 163.32,
value: "X"
}, {
start: 163.32,
end: 163.41,
value: "B"
}, {
start: 163.41,
end: 163.49,
value: "A"
}, {
start: 163.49,
end: 163.68,
value: "L"
}, {
start: 163.68,
end: 163.73,
value: "F"
}, {
start: 163.73,
end: 163.91,
value: "B"
}, {
start: 163.91,
end: 164.12,
value: "E"
}, {
start: 164.12,
end: 164.71,
value: "D"
}, {
start: 164.71,
end: 164.92,
value: "C"
}, {
start: 164.92,
end: 165.13,
value: "I"
}, {
start: 165.13,
end: 165.48,
value: "D"
}, {
start: 165.48,
end: 165.62,
value: "O"
}, {
start: 165.62,
end: 165.90,
value: "M"
}, {
start: 165.90,
end: 165.97,
value: "C"
}, {
start: 165.97,
end: 167.23,
value: "A"
}, {
start: 167.23,
end: 167.68,
value: "X"
}, {
start: 167.68,
end: 168.61,
value: "A"
}, {
start: 168.61,
end: 168.91,
value: "L"
}, {
start: 168.91,
end: 168.98,
value: "F"
}, {
start: 168.98,
end: 169.40,
value: "B"
}, {
start: 169.40,
end: 169.73,
value: "E"
}, {
start: 169.73,
end: 172.56,
value: "D"
}, {
start: 172.56,
end: 172.64,
value: "C"
}, {
start: 172.64,
end: 173.40,
value: "X"
}, {
start: 173.40,
end: 173.53,
value: "B"
}, {
start: 173.53,
end: 174.56,
value: "U"
}, {
start: 174.56,
end: 174.63,
value: "F"
}, {
start: 174.63,
end: 174.70,
value: "G"
}, {
start: 174.70,
end: 174.98,
value: "E"
}, {
start: 174.98,
end: 175.26,
value: "H"
}, {
start: 175.26,
end: 175.68,
value: "C"
}, {
start: 175.68,
end: 176.10,
value: "B"
}, {
start: 176.10,
end: 176.24,
value: "E"
}, {
start: 176.24,
end: 176.45,
value: "F"
}, {
start: 176.45,
end: 176.52,
value: "B"
}, {
start: 176.52,
end: 177.64,
value: "O"
}, {
start: 177.64,
end: 177.85,
value: "X"
}, {
start: 177.85,
end: 177.97,
value: "C"
}, {
start: 177.97,
end: 178.27,
value: "X"
}, {
start: 178.27,
end: 178.40,
value: "B"
}, {
start: 178.40,
end: 178.54,
value: "A"
}, {
start: 178.54,
end: 178.68,
value: "R"
}, {
start: 178.68,
end: 179.17,
value: "E"
}, {
start: 179.17,
end: 179.94,
value: "B"
}, {
start: 179.94,
end: 181.13,
value: "X"
}, {
start: 181.13,
end: 181.25,
value: "D"
}, {
start: 181.25,
end: 181.39,
value: "B"
}, {
start: 181.39,
end: 181.94,
value: "E"
}, {
start: 181.94,
end: 182.33,
value: "A"
}, {
start: 182.33,
end: 182.41,
value: "B"
}, {
start: 182.41,
end: 182.86,
value: "B"
}, {
start: 182.86,
end: 182.98,
value: "X"
}, {
start: 182.98,
end: 183.17,
value: "B"
}, {
start: 183.17,
end: 183.81,
value: "X"
}, {
start: 183.81,
end: 183.96,
value: "C"
}, {
start: 183.96,
end: 184.18,
value: "A"
}, {
start: 184.18,
end: 184.63,
value: "N"
}, {
start: 184.63,
end: 184.71,
value: "A"
}, {
start: 184.71,
end: 185.44,
value: "B"
}, {
start: 185.44,
end: 186.32,
value: "X"
}, {
start: 186.32,
end: 186.49,
value: "B"
}, {
start: 186.49,
end: 186.56,
value: "A"
}, {
start: 186.56,
end: 186.63,
value: "T"
}, {
start: 186.63,
end: 186.78,
value: "B"
}, {
start: 186.78,
end: 187.00,
value: "A"
}, {
start: 187.00,
end: 188.12,
value: "N"
}, {
start: 188.12,
end: 189.10,
value: "X"
}, {
start: 189.10,
end: 189.33,
value: "N"
}, {
start: 189.33,
end: 189.47,
value: "A"
}, {
start: 189.47,
end: 189.66,
value: "N"
}, {
start: 189.66,
end: 189.71,
value: "A"
}, {
start: 189.71,
end: 190.80,
value: "N"
}, {
start: 190.80,
end: 191.22,
value: "A"
}, {
start: 191.22,
end: 191.46,
value: "N"
}, {
start: 191.46,
end: 191.50,
value: "A"
}, {
start: 191.50,
end: 191.54,
value: "N"
}, {
start: 191.54,
end: 192.52,
value: "A"
}, {
start: 192.52,
end: 192.69,
value: "N"
}, {
start: 192.69,
end: 192.83,
value: "A"
}, {
start: 192.83,
end: 193.25,
value: "B"
}, {
start: 193.25,
end: 193.50,
value: "A"
}, {
start: 193.50,
end: 193.71,
value: "T"
}, {
start: 193.71,
end: 193.82,
value: "B"
}, {
start: 193.82,
end: 194.03,
value: "A"
}, {
start: 194.03,
end: 194.09,
value: "N"
}, {
start: 194.09,
end: 195.53,
value: "B"
}, {
start: 195.53,
end: 195.77,
value: "X"
}, {
start: 195.77,
end: 195.95,
value: "N"
}, {
start: 195.95,
end: 196.65,
value: "A"
}, {
start: 196.65,
end: 198.41,
value: "N"
}, {
start: 198.41,
end: 198.66,
value: "A"
}, {
start: 198.66,
end: 198.80,
value: "N"
}, {
start: 198.80,
end: 199.08,
value: "A"
}, {
start: 199.08,
end: 199.15,
value: "N"
}, {
start: 199.15,
end: 199.43,
value: "A"
}, {
start: 199.43,
end: 199.85,
value: "N"
}, {
start: 199.85,
end: 199.99,
value: "A"
}, {
start: 199.99,
end: 200.20,
value: "N"
}, {
start: 200.20,
end: 200.41,
value: "A"
}, {
start: 200.41,
end: 201.15,
value: "B"
}, {
start: 201.15,
end: 202.25,
value: "A"
}, {
start: 202.25,
end: 202.33,
value: "T"
}, {
start: 202.33,
end: 203.09,
value: "B"
}, {
start: 203.09,
end: 203.19,
value: "A"
}, {
start: 203.19,
end: 203.26,
value: "N"
}, {
start: 203.26,
end: 203.33,
value: "B"
}, {
start: 203.33,
end: 203.89,
value: "A"
}, {
start: 203.89,
end: 206.72,
value: "X"
}];
// Viseme mapping
self.visemeMapping = {
'X': 'closed',
'A': 'AEI',
'B': 'BMP',
'C': 'CDGKNRSTXYZ',
'D': 'AEI',
'E': 'EE',
'F': 'FV',
'G': 'CDGKNRSTXYZ',
'H': 'TH',
// Added TH viseme
'L': 'L',
// Added L viseme
'N': 'N',
// Added N viseme
'O': 'O',
// Added O viseme
'Q': 'QW',
// Added QW viseme
'U': 'U' // Added U viseme
};
self.songStartTime = null;
self.isLipSyncActive = false;
self.currentLipSyncIndex = 0;
// Position tracking variables (keeping your existing setup)
var targetX = GAME_WIDTH / 2;
var targetY = GAME_HEIGHT / 2 + 200;
var smoothingFactor = 0.18;
var prevX = null;
var prevY = null;
// Scale tracking variables (keeping your existing setup)
var scaleHistory = new Array(5).fill(1);
var scaleIndex = 0;
var currentScale = 1;
// Rotation variables (keeping your existing setup)
var targetTilt = 0;
var tiltSmoothingFactor = 0.11;
var tiltScaleFactor = 0.09;
function calculateFaceTilt() {
if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) {
var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2;
var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2;
var dx = facekit.mouthCenter.x - eyeMidX;
var dy = facekit.mouthCenter.y - eyeMidY;
var angle = -(Math.atan2(dx, dy) * (180 / Math.PI));
return Math.max(-15, Math.min(15, angle * 0.15));
}
return 0;
}
self.startLipSync = function () {
self.songStartTime = Date.now();
self.isLipSyncActive = true;
self.currentLipSyncIndex = 0;
};
self.setViseme = function (visemeName) {
if (self.currentViseme !== visemeName && self.visemes[visemeName]) {
if (self.visemes[self.currentViseme]) {
self.visemes[self.currentViseme].visible = false;
}
self.visemes[visemeName].visible = true;
self.currentViseme = visemeName;
}
};
self.updateAutomatedLipSync = function () {
if (!self.isLipSyncActive || !self.songStartTime || self.automatedLipSync.length === 0) {
return;
}
var currentTimeSeconds = (Date.now() - self.songStartTime) / 1000.0;
var targetVisemeKey = 'closed';
// Start searching from currentLipSyncIndex for optimization
for (var i = self.currentLipSyncIndex; i < self.automatedLipSync.length; i++) {
var cue = self.automatedLipSync[i];
if (currentTimeSeconds >= cue.start && currentTimeSeconds < cue.end) {
var automatedCode = cue.value;
targetVisemeKey = self.visemeMapping[automatedCode] || 'closed';
self.currentLipSyncIndex = i;
break;
}
if (currentTimeSeconds >= cue.end) {
if (i === self.automatedLipSync.length - 1) {
var automatedCode = cue.value;
targetVisemeKey = self.visemeMapping[automatedCode] || 'closed';
}
} else if (currentTimeSeconds < cue.start) {
break;
}
}
if (self.currentLipSyncIndex === self.automatedLipSync.length - 1 && currentTimeSeconds >= self.automatedLipSync[self.automatedLipSync.length - 1].end) {
targetVisemeKey = 'closed';
}
self.setViseme(targetVisemeKey);
};
self.update = function () {
// Update automated lip sync
self.updateAutomatedLipSync();
// Smooth position tracking
if (facekit.mouthCenter) {
targetX = facekit.mouthCenter.x;
targetY = facekit.mouthCenter.y;
if (prevX === null) {
prevX = targetX;
prevY = targetY;
}
var newX = prevX * (1 - smoothingFactor) + targetX * smoothingFactor;
var newY = prevY * (1 - smoothingFactor) + targetY * smoothingFactor;
self.x = newX;
self.y = newY;
prevX = newX;
prevY = newY;
}
// Dynamic scale adjustment
if (facekit.leftEye && facekit.rightEye) {
var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x);
var newScale = eyeDistance / 250;
scaleHistory[scaleIndex] = newScale;
scaleIndex = (scaleIndex + 1) % scaleHistory.length;
var avgScale = scaleHistory.reduce(function (a, b) {
return a + b;
}, 0) / scaleHistory.length;
var targetScale = avgScale;
currentScale = currentScale * 0.85 + targetScale * 0.15;
Object.keys(self.visemes).forEach(function (key) {
self.visemes[key].scaleX = currentScale;
self.visemes[key].scaleY = currentScale;
});
}
// Rotation tracking
if (facekit.leftEye && facekit.rightEye) {
targetTilt = calculateFaceTilt() * tiltScaleFactor;
targetTilt = Math.max(-15, Math.min(15, targetTilt));
self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor;
}
};
return self;
});
var StartButton = Container.expand(function () {
var self = Container.call(this);
self.buttonGraphics = self.attachAsset('startButtonImage', {
anchorX: 0.5,
anchorY: 0.5
});
self.down = function () {
// This will call the globally defined startGame function
// when the button is pressed.
if (typeof handleStartButtonPressed === 'function') {
handleStartButtonPressed();
}
};
return self;
});
var SubtitleDisplay = Container.expand(function () {
var self = Container.call(this);
// Create text display properties
self.currentText = "";
self.textDisplay = null;
self.subtitleData = [{
start: 4.8,
end: 7.5,
text: "From the ashes of tragedy"
}, {
start: 10.0,
end: 12.5,
text: "A guardian rises"
}, {
start: 14.0,
end: 16.0,
text: "Not the hero they deserve"
}, {
start: 17.5,
end: 19.5,
text: "But the one they need"
}, {
start: 22.5,
end: 24.5,
text: "Parents fallen in an alley cold"
}, {
start: 25.0,
end: 29.0,
text: "A boy's heart turned to stone"
}, {
start: 33.0,
end: 35.0,
text: "Fortune built on others' pain"
}, {
start: 36.0,
end: 40.0,
text: "Now I claim the night as my own"
}, {
start: 43.5,
end: 46.0,
text: "Gotham calls to me in whispers dire"
}, {
start: 47.0,
end: 51.0,
text: "Criminals scatter at my arrival"
}, {
start: 53.5,
end: 56.0,
text: "The cape becomes my second skin"
}, {
start: 57.0,
end: 62.0,
text: "This mask - my true survival"
},
// Chorus
{
start: 63.5,
end: 69.0,
text: "From the darkness I emerge"
}, {
start: 75.0,
end: 79.0,
text: "Justice is what I serve"
}, {
start: 85.5,
end: 90.0,
text: "When evil plagues these streets"
}, {
start: 90.5,
end: 92.0,
text: "I'll be there"
}, {
start: 93.5,
end: 94.5,
text: "I'll be there"
}, {
start: 96.0,
end: 97.0,
text: "I'm Batman"
}, {
start: 98.5,
end: 100.0,
text: "I'm Batman"
}, {
start: 101.5,
end: 102.5,
text: "I'm Batman"
}, {
start: 103.0,
end: 105.0,
text: "I'm Batman"
},
// Verse 2
{
start: 109.0,
end: 112.5,
text: "A gallery of madness I've faced"
}, {
start: 114.0,
end: 116.5,
text: "The Joker with his twisted grin"
}, {
start: 120.0,
end: 123.8,
text: "Riddler's puzzles, Penguin's schemes"
}, {
start: 124.0,
end: 128.5,
text: "Two-Face's coin, the battle within"
}, {
start: 130.0,
end: 132.0,
text: "My allies few, my burden great"
}, {
start: 133.0,
end: 137.0,
text: "The cave below, my sanctuary"
}, {
start: 140.5,
end: 144.5,
text: "Alfred's wisdom guides my path"
}, {
start: 145.0,
end: 150.5,
text: "Through this life so solitary"
},
// Chorus repeat
{
start: 151.5,
end: 157.0,
text: "From the darkness I emerge"
}, {
start: 163.5,
end: 167.5,
text: "Justice is what I serve"
}, {
start: 173.5,
end: 178.0,
text: "When evil plagues these streets"
}, {
start: 178.5,
end: 180.0,
text: "I'll be there"
}, {
start: 181.0,
end: 182.5,
text: "I'll be there"
}, {
start: 184.0,
end: 185.0,
text: "I'm Batman"
}, {
start: 186.5,
end: 188.0,
text: "I'm Batman"
}, {
start: 189.0,
end: 190.5,
text: "I'm Batman"
}, {
start: 191.5,
end: 193.0,
text: "I'm Batman"
},
// Outro
{
start: 194.0,
end: 197.0,
text: "Na na na na na na na na"
}, {
start: 197.0,
end: 200.0,
text: "Na na na na na na na na"
}, {
start: 198.0,
end: 198.5,
text: "Batman"
}, {
start: 199.0,
end: 202.0,
text: "Na na na na na na na na"
}, {
start: 202.0,
end: 205.0,
text: "Na na na na na na na na"
}, {
start: 203.0,
end: 204.5,
text: "Batman!"
}];
self.songStartTime = null;
self.isActive = false;
self.currentSubtitleIndex = 0;
// Text styling properties
self.textStyle = {
fontFamily: 'Arial',
// Font family for Text2
fontSize: 96,
// Font size (doubled)
// fontWeight: 'bold', // fontWeight is part of font string in Text2 if specific bold font is used
color: '#FFFFFF',
// Fill color
stroke: '#000000',
// Stroke color
strokeWidth: 3,
// Mapped to strokeThickness in Text2
textAlign: 'center' // Mapped to align in Text2
// textBaseline: 'middle', // Handled by anchor in Text2
// shadowColor: '#000000', // Shadow not directly supported by LK Text2
// shadowBlur: 8,
// shadowOffsetX: 2,
// shadowOffsetY: 2
};
self.createTextGraphics = function (text) {
if (self.textDisplay) {
// If a previous text object exists
self.textDisplay.destroy(); // Destroy it
self.textDisplay = null; // Clear the reference
}
if (!text || text.trim() === "") {
// If new text is empty, do nothing further
return;
}
// Create new Text2 object
self.textDisplay = new Text2(text, {
font: self.textStyle.fontFamily,
size: self.textStyle.fontSize,
fill: self.textStyle.color,
stroke: self.textStyle.stroke,
strokeThickness: self.textStyle.strokeWidth,
align: self.textStyle.textAlign
});
self.textDisplay.anchor.set(0.5, 0.5); // Center the text within its bounds
self.addChild(self.textDisplay); // Add the new text display to the container
};
self.startSubtitles = function () {
self.songStartTime = Date.now();
self.isActive = true;
self.currentSubtitleIndex = 0;
};
self.updateSubtitles = function () {
if (!self.isActive || !self.songStartTime || self.subtitleData.length === 0) {
return;
}
var currentTimeSeconds = (Date.now() - self.songStartTime) / 1000.0;
var newText = "";
var foundSubtitle = false;
// Find current subtitle
// Iterate from the current index for efficiency or from start if reset needed
for (var i = self.currentSubtitleIndex; i < self.subtitleData.length; i++) {
var subtitle = self.subtitleData[i];
if (currentTimeSeconds >= subtitle.start && currentTimeSeconds < subtitle.end) {
newText = subtitle.text;
self.currentSubtitleIndex = i; // Keep track of the current subtitle index
foundSubtitle = true;
break;
}
// If current time is less than the start of this subtitle, means no future subtitle will match yet.
if (currentTimeSeconds < subtitle.start) {
break;
}
// If time has passed this subtitle's end, it might be the one if nothing else matches
if (currentTimeSeconds >= subtitle.end) {
// If it's the last subtitle and we've passed its start time, it might still be active until next change or song end
// This logic clears the subtitle if we've passed its end time and it's not picked up by another.
}
}
// If no subtitle is active for the current time (e.g., between cues), clear the text.
if (!foundSubtitle && currentTimeSeconds > 0) {
// Check currentTimeSeconds > 0 to avoid clearing at very start
var shouldClear = true;
// Check if we are before the first subtitle or after the last one
if (self.subtitleData.length > 0) {
if (currentTimeSeconds < self.subtitleData[0].start) {
shouldClear = true;
} else {
// Check if we are between subtitles
for (var k = 0; k < self.subtitleData.length; k++) {
if (currentTimeSeconds >= self.subtitleData[k].start && currentTimeSeconds < self.subtitleData[k].end) {
shouldClear = false; // We are in a subtitle, don't clear
break;
}
}
}
}
if (shouldClear) {
newText = "";
}
}
// Update display if text changed
if (newText !== self.currentText) {
self.currentText = newText;
self.createTextGraphics(newText);
}
};
self.setPosition = function (x, y) {
self.x = x;
self.y = y;
};
self.update = function () {
self.updateSubtitles();
};
return self;
});
/****
* Initialize Game
****/
// Facekit provides the camera feed as background, so no explicit backgroundColor needed.
var game = new LK.Game({});
/****
* Game Code
****/
// Game state variables
var isGameActive = false;
var startButtonInstance;
var maskInstance;
var mouthAnimatorInstance;
var subtitleInstance;
var songDuration = 206.72; // Song duration in seconds based on lip sync data
var musicStartTime = null;
var songCompleted = false;
// Game constants
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var AUTOMATED_VISEME_MAPPING = {
'X': 'closed',
// Silence/rest position
'A': 'AEI',
// Open vowels (father, palm)
'B': 'BMP',
// Lip closure (b, m, p)
'C': 'CDGKNRSTXYZ',
// Consonants (c, d, g, k, n, r, s, t, x, y, z)
'D': 'AEI',
// Mid vowels (day, face)
'E': 'EE',
// High front vowels (see, fleece)
'F': 'FV',
// Lip-teeth contact (f, v)
'G': 'CDGKNRSTXYZ',
// Back consonants (g, ng)
'H': 'CHJSH' // Fricatives (h, ch, j, sh)
};
function handleStartButtonPressed() {
if (isGameActive) {
return;
}
isGameActive = true;
// Remove start button
if (startButtonInstance && startButtonInstance.parent) {
startButtonInstance.destroy();
startButtonInstance = null;
}
// Create and show mask
maskInstance = new Mask();
game.addChild(maskInstance);
var maskGraphicHeight = maskInstance.maskGraphics.height;
var yOffset = -50;
var maskTargetX, maskTargetY;
if (facekit.leftEye && facekit.rightEye && facekit.leftEye.x !== 0 && facekit.rightEye.x !== 0) {
maskTargetX = (facekit.leftEye.x + facekit.rightEye.x) / 2;
maskTargetY = (facekit.leftEye.y + facekit.rightEye.y) / 2 - maskGraphicHeight * 0.20 + yOffset;
} else {
maskTargetX = GAME_WIDTH / 2;
maskTargetY = GAME_HEIGHT / 3 + yOffset;
}
// Create mouth animator
mouthAnimatorInstance = new MouthAnimator();
var mouthInitialX = facekit.mouthCenter && facekit.mouthCenter.x !== 0 ? facekit.mouthCenter.x : GAME_WIDTH / 2;
var mouthInitialY = facekit.mouthCenter && facekit.mouthCenter.y !== 0 ? facekit.mouthCenter.y : GAME_HEIGHT / 2 + 200;
mouthAnimatorInstance.x = mouthInitialX;
mouthAnimatorInstance.y = mouthInitialY;
game.addChild(mouthAnimatorInstance);
// Create subtitle display
subtitleInstance = new SubtitleDisplay();
subtitleInstance.setPosition(GAME_WIDTH / 2, GAME_HEIGHT - 124 - GAME_HEIGHT * 0.1); // Position moved up 10% from previous position
game.addChild(subtitleInstance);
// Show mask with callback to start music and lip-sync when animation completes
maskInstance.show(maskTargetX, maskTargetY, 1200);
// Start music, lip-sync, and subtitles after mask animation completes (1.2 seconds)
LK.setTimeout(function () {
LK.playMusic('batmansong');
musicStartTime = Date.now(); // Track when music started
songCompleted = false; // Reset completion flag
if (mouthAnimatorInstance) {
// Ensure mouthAnimatorInstance still exists
mouthAnimatorInstance.startLipSync();
}
if (subtitleInstance) {
// Ensure subtitleInstance still exists
subtitleInstance.startSubtitles();
}
}, 1200);
}
// Initial game setup
function initializeGameScreen() {
isGameActive = false; // Reset game state
// Create and position the start button
startButtonInstance = new StartButton();
// Center the button horizontally
startButtonInstance.x = GAME_WIDTH / 2;
// Position it towards the bottom of the screen
var buttonHeight = startButtonInstance.buttonGraphics.height;
startButtonInstance.y = GAME_HEIGHT - buttonHeight / 2 - 100; // 100px padding from bottom
game.addChild(startButtonInstance);
}
// Call initial setup
initializeGameScreen();
// Game update loop
game.update = function () {
if (isGameActive) {
// Check if song has completed and stop music
if (musicStartTime && !songCompleted) {
var currentMusicTime = (Date.now() - musicStartTime) / 1000.0;
if (currentMusicTime >= songDuration) {
LK.stopMusic();
songCompleted = true;
}
}
// Update mouth animator with enhanced tracking
if (mouthAnimatorInstance) {
// mouthAnimatorInstance.updateViseme(facekit.volume); // Viseme update is now handled by updateLipSync within MouthAnimator's update
mouthAnimatorInstance.update(); // This now includes smooth position tracking, scaling, and lip-sync
}
// Update mask with enhanced tracking
if (maskInstance) {
maskInstance.update(); // This now includes smooth position tracking and scaling
}
if (subtitleInstance) {
subtitleInstance.update();
}
}
};