(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
glassesImage?: HTMLImageElement
mustacheImage?: HTMLImageElement
beretImage?: HTMLImageElement
constructor(opts: BackgroundOptions) {
this.name = 'face_landmarks'
this.options = opts
@@ -65,6 +65,10 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
this.mustacheImage = new Image()
this.mustacheImage.src = '/assets/mustache.png'
this.mustacheImage.crossOrigin = 'anonymous'
this.beretImage = new Image()
this.beretImage.src = '/assets/beret.png'
this.beretImage.crossOrigin = 'anonymous'
}
static get isSupported() {
@@ -172,7 +176,8 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
rightPoint: { x: number; y: number },
image: HTMLImageElement,
widthScale: number,
heightScale: number
heightScale: number,
yOffset: number = 0
) {
// Calculate distance between points
const distance = Math.sqrt(
@@ -186,7 +191,7 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
// Calculate center position between points
const centerX = (leftPoint.x + rightPoint.x) / 2
const centerY = (leftPoint.y + rightPoint.y) / 2
const centerY = (leftPoint.y + rightPoint.y) / 2 + yOffset
// Draw image
this.outputCanvasCtx!.save()
@@ -237,21 +242,29 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
this.outputCanvasCtx!.lineWidth = 2
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 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 rightMoustache = face[322]
// Find forehead landmarks for beret
const leftForehead = face[103]
const rightForehead = face[332]
if (leftEye && rightEye && this.options.showGlasses) {
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)
}
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
imagePath?: string
showGlasses?: boolean
showMustache?: boolean
showFrench?: boolean
}
export interface ProcessorSerialized {

View File

@@ -15,7 +15,7 @@ import { BlurOnStrong } from '@/components/icons/BlurOnStrong'
import { useTrackToggle } from '@livekit/components-react'
import { Loader } from '@/primitives/Loader'
import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay'
import { RiProhibited2Line, RiGlassesLine, RiEmotionLine } from '@remixicon/react'
import { RiProhibited2Line, RiGlassesLine, RiGoblet2Fill } from '@remixicon/react'
import { useHasFaceLandmarksAccess } from '../../hooks/useHasFaceLandmarksAccess'
enum BlurRadius {
@@ -142,7 +142,7 @@ export const EffectsConfiguration = ({
const tooltipLabel = (type: ProcessorType, options: BackgroundOptions) => {
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(`${type}.${isSelected(type, options) ? 'clear' : 'apply'}`)
@@ -151,19 +151,19 @@ export const EffectsConfiguration = ({
const getFaceLandmarksOptions = () => {
const processor = getProcessor()
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 newOptions = {
...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
await clearEffect()
} else {
@@ -356,11 +356,11 @@ export const EffectsConfiguration = ({
variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: true,
showMustache: false,
showFrench: false,
})}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: true,
showMustache: false,
showFrench: false,
})}
isDisabled={processorPendingReveal}
onChange={async () => await toggleFaceLandmarkEffect('glasses')}
@@ -373,18 +373,18 @@ export const EffectsConfiguration = ({
variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false,
showMustache: true,
showFrench: true,
})}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false,
showMustache: true,
showFrench: true,
})}
isDisabled={processorPendingReveal}
onChange={async () => await toggleFaceLandmarkEffect('mustache')}
isSelected={getFaceLandmarksOptions().showMustache}
data-attr="toggle-mustache"
onChange={async () => await toggleFaceLandmarkEffect('french')}
isSelected={getFaceLandmarksOptions().showFrench}
data-attr="toggle-french"
>
<RiEmotionLine />
<RiGoblet2Fill />
</ToggleButton>
</div>
</div>

View File

@@ -156,9 +156,9 @@
"apply": "Brille hinzufügen",
"clear": "Brille entfernen"
},
"mustache": {
"apply": "Schnurrbart hinzufügen",
"clear": "Schnurrbart entfernen"
"french": {
"apply": "Französische Touch hinzufügen",
"clear": "Französische Touch entfernen"
}
},
"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",
"clear": "Remove Glasses"
},
"mustache": {
"apply": "Add Mustache",
"clear": "Remove Mustache"
"french": {
"apply": "Add French touch",
"clear": "Remove French touch"
}
},
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."

View File

@@ -155,9 +155,9 @@
"apply": "Ajouter des lunettes",
"clear": "Retirer les lunettes"
},
"mustache": {
"apply": "Ajouter une moustache",
"clear": "Retirer la moustache"
"french": {
"apply": "Ajouter la touche française",
"clear": "Retirer la touche française"
}
},
"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",
"clear": "Bril verwijderen"
},
"mustache": {
"apply": "Snor toevoegen",
"clear": "Snor verwijderen"
"french": {
"apply": "Franse stijl toevoegen",
"clear": "Franse stijl verwijderen"
}
},
"experimental": "Experimentele functie. Een v2 komt eraan voor volledige browserondersteuning en verbeterde kwaliteit."