Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import type { FaceSkinPalette } from 'face-skin'
import {
copyFaceState,
copyFaceStateForDistribution,
createFaceState,
type FaceState,
faceStatesEqual,
} from 'face-state'
import { createBlinkMotion } from 'motions/blink'
import { createBreathMotion } from 'motions/breath'
import { createSaccadeMotion } from 'motions/saccade'
import type { FaceMotion } from 'motions/types'
import { DogEyebrow } from 'parts/dog/eyebrow'
import { DogMouth } from 'parts/dog/mouth'
import { DogNose } from 'parts/dog/nose'
import { Eye } from 'parts/eye'
import { EyeSprite } from 'parts/image/eye-sprite'
import { MouthSprite } from 'parts/image/mouth-sprite'
import { Mouth } from 'parts/mouth'
import { RelaxedEye } from 'parts/relaxed-eye'
import { RelaxedMouth } from 'parts/relaxed-mouth'
import type {
Container as PiuContainer,
ContainerDictionary as PiuContainerDictionary,
Content as PiuContent,
} from 'piu/MC'
type TemplateCtor<TData> = {
new (behaviorData?: TData, dictionary?: PiuContainerDictionary): PiuContainer
template<TNextData>(factory: (arg: TNextData) => PiuContainerDictionary): TemplateCtor<TNextData>
}
export type FaceBaseParams = {
contents?: PiuContent[]
motions?: FaceMotion[]
intervalMs?: number
left?: number
top?: number
right?: number
bottom?: number
width?: number
height?: number
}
export type FaceTemplateCtor = TemplateCtor<FaceBaseParams>
type FaceBehaviorOptions = {
motions?: FaceMotion[]
intervalMs?: number
}
export class FaceBehavior extends Behavior {
#current: FaceState
#desired: FaceState
#lastDistributed: FaceState
#nextDistributed: FaceState
#motions: FaceMotion[]
#motionsEnabled: boolean
#baseCoordinates: { left: number; top: number }
#hasBaseCoordinates: boolean
#paused: boolean
#skinPalette: FaceSkinPalette | null
#breathPixels: number
#breathOffset: number
constructor({ motions, intervalMs }: FaceBehaviorOptions) {
super()
this.#motions = motions ?? [
createBlinkMotion({ openMin: 400, openMax: 5000, closeMin: 200, closeMax: 400 }),
createBreathMotion({ duration: 6000 }),
createSaccadeMotion({ updateMin: 300, updateMax: 2000, gain: 0.2 }),
]
this.#motionsEnabled = true
this.#current = createFaceState()
this.#desired = createFaceState()
this.#lastDistributed = createFaceState()
this.#nextDistributed = createFaceState()
this.#baseCoordinates = { left: 0, top: 0 }
this.#hasBaseCoordinates = false
this.#paused = false
this.#skinPalette = null
this.#breathPixels = 6
this.#breathOffset = 0
this.intervalMs = intervalMs ?? 33
}
intervalMs: number
onCreate(container: PiuContainer) {
container.interval = this.intervalMs
copyFaceState(this.#current, this.#desired)
this.distributeFaceSkin(container)
this.distributeFaceState(container, true)
}
onUndisplaying(container: PiuContainer) {
container.stop?.()
}
onDisplaying(container: PiuContainer) {
if (!this.#hasBaseCoordinates) {
this.captureBaseCoordinates(container)
}
if (!this.#paused && this.#motionsEnabled) {
container.start?.()
}
this.distributeFaceSkin(container)
this.distributeFaceState(container, true)
}
onFaceUpdate(container: PiuContainer, face: FaceState) {
copyFaceState(face, this.#desired)
if (!this.#motionsEnabled) {
copyFaceState(this.#desired, this.#current)
this.distributeFaceState(container)
}
}
onFaceSkin(_container: PiuContainer, palette: FaceSkinPalette) {
this.#skinPalette = palette
}
onTimeChanged(container: PiuContainer) {
if (this.#paused || !this.#motionsEnabled) {
return
}
const interval = container.interval ?? this.intervalMs
copyFaceState(this.#desired, this.#current)
for (const motion of this.#motions) {
motion(interval, this.#current)
}
if (!this.#hasBaseCoordinates) {
this.captureBaseCoordinates(container)
}
const nextBreathOffset = Math.round(this.#current.breath * this.#breathPixels)
const dy = nextBreathOffset - this.#breathOffset
if (dy !== 0) {
container.moveBy(0, dy)
this.#breathOffset = nextBreathOffset
}
this.distributeFaceState(container)
}
onTouchEnded(container: PiuContainer) {
container.bubble('onFaceTouch')
}
getBaseCoordinates(container: PiuContainer): { left: number; top: number } {
if (!this.#hasBaseCoordinates) {
this.captureBaseCoordinates(container)
}
return { ...this.#baseCoordinates }
}
rehydrate(container: PiuContainer, face: FaceState, palette?: FaceSkinPalette | null) {
copyFaceState(face, this.#current)
copyFaceState(face, this.#desired)
if (palette !== undefined) {
this.#skinPalette = palette
}
if (!this.#hasBaseCoordinates) {
this.captureBaseCoordinates(container)
}
}
pause(container: PiuContainer) {
if (this.#paused) return
this.#paused = true
container.stop?.()
container.visible = false
container.active = false
}
resume(container: PiuContainer) {
if (!this.#paused) return
this.#paused = false
container.visible = true
container.active = true
if (this.#motionsEnabled) container.start?.()
this.distributeFaceSkin(container)
this.distributeFaceState(container, true)
container.bubble('onFaceState', this.#current)
}
get breathPixels(): number {
return this.#breathPixels
}
setMotionsEnabled(container: PiuContainer, enabled: boolean) {
if (this.#motionsEnabled === enabled) return
this.#motionsEnabled = enabled
if (enabled) {
if (!this.#paused) container.start?.()
return
}
container.stop?.()
if (this.#breathOffset !== 0) {
container.moveBy(0, -this.#breathOffset)
this.#breathOffset = 0
}
copyFaceState(this.#desired, this.#current)
this.distributeFaceState(container, true)
}
private distributeFaceSkin(container: PiuContainer): void {
if (this.#skinPalette) {
container.distribute('onFaceSkin', this.#skinPalette)
container.bubble('onFaceSkin', this.#skinPalette)
}
}
private distributeFaceState(container: PiuContainer, force = false): void {
copyFaceStateForDistribution(this.#current, this.#nextDistributed, this.#breathPixels)
if (!force && faceStatesEqual(this.#nextDistributed, this.#lastDistributed)) return
copyFaceState(this.#nextDistributed, this.#lastDistributed)
container.distribute('onFaceState', this.#nextDistributed)
// container.bubble('onFaceState', this.#current)
}
private captureBaseCoordinates(container: PiuContainer): void {
const coordinates = container.coordinates
this.#baseCoordinates.left = coordinates?.left ?? 0
this.#baseCoordinates.top = coordinates?.top ?? 0
this.#hasBaseCoordinates = true
}
}
const DEFAULT_FACE_LEFT = 60
const DEFAULT_FACE_TOP = 60
const DEFAULT_FACE_WIDTH = 200
const DEFAULT_FACE_HEIGHT = 120
function resolveFaceBaseParams(data: FaceBaseParams, it?: FaceBaseParams): FaceBaseParams {
return {
...data,
...it,
contents: it?.contents ?? data.contents,
}
}
export const FaceBase: FaceTemplateCtor = Container.template(($: FaceBaseParams, it?: FaceBaseParams) => {
const params = resolveFaceBaseParams($, it)
const contents = params.contents ?? []
if (it?.contents) {
delete it.contents
}
return {
left: params.left,
right: params.right,
top: params.top,
bottom: params.bottom,
width: params.width,
height: params.height,
active: true,
contents,
Behavior: class extends FaceBehavior {
constructor() {
super({
motions: params.motions,
intervalMs: params.intervalMs,
})
}
},
}
}) as unknown as FaceTemplateCtor
export const SimpleFace: FaceTemplateCtor = FaceBase.template(($: FaceBaseParams = {}) => {
const left = $.left ?? DEFAULT_FACE_LEFT
const top = $.top ?? DEFAULT_FACE_TOP
const width = $.width ?? DEFAULT_FACE_WIDTH
const height = $.height ?? DEFAULT_FACE_HEIGHT
return {
left,
top,
width,
height,
contents: [
new Eye({ cx: 30, cy: 33, radius: 8, side: 'left' }),
new Eye({ cx: 170, cy: 36, radius: 8, side: 'right' }),
new Mouth({ cx: 100, cy: 88 }),
],
}
})
export const SmallFace: FaceTemplateCtor = FaceBase.template(($: FaceBaseParams = {}) => {
const left = $.left ?? DEFAULT_FACE_LEFT
const top = $.top ?? DEFAULT_FACE_TOP
const width = $.width ?? DEFAULT_FACE_WIDTH
const height = $.height ?? DEFAULT_FACE_HEIGHT
return {
left,
top,
width,
height,
contents: [
new Eye({ cx: 36, cy: 53, radius: 4, side: 'left' }),
new Eye({ cx: 92, cy: 54, radius: 4, side: 'right' }),
new Mouth({ cx: 64, cy: 80, minWidth: 20, maxWidth: 36, minHeight: 3, maxHeight: 23 }),
],
}
})
export const RelaxedFace: FaceTemplateCtor = FaceBase.template(($: FaceBaseParams = {}) => ({
left: $.left ?? DEFAULT_FACE_LEFT,
top: $.top ?? DEFAULT_FACE_TOP,
width: $.width ?? DEFAULT_FACE_WIDTH,
height: $.height ?? DEFAULT_FACE_HEIGHT,
motions: $.motions ?? [createBreathMotion({ duration: 6000 })],
contents: [
new RelaxedEye({ cx: 30, cy: 38, side: 'left' }),
new RelaxedEye({ cx: 170, cy: 38, side: 'right' }),
new RelaxedMouth({ cx: 100, cy: 88 }),
],
}))
export const DogFace: FaceTemplateCtor = FaceBase.template(($: FaceBaseParams = {}) => {
const left = $.left ?? DEFAULT_FACE_LEFT
const top = $.top ?? DEFAULT_FACE_TOP
const width = $.width ?? DEFAULT_FACE_WIDTH
const height = $.height ?? DEFAULT_FACE_HEIGHT
const mouthCy = 136 - DEFAULT_FACE_TOP
return {
left,
top,
width,
height,
contents: [
new Eye({ cx: 30, cy: 33, radius: 10, side: 'left', eyelidWidth: 24, eyelidHeight: 24 }),
new Eye({ cx: 170, cy: 36, radius: 10, side: 'right', eyelidWidth: 24, eyelidHeight: 24 }),
new DogEyebrow({
cx: 30,
cy: 33,
side: 'left',
canvasWidth: width,
canvasHeight: height,
}),
new DogEyebrow({
cx: 170,
cy: 36,
side: 'right',
canvasWidth: width,
canvasHeight: height,
}),
new DogMouth({
cx: 100,
cy: mouthCy,
maxWidth: 70,
maxHeight: 32,
canvasWidth: width,
canvasHeight: height,
}),
new DogNose({
cx: 100,
cy: mouthCy,
canvasWidth: width,
canvasHeight: height,
}),
],
}
})
export const ImageFace: FaceTemplateCtor = FaceBase.template(($: FaceBaseParams = {}) => {
const left = $.left ?? DEFAULT_FACE_LEFT
const top = $.top ?? DEFAULT_FACE_TOP
const width = $.width ?? DEFAULT_FACE_WIDTH
const height = $.height ?? DEFAULT_FACE_HEIGHT
return {
left,
top,
width,
height,
contents: [
new EyeSprite({ cx: 30, cy: 33, side: 'left' }),
new EyeSprite({ cx: 170, cy: 36, side: 'right' }),
new MouthSprite({ cx: 100, cy: 88 }),
],
}
})
c44ec7454This file is pinned to the selected release. Download the original to open it in your design tool.