(frontend) add french touch effect

Enhanced the FaceLandmarksProcessor to include
a new 'french' effect, allowing users to add
a beret image to detected faces. Updated the
EffectsConfiguration component to toggle this
effect and modified localization files to
reflect the change from 'mustache' to 'french'.
This commit is contained in:
Arnaud Robin
2025-03-31 00:46:52 +02:00
committed by aleb_the_flash
parent 1d5aebcfdc
commit 7405011cd2
8 changed files with 47 additions and 34 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -49,7 +49,7 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
// Effect images // Effect images
glassesImage?: HTMLImageElement glassesImage?: HTMLImageElement
mustacheImage?: HTMLImageElement mustacheImage?: HTMLImageElement
beretImage?: HTMLImageElement
constructor(opts: BackgroundOptions) { constructor(opts: BackgroundOptions) {
this.name = 'face_landmarks' this.name = 'face_landmarks'
this.options = opts this.options = opts
@@ -65,6 +65,10 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
this.mustacheImage = new Image() this.mustacheImage = new Image()
this.mustacheImage.src = '/assets/mustache.png' this.mustacheImage.src = '/assets/mustache.png'
this.mustacheImage.crossOrigin = 'anonymous' this.mustacheImage.crossOrigin = 'anonymous'
this.beretImage = new Image()
this.beretImage.src = '/assets/beret.png'
this.beretImage.crossOrigin = 'anonymous'
} }
static get isSupported() { static get isSupported() {
@@ -172,7 +176,8 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
rightPoint: { x: number; y: number }, rightPoint: { x: number; y: number },
image: HTMLImageElement, image: HTMLImageElement,
widthScale: number, widthScale: number,
heightScale: number heightScale: number,
yOffset: number = 0
) { ) {
// Calculate distance between points // Calculate distance between points
const distance = Math.sqrt( const distance = Math.sqrt(
@@ -186,7 +191,7 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
// Calculate center position between points // Calculate center position between points
const centerX = (leftPoint.x + rightPoint.x) / 2 const centerX = (leftPoint.x + rightPoint.x) / 2
const centerY = (leftPoint.y + rightPoint.y) / 2 const centerY = (leftPoint.y + rightPoint.y) / 2 + yOffset
// Draw image // Draw image
this.outputCanvasCtx!.save() this.outputCanvasCtx!.save()
@@ -237,21 +242,29 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
this.outputCanvasCtx!.lineWidth = 2 this.outputCanvasCtx!.lineWidth = 2
for (const face of this.faceLandmarkerResult.faceLandmarks) { for (const face of this.faceLandmarkerResult.faceLandmarks) {
// Find eye landmarks (indices 33 and 263 are the left and right eye corners) // Find eye landmarks
const leftEye = face[468] const leftEye = face[468]
const rightEye = face[473] const rightEye = face[473]
// Find mouth landmarks for mustache (indices 0 and 17 are the left and right corners of the mouth) // Find mouth landmarks for mustache
const leftMoustache = face[92] const leftMoustache = face[92]
const rightMoustache = face[322] const rightMoustache = face[322]
// Find forehead landmarks for beret
const leftForehead = face[103]
const rightForehead = face[332]
if (leftEye && rightEye && this.options.showGlasses) { if (leftEye && rightEye && this.options.showGlasses) {
this.drawEffect(leftEye, rightEye, this.glassesImage!, 2.5, 0.7) this.drawEffect(leftEye, rightEye, this.glassesImage!, 2.5, 0.7)
} }
if (leftMoustache && rightMoustache && this.options.showMustache) { if (leftMoustache && rightMoustache && this.options.showFrench) {
this.drawEffect(leftMoustache, rightMoustache, this.mustacheImage!, 1.5, 0.5) this.drawEffect(leftMoustache, rightMoustache, this.mustacheImage!, 1.5, 0.5)
} }
if (leftForehead && rightForehead && this.options.showFrench) {
this.drawEffect(leftForehead, rightForehead, this.beretImage!, 2.1, 0.7, -0.1)
}
} }
} }

View File

@@ -9,7 +9,7 @@ export type BackgroundOptions = {
blurRadius?: number blurRadius?: number
imagePath?: string imagePath?: string
showGlasses?: boolean showGlasses?: boolean
showMustache?: boolean showFrench?: boolean
} }
export interface ProcessorSerialized { export interface ProcessorSerialized {

View File

@@ -15,7 +15,7 @@ import { BlurOnStrong } from '@/components/icons/BlurOnStrong'
import { useTrackToggle } from '@livekit/components-react' import { useTrackToggle } from '@livekit/components-react'
import { Loader } from '@/primitives/Loader' import { Loader } from '@/primitives/Loader'
import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay' import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay'
import { RiProhibited2Line, RiGlassesLine, RiEmotionLine } from '@remixicon/react' import { RiProhibited2Line, RiGlassesLine, RiGoblet2Fill } from '@remixicon/react'
import { useHasFaceLandmarksAccess } from '../../hooks/useHasFaceLandmarksAccess' import { useHasFaceLandmarksAccess } from '../../hooks/useHasFaceLandmarksAccess'
enum BlurRadius { enum BlurRadius {
@@ -142,7 +142,7 @@ export const EffectsConfiguration = ({
const tooltipLabel = (type: ProcessorType, options: BackgroundOptions) => { const tooltipLabel = (type: ProcessorType, options: BackgroundOptions) => {
if (type === ProcessorType.FACE_LANDMARKS) { if (type === ProcessorType.FACE_LANDMARKS) {
const effect = options.showGlasses ? 'glasses' : 'mustache' const effect = options.showGlasses ? 'glasses' : 'french'
return t(`faceLandmarks.${effect}.${isSelected(type, options) ? 'clear' : 'apply'}`) return t(`faceLandmarks.${effect}.${isSelected(type, options) ? 'clear' : 'apply'}`)
} }
return t(`${type}.${isSelected(type, options) ? 'clear' : 'apply'}`) return t(`${type}.${isSelected(type, options) ? 'clear' : 'apply'}`)
@@ -151,19 +151,19 @@ export const EffectsConfiguration = ({
const getFaceLandmarksOptions = () => { const getFaceLandmarksOptions = () => {
const processor = getProcessor() const processor = getProcessor()
if (processor?.serialize().type === ProcessorType.FACE_LANDMARKS) { if (processor?.serialize().type === ProcessorType.FACE_LANDMARKS) {
return processor.serialize().options as { showGlasses?: boolean; showMustache?: boolean } return processor.serialize().options as { showGlasses?: boolean; showFrench?: boolean }
} }
return { showGlasses: false, showMustache: false } return { showGlasses: false, showFrench: false }
} }
const toggleFaceLandmarkEffect = async (effect: 'glasses' | 'mustache') => { const toggleFaceLandmarkEffect = async (effect: 'glasses' | 'french') => {
const currentOptions = getFaceLandmarksOptions() const currentOptions = getFaceLandmarksOptions()
const newOptions = { const newOptions = {
...currentOptions, ...currentOptions,
[effect === 'glasses' ? 'showGlasses' : 'showMustache']: !currentOptions[effect === 'glasses' ? 'showGlasses' : 'showMustache'] [effect === 'glasses' ? 'showGlasses' : 'showFrench']: !currentOptions[effect === 'glasses' ? 'showGlasses' : 'showFrench']
} }
if (!newOptions.showGlasses && !newOptions.showMustache) { if (!newOptions.showGlasses && !newOptions.showFrench) {
// If both effects are off stop the processor // If both effects are off stop the processor
await clearEffect() await clearEffect()
} else { } else {
@@ -356,11 +356,11 @@ export const EffectsConfiguration = ({
variant="bigSquare" variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, { aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: true, showGlasses: true,
showMustache: false, showFrench: false,
})} })}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, { tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: true, showGlasses: true,
showMustache: false, showFrench: false,
})} })}
isDisabled={processorPendingReveal} isDisabled={processorPendingReveal}
onChange={async () => await toggleFaceLandmarkEffect('glasses')} onChange={async () => await toggleFaceLandmarkEffect('glasses')}
@@ -373,18 +373,18 @@ export const EffectsConfiguration = ({
variant="bigSquare" variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, { aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false, showGlasses: false,
showMustache: true, showFrench: true,
})} })}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, { tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false, showGlasses: false,
showMustache: true, showFrench: true,
})} })}
isDisabled={processorPendingReveal} isDisabled={processorPendingReveal}
onChange={async () => await toggleFaceLandmarkEffect('mustache')} onChange={async () => await toggleFaceLandmarkEffect('french')}
isSelected={getFaceLandmarksOptions().showMustache} isSelected={getFaceLandmarksOptions().showFrench}
data-attr="toggle-mustache" data-attr="toggle-french"
> >
<RiEmotionLine /> <RiGoblet2Fill />
</ToggleButton> </ToggleButton>
</div> </div>
</div> </div>

