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.5 }); self.animationFinished = false; // Flag to track if the intro animation is done // Face tracking variables - much more responsive settings var targetX = GAME_WIDTH / 2; var targetY = GAME_HEIGHT / 3; var smoothingFactor = 0.25; // Much more responsive than 0.12 var prevX = null; var prevY = null; // Rotation tracking variables - more aggressive like DragonHead var targetTilt = 0; var tiltSmoothingFactor = 0.2; // More responsive than 0.11 var tiltScaleFactor = 0.15; // Much higher than my 0.05 // Scale tracking variables - more dynamic var scaleHistory = new Array(3).fill(1); // Shorter history for faster response var scaleIndex = 0; // Function to calculate face tilt (copied directly from working 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)); // Use same scaling as working example return Math.max(-15, Math.min(15, angle * 0.15)); } return 0; } 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; } }); }; // Much more responsive tracking based on DragonHead example self.updateFaceTracking = function () { if (!self.animationFinished) { return; } // Position tracking - direct from working example pattern if (facekit.noseTip) { targetX = facekit.noseTip.x; var maskGraphicHeight = self.maskGraphics.height; var yOffset = -50; // Keep existing offset targetY = facekit.noseTip.y - maskGraphicHeight * 0.20 + yOffset; // Initialize previous positions if not set if (prevX === null) { prevX = targetX; prevY = targetY; } // Much more responsive smoothing 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; } // Rotation tracking - copied from working example approach if (facekit.leftEye && facekit.rightEye) { targetTilt = calculateFaceTilt() * tiltScaleFactor; // Keep in degrees like the working example targetTilt = Math.max(-15, Math.min(15, targetTilt)); // Convert to radians for rotation and apply smoothing var targetTiltRad = targetTilt * (Math.PI / 180); var currentTiltRad = self.rotation; self.rotation = currentTiltRad + (targetTiltRad - currentTiltRad) * tiltSmoothingFactor; } // Scale tracking - much more dynamic range and faster response if (facekit.leftEye && facekit.rightEye) { var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x); var newScale = eyeDistance / 200; // More sensitive divisor // Shorter rolling average for faster response scaleHistory[scaleIndex] = newScale; scaleIndex = (scaleIndex + 1) % scaleHistory.length; var avgScale = scaleHistory.reduce(function (a, b) { return a + b; }, 0) / scaleHistory.length; // Much wider scale range and faster blending var targetScale = Math.max(0.5, Math.min(2.0, avgScale)); self.maskGraphics.scaleX = self.maskGraphics.scaleX * 0.7 + targetScale * 0.3; self.maskGraphics.scaleY = self.maskGraphics.scaleY * 0.7 + targetScale * 0.3; } }; 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; } // Much more responsive mouth tracking self.mouthOpenness = 0; self.mouthSmoothingSpeed = 0.3; // Much faster response // Position tracking - more responsive var targetX = GAME_WIDTH / 2; var targetY = GAME_HEIGHT / 2 + 200; var positionSmoothingFactor = 0.25; // Much more responsive var prevX = null; var prevY = null; // Scale tracking - faster response var scaleHistory = new Array(3).fill(1); // Shorter history var scaleIndex = 0; self.updateViseme = function (volume) { // More responsive mouth state detection var targetMouthState = 0; if (facekit && facekit.mouthOpen) { targetMouthState = 1; } // Faster mouth transitions self.mouthOpenness += (targetMouthState - self.mouthOpenness) * self.mouthSmoothingSpeed; // Convert to viseme index var newIndex = Math.floor(self.mouthOpenness * 3.99); newIndex = Math.max(0, Math.min(3, newIndex)); 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; } } }; self.updateFaceTracking = function () { // Much more responsive position tracking if (facekit.mouthCenter && (facekit.mouthCenter.x !== 0 || facekit.mouthCenter.y !== 0)) { targetX = facekit.mouthCenter.x; targetY = facekit.mouthCenter.y; if (prevX === null) { prevX = targetX; prevY = targetY; } // Faster position updates var newX = prevX * (1 - positionSmoothingFactor) + targetX * positionSmoothingFactor; var newY = prevY * (1 - positionSmoothingFactor) + targetY * positionSmoothingFactor; self.x = newX; self.y = newY; prevX = newX; prevY = newY; } else { // Fallback if (prevX !== null) { self.x = prevX; self.y = prevY; } else { self.x = GAME_WIDTH / 2; self.y = GAME_HEIGHT / 2 + 200; } } // Much more dynamic scale tracking if (facekit.leftEye && facekit.rightEye) { var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x); var newScale = eyeDistance / 180; // More sensitive scaleHistory[scaleIndex] = newScale; scaleIndex = (scaleIndex + 1) % scaleHistory.length; var avgScale = scaleHistory.reduce(function (a, b) { return a + b; }, 0) / scaleHistory.length; // Much wider range and faster blending var targetScale = Math.max(0.4, Math.min(2.5, avgScale)); for (var i = 0; i < self.visemes.length; i++) { self.visemes[i].scaleX = self.visemes[i].scaleX * 0.6 + targetScale * 0.4; self.visemes[i].scaleY = self.visemes[i].scaleY * 0.6 + targetScale * 0.4; } } // Update viseme 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.
===================================================================
--- original.js
+++ change.js
@@ -14,22 +14,22 @@
anchorX: 0.5,
anchorY: 0.5
});
self.animationFinished = false; // Flag to track if the intro animation is done
- // Face tracking variables for smooth movement
+ // Face tracking variables - much more responsive settings
var targetX = GAME_WIDTH / 2;
var targetY = GAME_HEIGHT / 3;
- var smoothingFactor = 0.12;
+ var smoothingFactor = 0.25; // Much more responsive than 0.12
var prevX = null;
var prevY = null;
- // Rotation tracking variables
+ // Rotation tracking variables - more aggressive like DragonHead
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 tiltSmoothingFactor = 0.2; // More responsive than 0.11
+ var tiltScaleFactor = 0.15; // Much higher than my 0.05
+ // Scale tracking variables - more dynamic
+ var scaleHistory = new Array(3).fill(1); // Shorter history for faster response
var scaleIndex = 0;
- // Function to calculate face tilt (adapted from face tracking example)
+ // Function to calculate face tilt (copied directly from working example)
function calculateFaceTilt() {
if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) {
// Calculate midpoint between eyes
var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2;
@@ -37,85 +37,75 @@
// 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));
+ // Use same scaling as working example
+ return Math.max(-15, Math.min(15, angle * 0.15));
}
- return 0; // Default to straight when face points aren't available
+ return 0;
}
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
+ self.y = -self.maskGraphics.height / 2 - 50;
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
+ self.animationFinished = true;
}
});
};
- // New smooth update method for face tracking
+ // Much more responsive tracking based on DragonHead example
self.updateFaceTracking = function () {
if (!self.animationFinished) {
- return; // Don't track until animation is finished
+ return;
}
- // Smooth position tracking
- if (facekit.noseTip && facekit.noseTip.x !== 0) {
+ // Position tracking - direct from working example pattern
+ if (facekit.noseTip) {
targetX = facekit.noseTip.x;
var maskGraphicHeight = self.maskGraphics.height;
- var yOffset = -50; // Keep the existing Y offset
+ var yOffset = -50; // Keep existing 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
+ // Much more responsive smoothing
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
+ // Rotation tracking - copied from working example approach
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
+ // Keep in degrees like the working example
+ targetTilt = Math.max(-15, Math.min(15, targetTilt));
+ // Convert to radians for rotation and apply smoothing
var targetTiltRad = targetTilt * (Math.PI / 180);
- self.rotation += (targetTiltRad - self.rotation) * tiltSmoothingFactor;
+ var currentTiltRad = self.rotation;
+ self.rotation = currentTiltRad + (targetTiltRad - currentTiltRad) * tiltSmoothingFactor;
}
- // Scale tracking
+ // Scale tracking - much more dynamic range and faster response
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
+ var newScale = eyeDistance / 200; // More sensitive divisor
+ // Shorter rolling average for faster response
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;
+ // Much wider scale range and faster blending
+ var targetScale = Math.max(0.5, Math.min(2.0, avgScale));
+ self.maskGraphics.scaleX = self.maskGraphics.scaleX * 0.7 + targetScale * 0.3;
+ self.maskGraphics.scaleY = self.maskGraphics.scaleY * 0.7 + targetScale * 0.3;
}
};
return self;
});
@@ -142,89 +132,82 @@
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
+ // Much more responsive mouth tracking
+ self.mouthOpenness = 0;
+ self.mouthSmoothingSpeed = 0.3; // Much faster response
+ // Position tracking - more responsive
var targetX = GAME_WIDTH / 2;
var targetY = GAME_HEIGHT / 2 + 200;
- var positionSmoothingFactor = 0.12;
+ var positionSmoothingFactor = 0.25; // Much more responsive
var prevX = null;
var prevY = null;
- // Scale tracking variables
- var scaleHistory = new Array(5).fill(1);
+ // Scale tracking - faster response
+ var scaleHistory = new Array(3).fill(1); // Shorter history
var scaleIndex = 0;
self.updateViseme = function (volume) {
- // Use face tracking for mouth state instead of volume
+ // More responsive mouth state detection
var targetMouthState = 0;
if (facekit && facekit.mouthOpen) {
targetMouthState = 1;
}
- // Smoothly transition mouth state
+ // Faster mouth transitions
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
+ // Convert to viseme index
+ var newIndex = Math.floor(self.mouthOpenness * 3.99);
+ newIndex = Math.max(0, Math.min(3, newIndex));
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
+ // Much more responsive 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
+ // Faster position updates
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
+ // Fallback
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
+ // Much more dynamic scale tracking
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
+ var newScale = eyeDistance / 180; // More sensitive
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
+ // Much wider range and faster blending
+ var targetScale = Math.max(0.4, Math.min(2.5, avgScale));
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;
+ self.visemes[i].scaleX = self.visemes[i].scaleX * 0.6 + targetScale * 0.4;
+ self.visemes[i].scaleY = self.visemes[i].scaleY * 0.6 + targetScale * 0.4;
}
}
- // Update viseme based on face tracking (call this after position/scale updates)
+ // Update viseme
self.updateViseme(facekit.volume);
};
return self;
});
@@ -253,16 +236,8 @@
/****
* Game Code
****/
// Game state variables
-// Initialize assets used in this game.
-// Button Image
-// Mask Image
-// Mouth Viseme Images
-// Slightly more open
-// Medium open
-// Large open
-// Music
var isGameActive = false;
var startButtonInstance;
var maskInstance;
var mouthAnimatorInstance;