feat: add filled lock avatar
This commit is contained in:
parent
edc7a6004d
commit
e73ce55fb6
@ -110,6 +110,7 @@ describe('@bible-strong/avatar-core', () => {
|
|||||||
'trapezoid',
|
'trapezoid',
|
||||||
'frustum',
|
'frustum',
|
||||||
'half-cone',
|
'half-cone',
|
||||||
|
'lock',
|
||||||
]
|
]
|
||||||
const neutral = poseFromExpression(expressionFromDefinition('neutral', expression))
|
const neutral = poseFromExpression(expressionFromDefinition('neutral', expression))
|
||||||
|
|
||||||
|
|||||||
@ -83,7 +83,8 @@
|
|||||||
"half-cone",
|
"half-cone",
|
||||||
"diamond",
|
"diamond",
|
||||||
"ghost",
|
"ghost",
|
||||||
"apple-bite"
|
"apple-bite",
|
||||||
|
"lock"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"width": { "$ref": "#/$defs/dimension" },
|
"width": { "$ref": "#/$defs/dimension" },
|
||||||
|
|||||||
@ -45,7 +45,10 @@ export type SurfaceDefinition<TType extends SurfaceType = SurfaceType> = {
|
|||||||
cutAngle?: number
|
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 PrimarySurfaceDefinition = SurfaceDefinition<SurfaceType>
|
||||||
export type BodyNodeSurfaceDefinition = SurfaceDefinition<BodyNodeSurfaceType>
|
export type BodyNodeSurfaceDefinition = SurfaceDefinition<BodyNodeSurfaceType>
|
||||||
|
|
||||||
|
|||||||
@ -1345,6 +1345,56 @@ const projectedAppleBitePath = (pose: AvatarPose, surface: SurfaceConfig) => {
|
|||||||
].join('')
|
].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) => {
|
const headPath = (pose: AvatarPose, surface: SurfaceConfig) => {
|
||||||
if (surface.type === 'sphere' || surface.type === 'mickey') {
|
if (surface.type === 'sphere' || surface.type === 'mickey') {
|
||||||
const exactPath = projectedEllipsoidPath(pose, surface)
|
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 === 'diamond') return projectedDiamondPath(pose, surface)
|
||||||
if (surface.type === 'ghost') return projectedGhostPath(pose, surface)
|
if (surface.type === 'ghost') return projectedGhostPath(pose, surface)
|
||||||
if (surface.type === 'apple-bite') return projectedAppleBitePath(pose, surface)
|
if (surface.type === 'apple-bite') return projectedAppleBitePath(pose, surface)
|
||||||
|
if (surface.type === 'lock') return projectedLockPath(pose, surface)
|
||||||
|
|
||||||
const key = surfaceCacheKey(surface)
|
const key = surfaceCacheKey(surface)
|
||||||
let localSamples = headSamplesCache.get(key)
|
let localSamples = headSamplesCache.get(key)
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export type SurfaceType =
|
|||||||
| 'diamond'
|
| 'diamond'
|
||||||
| 'ghost'
|
| 'ghost'
|
||||||
| 'apple-bite'
|
| 'apple-bite'
|
||||||
|
| 'lock'
|
||||||
|
|
||||||
export type SurfaceConfig = {
|
export type SurfaceConfig = {
|
||||||
type: SurfaceType
|
type: SurfaceType
|
||||||
@ -91,6 +92,7 @@ export const surfacePresets: Record<SurfaceType, SurfaceConfig> = {
|
|||||||
diamond: { type: 'diamond', width: 235, height: 260, depth: 215, roundness: 0 },
|
diamond: { type: 'diamond', width: 235, height: 260, depth: 215, roundness: 0 },
|
||||||
ghost: { type: 'ghost', width: 250, height: 245, depth: 190, roundness: 1 },
|
ghost: { type: 'ghost', width: 250, height: 245, depth: 190, roundness: 1 },
|
||||||
'apple-bite': { type: 'apple-bite', width: 240, height: 250, 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> = {
|
export const surfaceLabels: Record<SurfaceType, string> = {
|
||||||
@ -111,6 +113,7 @@ export const surfaceLabels: Record<SurfaceType, string> = {
|
|||||||
diamond: 'Diamant',
|
diamond: 'Diamant',
|
||||||
ghost: 'Ghost',
|
ghost: 'Ghost',
|
||||||
'apple-bite': 'Apple Bite',
|
'apple-bite': 'Apple Bite',
|
||||||
|
lock: 'Lock',
|
||||||
}
|
}
|
||||||
|
|
||||||
const signedPower = (value: number, exponent: number) =>
|
const signedPower = (value: number, exponent: number) =>
|
||||||
@ -482,6 +485,7 @@ export const surfacePointAt = (
|
|||||||
case 'capsule':
|
case 'capsule':
|
||||||
case 'ghost':
|
case 'ghost':
|
||||||
case 'apple-bite':
|
case 'apple-bite':
|
||||||
|
case 'lock':
|
||||||
return capsule(config, longitude, latitude)
|
return capsule(config, longitude, latitude)
|
||||||
case 'cone': {
|
case 'cone': {
|
||||||
const progress = (latitude + Math.PI / 2) / Math.PI
|
const progress = (latitude + Math.PI / 2) / Math.PI
|
||||||
@ -751,7 +755,8 @@ export const surfaceFrontSampleAt = (
|
|||||||
|
|
||||||
case 'capsule':
|
case 'capsule':
|
||||||
case 'ghost':
|
case 'ghost':
|
||||||
case 'apple-bite': {
|
case 'apple-bite':
|
||||||
|
case 'lock': {
|
||||||
const capRadiusY = Math.min(radiusX, radiusY)
|
const capRadiusY = Math.min(radiusX, radiusY)
|
||||||
const straightHalf = Math.max(0, radiusY - capRadiusY)
|
const straightHalf = Math.max(0, radiusY - capRadiusY)
|
||||||
const capCenterY = y < -straightHalf ? -straightHalf : y > straightHalf ? straightHalf : y
|
const capCenterY = y < -straightHalf ? -straightHalf : y > straightHalf ? straightHalf : y
|
||||||
|
|||||||
@ -155,6 +155,7 @@ const bundledAvatarColorMigrations: Record<string, { from: AvatarColors; to: Ava
|
|||||||
export const simpleBundledAvatarIds = [
|
export const simpleBundledAvatarIds = [
|
||||||
'primitive-ghost',
|
'primitive-ghost',
|
||||||
'primitive-apple-bite',
|
'primitive-apple-bite',
|
||||||
|
'primitive-lock',
|
||||||
'oneworks-cat',
|
'oneworks-cat',
|
||||||
'oneworks-cat-siamese',
|
'oneworks-cat-siamese',
|
||||||
'oneworks-cat-british-shorthair',
|
'oneworks-cat-british-shorthair',
|
||||||
@ -224,6 +225,7 @@ const primitiveBundledAvatarSpecs = [
|
|||||||
['primitive-diamond', 'Diamond', 'diamond', deslopAccentColors.pink, 1],
|
['primitive-diamond', 'Diamond', 'diamond', deslopAccentColors.pink, 1],
|
||||||
['primitive-apple-bite', 'Apple Bite', 'apple-bite', deslopAccentColors.red, 0.9],
|
['primitive-apple-bite', 'Apple Bite', 'apple-bite', deslopAccentColors.red, 0.9],
|
||||||
['primitive-ghost', 'Memento', 'ghost', deslopAccentColors.orange, 0.9],
|
['primitive-ghost', 'Memento', 'ghost', deslopAccentColors.orange, 0.9],
|
||||||
|
['primitive-lock', 'Lock', 'lock', deslopAccentColors.blue, 0.9],
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const fixedSkinColorBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehaviorLibrary => {
|
const fixedSkinColorBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehaviorLibrary => {
|
||||||
@ -815,7 +817,10 @@ const primitiveBundledAvatars: StudioAvatar[] = [
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
body: color,
|
body: color,
|
||||||
eyes: surface === 'ghost' || surface === 'apple-bite' ? filledAvatarEyeColor : color,
|
eyes:
|
||||||
|
surface === 'ghost' || surface === 'apple-bite' || surface === 'lock'
|
||||||
|
? filledAvatarEyeColor
|
||||||
|
: color,
|
||||||
},
|
},
|
||||||
eyes:
|
eyes:
|
||||||
surface === 'ghost'
|
surface === 'ghost'
|
||||||
@ -830,14 +835,22 @@ const primitiveBundledAvatars: StudioAvatar[] = [
|
|||||||
positionXLeft: defaultAvatarEyes.positionXLeft - 18,
|
positionXLeft: defaultAvatarEyes.positionXLeft - 18,
|
||||||
positionXRight: defaultAvatarEyes.positionXRight - 18,
|
positionXRight: defaultAvatarEyes.positionXRight - 18,
|
||||||
}
|
}
|
||||||
|
: surface === 'lock'
|
||||||
|
? {
|
||||||
|
...defaultAvatarEyes,
|
||||||
|
positionYLeft: defaultAvatarEyes.positionYLeft + 22,
|
||||||
|
positionYRight: defaultAvatarEyes.positionYRight + 22,
|
||||||
|
}
|
||||||
: { ...defaultAvatarEyes },
|
: { ...defaultAvatarEyes },
|
||||||
renderStyle:
|
renderStyle:
|
||||||
surface === 'ghost' || surface === 'apple-bite'
|
surface === 'ghost' || surface === 'apple-bite' || surface === 'lock'
|
||||||
? { ...filledAvatarRenderStyle }
|
? { ...filledAvatarRenderStyle }
|
||||||
: { ...defaultAvatarRenderStyle },
|
: { ...defaultAvatarRenderStyle },
|
||||||
...(surface === 'ghost'
|
...(surface === 'ghost'
|
||||||
? { behavior: mementoBehavior() }
|
? { behavior: mementoBehavior() }
|
||||||
: surface === 'apple-bite'
|
: surface === 'apple-bite'
|
||||||
|
? { behavior: fixedSkinColorBehavior() }
|
||||||
|
: surface === 'lock'
|
||||||
? { behavior: fixedSkinColorBehavior() }
|
? { behavior: fixedSkinColorBehavior() }
|
||||||
: {}),
|
: {}),
|
||||||
})),
|
})),
|
||||||
@ -919,11 +932,13 @@ const filledBundledAvatarIds = new Set([
|
|||||||
...Object.keys(mementoAnimalColors),
|
...Object.keys(mementoAnimalColors),
|
||||||
'primitive-ghost',
|
'primitive-ghost',
|
||||||
'primitive-apple-bite',
|
'primitive-apple-bite',
|
||||||
|
'primitive-lock',
|
||||||
])
|
])
|
||||||
const oneWorksOriginalAnimalIds = new Set(Object.keys(oneWorksBundledAnimalColors))
|
const oneWorksOriginalAnimalIds = new Set(Object.keys(oneWorksBundledAnimalColors))
|
||||||
const filledPrimitiveAvatarColors: Record<string, AvatarColors> = {
|
const filledPrimitiveAvatarColors: Record<string, AvatarColors> = {
|
||||||
'primitive-ghost': { body: deslopAccentColors.orange, eyes: filledAvatarEyeColor },
|
'primitive-ghost': { body: deslopAccentColors.orange, eyes: filledAvatarEyeColor },
|
||||||
'primitive-apple-bite': { body: deslopAccentColors.red, eyes: filledAvatarEyeColor },
|
'primitive-apple-bite': { body: deslopAccentColors.red, eyes: filledAvatarEyeColor },
|
||||||
|
'primitive-lock': { body: deslopAccentColors.blue, eyes: filledAvatarEyeColor },
|
||||||
}
|
}
|
||||||
|
|
||||||
const clonePrimitiveBundledAvatar = (avatar: StudioAvatar): StudioAvatar => ({
|
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 = [
|
const bundledAvatarNames = [
|
||||||
'Memento',
|
'Memento',
|
||||||
'Apple Bite',
|
'Apple Bite',
|
||||||
|
'Lock',
|
||||||
'Siamese',
|
'Siamese',
|
||||||
'British Shorthair',
|
'British Shorthair',
|
||||||
'Russian Blue',
|
'Russian Blue',
|
||||||
@ -78,9 +79,10 @@ describe('Studio document', () => {
|
|||||||
.filter(avatar => avatar.renderStyle.type !== 'vector' || !avatar.renderStyle.filled)
|
.filter(avatar => avatar.renderStyle.type !== 'vector' || !avatar.renderStyle.filled)
|
||||||
.every(avatar => avatar.colors.body === avatar.colors.eyes)
|
.every(avatar => avatar.colors.body === avatar.colors.eyes)
|
||||||
).toBe(true)
|
).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',
|
'ghost',
|
||||||
'apple-bite',
|
'apple-bite',
|
||||||
|
'lock',
|
||||||
'ellipse',
|
'ellipse',
|
||||||
'ellipse',
|
'ellipse',
|
||||||
'ellipse',
|
'ellipse',
|
||||||
@ -107,9 +109,7 @@ describe('Studio document', () => {
|
|||||||
expect(
|
expect(
|
||||||
document.library.avatars
|
document.library.avatars
|
||||||
.filter(avatar =>
|
.filter(avatar =>
|
||||||
['dog', 'dog-brown', 'bear', 'rabbit', 'bun'].some(
|
['dog', 'dog-brown', 'bear', 'rabbit', 'bun'].some(id => avatar.id === `oneworks-${id}`)
|
||||||
id => avatar.id === `oneworks-${id}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.every(avatar => avatar.renderStyle.type === 'vector' && avatar.renderStyle.filled === true)
|
.every(avatar => avatar.renderStyle.type === 'vector' && avatar.renderStyle.filled === true)
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
@ -122,12 +122,18 @@ describe('Studio document', () => {
|
|||||||
expect(
|
expect(
|
||||||
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.renderStyle
|
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.renderStyle
|
||||||
).toEqual({ type: 'vector', filled: true })
|
).toEqual({ type: 'vector', filled: true })
|
||||||
|
expect(
|
||||||
|
document.library.avatars.find(avatar => avatar.id === 'primitive-lock')?.renderStyle
|
||||||
|
).toEqual({ type: 'vector', filled: true })
|
||||||
expect(
|
expect(
|
||||||
document.library.avatars.find(avatar => avatar.id === 'primitive-ghost')?.body.primary
|
document.library.avatars.find(avatar => avatar.id === 'primitive-ghost')?.body.primary
|
||||||
).toMatchObject({ width: 225, height: 220.5, depth: 171 })
|
).toMatchObject({ width: 225, height: 220.5, depth: 171 })
|
||||||
expect(
|
expect(
|
||||||
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.body.primary
|
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.body.primary
|
||||||
).toMatchObject({ width: 216, height: 225, depth: 171 })
|
).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(
|
expect(
|
||||||
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.eyes
|
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.eyes
|
||||||
.positionXLeft
|
.positionXLeft
|
||||||
@ -199,6 +205,12 @@ describe('Studio document', () => {
|
|||||||
expect(
|
expect(
|
||||||
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.colors
|
document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.colors
|
||||||
).toEqual({ body: '#ff5558', eyes: '#111316' })
|
).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(
|
expect(
|
||||||
document.library.avatars
|
document.library.avatars
|
||||||
.find(avatar => avatar.id === 'primitive-apple-bite')
|
.find(avatar => avatar.id === 'primitive-apple-bite')
|
||||||
@ -296,6 +308,7 @@ describe('Studio document', () => {
|
|||||||
const filledIds = new Set([
|
const filledIds = new Set([
|
||||||
'primitive-ghost',
|
'primitive-ghost',
|
||||||
'primitive-apple-bite',
|
'primitive-apple-bite',
|
||||||
|
'primitive-lock',
|
||||||
...fallback.library.avatars
|
...fallback.library.avatars
|
||||||
.filter(avatar => avatar.id.startsWith('oneworks-'))
|
.filter(avatar => avatar.id.startsWith('oneworks-'))
|
||||||
.map(avatar => avatar.id),
|
.map(avatar => avatar.id),
|
||||||
@ -340,6 +353,10 @@ describe('Studio document', () => {
|
|||||||
type: 'vector',
|
type: 'vector',
|
||||||
filled: true,
|
filled: true,
|
||||||
})
|
})
|
||||||
|
expect(restoredById.get('primitive-lock')?.renderStyle).toEqual({
|
||||||
|
type: 'vector',
|
||||||
|
filled: true,
|
||||||
|
})
|
||||||
expect(restoredById.get('primitive-ghost')?.colors).toEqual({
|
expect(restoredById.get('primitive-ghost')?.colors).toEqual({
|
||||||
body: '#ff9d45',
|
body: '#ff9d45',
|
||||||
eyes: '#111316',
|
eyes: '#111316',
|
||||||
@ -348,6 +365,10 @@ describe('Studio document', () => {
|
|||||||
body: '#ff5558',
|
body: '#ff5558',
|
||||||
eyes: '#111316',
|
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.positionXLeft).toBe(-18)
|
||||||
expect(restoredById.get('primitive-apple-bite')?.eyes.positionXRight).toBe(-18)
|
expect(restoredById.get('primitive-apple-bite')?.eyes.positionXRight).toBe(-18)
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user