View File

@@ -156,9 +156,9 @@
"apply": "Brille hinzufügen", "apply": "Brille hinzufügen",
"clear": "Brille entfernen" "clear": "Brille entfernen"
}, },
"mustache": { "french": {
"apply": "Schnurrbart hinzufügen", "apply": "Französische Touch hinzufügen",
"clear": "Schnurrbart entfernen" "clear": "Französische Touch entfernen"
} }
}, },
"experimental": "Experimentelle Funktion. Eine v2 kommt für vollständige Browserunterstützung und verbesserte Qualität." "experimental": "Experimentelle Funktion. Eine v2 kommt für vollständige Browserunterstützung und verbesserte Qualität."

View File

@@ -155,9 +155,9 @@
"apply": "Add Glasses", "apply": "Add Glasses",
"clear": "Remove Glasses" "clear": "Remove Glasses"
}, },
"mustache": { "french": {
"apply": "Add Mustache", "apply": "Add French touch",
"clear": "Remove Mustache" "clear": "Remove French touch"
} }
}, },
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality." "experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."

View File

@@ -155,9 +155,9 @@
"apply": "Ajouter des lunettes", "apply": "Ajouter des lunettes",
"clear": "Retirer les lunettes" "clear": "Retirer les lunettes"
}, },
"mustache": { "french": {
"apply": "Ajouter une moustache", "apply": "Ajouter la touche française",
"clear": "Retirer la moustache" "clear": "Retirer la touche française"
} }
}, },
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet des navigateurs et une meilleure qualité." "experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet des navigateurs et une meilleure qualité."

View File

@@ -155,9 +155,9 @@
"apply": "Bril toevoegen", "apply": "Bril toevoegen",
"clear": "Bril verwijderen" "clear": "Bril verwijderen"
}, },
"mustache": { "french": {
"apply": "Snor toevoegen", "apply": "Franse stijl toevoegen",
"clear": "Snor verwijderen" "clear": "Franse stijl verwijderen"
} }
}, },
"experimental": "Experimentele functie. Een v2 komt eraan voor volledige browserondersteuning en verbeterde kwaliteit." "experimental": "Experimentele functie. Een v2 komt eraan voor volledige browserondersteuning en verbeterde kwaliteit."