✨(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:
committed by
aleb_the_flash
parent
1d5aebcfdc
commit
7405011cd2
BIN
src/frontend/public/assets/beret.png
Normal file
BIN
src/frontend/public/assets/beret.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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é."
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
Reference in New Issue
Block a user