feat: add filled lock avatar
This commit is contained in:
parent
edc7a6004d
commit
e73ce55fb6
@ -110,6 +110,7 @@ describe('@bible-strong/avatar-core', () => {
|
||||
'trapezoid',
|
||||
'frustum',
|
||||
'half-cone',
|
||||
'lock',
|
||||
]
|
||||
const neutral = poseFromExpression(expressionFromDefinition('neutral', expression))
|
||||
|
||||
|
||||
@ -83,7 +83,8 @@
|
||||
"half-cone",
|
||||
"diamond",
|
||||
"ghost",
|
||||
"apple-bite"
|
||||
"apple-bite",
|
||||
"lock"
|
||||
]
|
||||
},
|
||||
"width": { "$ref": "#/$defs/dimension" },
|
||||
|
||||
@ -45,7 +45,10 @@ export type SurfaceDefinition<TType extends SurfaceType = SurfaceType> = {
|
||||
cutAngle?: number
|
||||
}
|
||||
|
||||
export type BodyNodeSurfaceType = Exclude<SurfaceType, 'mickey' | 'cursor' | 'ghost' | 'apple-bite'>
|
||||
export type BodyNodeSurfaceType = Exclude<
|
||||
SurfaceType,
|
||||
'mickey' | 'cursor' | 'ghost' | 'apple-bite' | 'lock'
|
||||
>
|
||||
export type PrimarySurfaceDefinition = SurfaceDefinition<SurfaceType>
|
||||
export type BodyNodeSurfaceDefinition = SurfaceDefinition<BodyNodeSurfaceType>
|
||||
|
||||
|
||||
@ -1345,6 +1345,56 @@ const projectedAppleBitePath = (pose: AvatarPose, surface: SurfaceConfig) => {
|
||||
].join('')
|
||||
}
|
||||
|
||||
// Adapted from the supplied 24px lock icon. The second, reverse-wound subpath
|
||||
// keeps the shackle opening transparent while the character body stays filled.
|
||||
const projectedLockPath = (pose: AvatarPose, surface: SurfaceConfig) => {
|
||||
const halfWidth = surface.width / 2
|
||||
const halfHeight = surface.height / 2
|
||||
const point = (x: number, y: number) => projectLocalPoint(pose, [x, y, 0])
|
||||
const format = ([x, y]: Point3) => `${x.toFixed(2)} ${y.toFixed(2)}`
|
||||
const move = (x: number, y: number) => `M${format(point(x, y))}`
|
||||
const line = (x: number, y: number) => `L${format(point(x, y))}`
|
||||
const cubicTo = (
|
||||
firstX: number,
|
||||
firstY: number,
|
||||
secondX: number,
|
||||
secondY: number,
|
||||
endX: number,
|
||||
endY: number
|
||||
) =>
|
||||
`C${format(point(firstX, firstY))} ${format(point(secondX, secondY))} ${format(point(endX, endY))}`
|
||||
|
||||
const x = (iconX: number) => ((iconX - 12) / 10) * halfWidth
|
||||
const y = (iconY: number) => ((iconY - 12) / 10) * halfHeight
|
||||
|
||||
return [
|
||||
// Outer silhouette: shackle and rounded lock body as one continuous shape.
|
||||
move(x(7), y(9)),
|
||||
line(x(7), y(7)),
|
||||
cubicTo(x(7), y(4.24), x(9.24), y(2), x(12), y(2)),
|
||||
cubicTo(x(14.76), y(2), x(17), y(4.24), x(17), y(7)),
|
||||
line(x(17), y(9)),
|
||||
line(x(18), y(9)),
|
||||
cubicTo(x(20.21), y(9), x(22), y(10.79), x(22), y(13)),
|
||||
line(x(22), y(18)),
|
||||
cubicTo(x(22), y(20.21), x(20.21), y(22), x(18), y(22)),
|
||||
line(x(6), y(22)),
|
||||
cubicTo(x(3.79), y(22), x(2), y(20.21), x(2), y(18)),
|
||||
line(x(2), y(13)),
|
||||
cubicTo(x(2), y(10.79), x(3.79), y(9), x(6), y(9)),
|
||||
line(x(7), y(9)),
|
||||
'Z',
|
||||
// Inner shackle opening, intentionally wound in the opposite direction.
|
||||
move(x(9), y(9)),
|
||||
line(x(15), y(9)),
|
||||
line(x(15), y(7)),
|
||||
cubicTo(x(15), y(5.34), x(13.66), y(4), x(12), y(4)),
|
||||
cubicTo(x(10.34), y(4), x(9), y(5.34), x(9), y(7)),
|
||||
line(x(9), y(9)),
|
||||
'Z',
|
||||
].join('')
|
||||
}
|
||||
|
||||
const headPath = (pose: AvatarPose, surface: SurfaceConfig) => {
|
||||
if (surface.type === 'sphere' || surface.type === 'mickey') {
|
||||
const exactPath = projectedEllipsoidPath(pose, surface)
|
||||
@ -1363,6 +1413,7 @@ const headPath = (pose: AvatarPose, surface: SurfaceConfig) => {
|
||||
if (surface.type === 'diamond') return projectedDiamondPath(pose, surface)
|
||||
if (surface.type === 'ghost') return projectedGhostPath(pose, surface)
|
||||
if (surface.type === 'apple-bite') return projectedAppleBitePath(pose, surface)
|
||||
if (surface.type === 'lock') return projectedLockPath(pose, surface)
|
||||
|
||||
const key = surfaceCacheKey(surface)
|
||||
let localSamples = headSamplesCache.get(key)
|
||||
|
||||
@ -18,6 +18,7 @@ export type SurfaceType =
|
||||
| 'diamond'
|
||||
| 'ghost'
|
||||
| 'apple-bite'
|
||||
| 'lock'
|
||||
|
||||
export type SurfaceConfig = {
|
||||
type: SurfaceType
|
||||
@ -91,6 +92,7 @@ export const surfacePresets: Record<SurfaceType, SurfaceConfig> = {
|
||||
diamond: { type: 'diamond', width: 235, height: 260, depth: 215, roundness: 0 },
|
||||
ghost: { type: 'ghost', width: 250, height: 245, depth: 190, roundness: 1 },
|
||||
'apple-bite': { type: 'apple-bite', width: 240, height: 250, depth: 190, roundness: 1 },
|
||||
lock: { type: 'lock', width: 250, height: 250, depth: 190, roundness: 1 },
|
||||
}
|
||||
|
||||
export const surfaceLabels: Record<SurfaceType, string> = {
|
||||
@ -111,6 +113,7 @@ export const surfaceLabels: Record<SurfaceType, string> = {
|
||||
diamond: 'Diamant',
|
||||
ghost: 'Ghost',
|
||||
'apple-bite': 'Apple Bite',
|
||||
lock: 'Lock',
|
||||
}
|
||||
|
||||
const signedPower = (value: number, exponent: number) =>
|
||||
@ -482,6 +485,7 @@ export const surfacePointAt = (
|
||||
case 'capsule':
|
||||
case 'ghost':
|
||||
case 'apple-bite':
|
||||
case 'lock':
|
||||
return capsule(config, longitude, latitude)
|
||||
case 'cone': {
|
||||
const progress = (latitude + Math.PI / 2) / Math.PI
|
||||
@ -751,7 +755,8 @@ export const surfaceFrontSampleAt = (
|
||||
|
||||
case 'capsule':
|
||||
case 'ghost':
|
||||
case 'apple-bite': {
|
||||
case 'apple-bite':
|
||||
case 'lock': {
|
||||
const capRadiusY = Math.min(radiusX, radiusY)
|
||||
const straightHalf = Math.max(0, radiusY - capRadiusY)
|
||||
const capCenterY = y < -straightHalf ? -straightHalf : y > straightHalf ? straightHalf : y
|
||||
|
||||
@ -155,6 +155,7 @@ const bundledAvatarColorMigrations: Record<string, { from: AvatarColors; to: Ava
|
||||
export const simpleBundledAvatarIds = [
|
||||
'primitive-ghost',
|
||||
'primitive-apple-bite',
|
||||
'primitive-lock',
|
||||
'oneworks-cat',
|
||||
'oneworks-cat-siamese',
|
||||
'oneworks-cat-british-shorthair',
|
||||
@ -224,6 +225,7 @@ const primitiveBundledAvatarSpecs = [
|
||||
['primitive-diamond', 'Diamond', 'diamond', deslopAccentColors.pink, 1],
|
||||
['primitive-apple-bite', 'Apple Bite', 'apple-bite', deslopAccentColors.red, 0.9],
|
||||
['primitive-ghost', 'Memento', 'ghost', deslopAccentColors.orange, 0.9],
|
||||
['primitive-lock', 'Lock', 'lock', deslopAccentColors.blue, 0.9],
|
||||
] as const
|
||||
|
||||
const fixedSkinColorBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehaviorLibrary => {
|
||||
@ -815,7 +817,10 @@ const primitiveBundledAvatars: StudioAvatar[] = [
|
||||
},
|
||||
colors: {
|
||||
body: color,
|
||||
eyes: surface === 'ghost' || surface === 'apple-bite' ? filledAvatarEyeColor : color,
|
||||
eyes:
|
||||
surface === 'ghost' || surface === 'apple-bite' || surface === 'lock'
|
||||
? filledAvatarEyeColor
|
||||
: color,
|
||||
},
|
||||
eyes:
|
||||
surface === 'ghost'
|
||||
@ -830,16 +835,24 @@ const primitiveBundledAvatars: StudioAvatar[] = [
|
||||
positionXLeft: defaultAvatarEyes.positionXLeft - 18,
|
||||
positionXRight: defaultAvatarEyes.positionXRight - 18,
|
||||
}
|
||||
: { ...defaultAvatarEyes },
|
||||
: surface === 'lock'
|
||||
? {
|
||||
...defaultAvatarEyes,
|
||||
positionYLeft: defaultAvatarEyes.positionYLeft + 22,
|
||||
positionYRight: defaultAvatarEyes.positionYRight + 22,
|
||||
}
|
||||
: { ...defaultAvatarEyes },
|
||||
renderStyle:
|
||||
surface === 'ghost' || surface === 'apple-bite'
|
||||
surface === 'ghost' || surface === 'apple-bite' || surface === 'lock'
|
||||
? { ...filledAvatarRenderStyle }
|
||||
: { ...defaultAvatarRenderStyle },
|
||||
...(surface === 'ghost'
|
||||
? { behavior: mementoBehavior() }
|
||||
: surface === 'apple-bite'
|
||||
? { behavior: fixedSkinColorBehavior() }
|
||||
: {}),
|
||||
: surface === 'lock'
|
||||
? { behavior: fixedSkinColorBehavior() }
|
||||
: {}),
|
||||
})),
|
||||
mementoPigletAvatar,
|
||||
{
|
||||
@ -919,11 +932,13 @@ const filledBundledAvatarIds = new Set([
|
||||
...Object.keys(mementoAnimalColors),
|
||||
'primitive-ghost',
|
||||
'primitive-apple-bite',
|
||||
'primitive-lock',
|
||||
])
|
||||
const oneWorksOriginalAnimalIds = new Set(Object.keys(oneWorksBundledAnimalColors))
|
||||
const filledPrimitiveAvatarColors: Record<string, AvatarColors> = {
|
||||
'primitive-ghost': { body: deslopAccentColors.orange, eyes: filledAvatarEyeColor },
|
||||
'primitive-apple-bite': { body: deslopAccentColors.red, eyes: filledAvatarEyeColor },
|
||||
'primitive-lock': { body: deslopAccentColors.blue, eyes: filledAvatarEyeColor },
|
||||
}
|
||||
|
||||
const clonePrimitiveBundledAvatar = (avatar: StudioAvatar): StudioAvatar => ({
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -31,6 +31,7 @@ describe('Studio document', () => {
|
||||
const bundledAvatarNames = [
|
||||
'Memento',
|
||||
'Apple Bite',
|
||||
'Lock',
|
||||
'Siamese',
|
||||
'British Shorthair',
|
||||
'Russian Blue',
|
||||
@ -78,9 +79,10 @@ describe('Studio document', () => {
|
||||
.filter(avatar => avatar.renderStyle.type !== 'vector' || !avatar.renderStyle.filled)
|
||||
.every(avatar => avatar.colors.body === avatar.colors.eyes)
|
||||
).toBe(true)
|
||||
expect(document.library.avatars.slice(0, 12).map(avatar => avatar.body.primary.type)).toEqual([
|
||||
expect(document.library.avatars.slice(0, 13).map(avatar => avatar.body.primary.type)).toEqual([
|
||||
'ghost',
|
||||
'apple-bite',
|
||||
'lock',
|
||||
'ellipse',
|
||||
'ellipse',
|
||||
'ellipse',
|
||||
@ -107,9 +109,7 @@ describe('Studio document', () => {
|
||||
expect(
|
||||
document.library.avatars
|
||||
.filter(avatar =>
|
||||
['dog', 'dog-brown', 'bear', 'rabbit', 'bun'].some(
|
||||
id => avatar.id === `oneworks-${id}`
|
||||
)
|
||||
['dog', 'dog-brown', 'bear', 'rabbit', 'bun'].some(id => avatar.id === `oneworks-${id}`)
|
||||
)
|
||||
.every(avatar => avatar.renderStyle.type === 'vector' && avatar.renderStyle.filled === true)
|
||||
).toBe(true)
|
||||
@ -122,12 +122,18 @@ describe('Studio document', () => {
|
||||
expect(
|
||||
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.renderStyle
|
||||
).toEqual({ type: 'vector', filled: true })
|
||||
expect(
|
||||
document.library.avatars.find(avatar => avatar.id === 'primitive-lock')?.renderStyle
|
||||
).toEqual({ type: 'vector', filled: true })
|
||||
expect(
|
||||
document.library.avatars.find(avatar => avatar.id === 'primitive-ghost')?.body.primary
|
||||
).toMatchObject({ width: 225, height: 220.5, depth: 171 })
|
||||
expect(
|
||||
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.body.primary
|
||||
).toMatchObject({ width: 216, height: 225, depth: 171 })
|
||||
expect(
|
||||
document.library.avatars.find(avatar => avatar.id === 'primitive-lock')?.body.primary
|
||||
).toMatchObject({ type: 'lock', width: 225, height: 225, depth: 171 })
|
||||
expect(
|
||||
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.eyes
|
||||
.positionXLeft
|
||||
@ -199,6 +205,12 @@ describe('Studio document', () => {
|
||||
expect(
|
||||
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.colors
|
||||
).toEqual({ body: '#ff5558', eyes: '#111316' })
|
||||
expect(document.library.avatars.find(avatar => avatar.id === 'primitive-lock')?.colors).toEqual(
|
||||
{ body: '#1a9cff', eyes: '#111316' }
|
||||
)
|
||||
expect(
|
||||
document.library.avatars.find(avatar => avatar.id === 'primitive-lock')?.eyes.positionYLeft
|
||||
).toBe(defaultAvatarEyes.positionYLeft + 22)
|
||||
expect(
|
||||
document.library.avatars
|
||||
.find(avatar => avatar.id === 'primitive-apple-bite')
|
||||
@ -296,6 +308,7 @@ describe('Studio document', () => {
|
||||
const filledIds = new Set([
|
||||
'primitive-ghost',
|
||||
'primitive-apple-bite',
|
||||
'primitive-lock',
|
||||
...fallback.library.avatars
|
||||
.filter(avatar => avatar.id.startsWith('oneworks-'))
|
||||
.map(avatar => avatar.id),
|
||||
@ -340,6 +353,10 @@ describe('Studio document', () => {
|
||||
type: 'vector',
|
||||
filled: true,
|
||||
})
|
||||
expect(restoredById.get('primitive-lock')?.renderStyle).toEqual({
|
||||
type: 'vector',
|
||||
filled: true,
|
||||
})
|
||||
expect(restoredById.get('primitive-ghost')?.colors).toEqual({
|
||||
body: '#ff9d45',
|
||||
eyes: '#111316',
|
||||
@ -348,6 +365,10 @@ describe('Studio document', () => {
|
||||
body: '#ff5558',
|
||||
eyes: '#111316',
|
||||
})
|
||||
expect(restoredById.get('primitive-lock')?.colors).toEqual({
|
||||
body: '#1a9cff',
|
||||
eyes: '#111316',
|
||||
})
|
||||
expect(restoredById.get('primitive-apple-bite')?.eyes.positionXLeft).toBe(-18)
|
||||
expect(restoredById.get('primitive-apple-bite')?.eyes.positionXRight).toBe(-18)
|
||||
expect(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user