현재 작업 상태 저장

This commit is contained in:
Yun Chan 2026-06-27 11:20:24 +09:00
parent 07cc67761e
commit 6bd91b0d5e
674 changed files with 8726 additions and 298 deletions

View file

@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useMemo, type CSSProperties } from "react";
import type { AvatarExpression, AvatarState } from "./persona";
export type RasterVariant = "neutral" | "sad" | "tired" | "anxious" | "warm" | "startled";
@ -34,6 +34,9 @@ const VARIANT_FOR_EXPRESSION: Record<AvatarExpression, RasterVariant> = {
tired: "tired",
};
const LIVE2D_PARTS_ART_SETS = new Set(["seoyeon-live2d-v3"]);
const PSB_GROUP_ART_SETS = new Set(["seoyeon-live2d-psb"]);
export function rasterVariantFor(expression: AvatarExpression): RasterVariant {
return VARIANT_FOR_EXPRESSION[expression];
}
@ -51,37 +54,181 @@ export interface RasterBustProps {
reduced: boolean;
}
const TX = "opacity 220ms cubic-bezier(0.22,0.61,0.36,1), transform 220ms cubic-bezier(0.22,0.61,0.36,1)";
function clamp01(value: number): number {
return Math.min(1, Math.max(0, value));
}
function layerTransform(extra = ""): string {
return `translateX(-50%)${extra ? ` ${extra}` : ""}`;
}
function mouthPartFor(variant: RasterVariant): string {
return variant === "startled" ? "mouth-startled" : `mouth-${variant}`;
function eyeClipStyle(maskUrl: string): CSSProperties {
return {
WebkitMaskImage: `url("${maskUrl}")`,
maskImage: `url("${maskUrl}")`,
WebkitMaskRepeat: "no-repeat",
maskRepeat: "no-repeat",
WebkitMaskSize: "100% 100%",
maskSize: "100% 100%",
};
}
function browPoseFor(variant: RasterVariant): { y: number; rotate: number } {
function eyeApertureClipStyle(
side: "left" | "right",
upperLidY: number,
lowerLidY: number,
closedEyes: number,
): CSSProperties {
const baseTop = side === "left" ? 30.25 : 29.0;
const baseBottom = side === "left" ? 65.85 : 67.05;
const top = clamp(baseTop + upperLidY * 0.18 + closedEyes * 2.4, 0, 48);
const bottom = clamp(baseBottom - lowerLidY * 0.12 + closedEyes * 2.1, 45, 82);
const clipPath = `inset(${top.toFixed(2)}% 0 ${bottom.toFixed(2)}% 0)`;
return {
WebkitClipPath: clipPath,
clipPath,
};
}
function clamp01(value: number): number {
return Math.max(0, Math.min(1, value));
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
function mouthForVariant(variant: RasterVariant): string {
if (variant === "sad" || variant === "tired" || variant === "anxious") return "mouth-sad";
if (variant === "warm") return "mouth-warm";
return "mouth-neutral";
}
function browPoseForVariant(variant: RasterVariant): { y: number; leftRotate: number; rightRotate: number } {
switch (variant) {
case "sad":
return { y: 1.4, rotate: -0.35 };
case "tired":
return { y: 1.8, rotate: -0.2 };
return { y: 1.4, leftRotate: 1.1, rightRotate: -1.1 };
case "anxious":
return { y: -1.1, rotate: 0.25 };
case "warm":
return { y: -0.7, rotate: 0.12 };
return { y: -1.2, leftRotate: -1.4, rightRotate: 1.4 };
case "startled":
return { y: -3.2, rotate: 0.1 };
return { y: -2.4, leftRotate: 0, rightRotate: 0 };
case "warm":
return { y: -0.6, leftRotate: -0.4, rightRotate: 0.4 };
default:
return { y: 0, rotate: 0 };
return { y: 0, leftRotate: 0, rightRotate: 0 };
}
}
function psbMouthForVariant(variant: RasterVariant): string {
switch (variant) {
case "warm":
return "mouth-warm";
case "sad":
return "mouth-sad";
case "tired":
return "mouth-tired";
case "anxious":
return "mouth-anxious";
case "startled":
return "mouth-startled";
default:
return "mouth-neutral";
}
}
function psbBrowPoseForVariant(variant: RasterVariant): { y: number; leftRotate: number; rightRotate: number } {
switch (variant) {
case "sad":
return { y: 2.6, leftRotate: 2.2, rightRotate: -2.2 };
case "tired":
return { y: 2.0, leftRotate: 0.8, rightRotate: -0.8 };
case "anxious":
return { y: -2.2, leftRotate: -3.0, rightRotate: 3.0 };
case "startled":
return { y: -4.2, leftRotate: 0, rightRotate: 0 };
case "warm":
return { y: -1.0, leftRotate: -0.8, rightRotate: 0.8 };
default:
return { y: 0, leftRotate: 0, rightRotate: 0 };
}
}
function psbEyePoseForVariant(variant: RasterVariant): {
scaleY: number;
upperY: number;
lowerY: number;
lashY: number;
leftLashRotate: number;
rightLashRotate: number;
detailY: number;
} {
switch (variant) {
case "warm":
return {
scaleY: 0.72,
upperY: 5.4,
lowerY: -1.4,
lashY: 3.0,
leftLashRotate: -1.4,
rightLashRotate: 1.4,
detailY: 0.9,
};
case "sad":
return {
scaleY: 0.92,
upperY: 1.8,
lowerY: -0.5,
lashY: 1.0,
leftLashRotate: 1.2,
rightLashRotate: -1.2,
detailY: 0.4,
};
case "tired":
return {
scaleY: 0.56,
upperY: 7.2,
lowerY: -1.9,
lashY: 4.0,
leftLashRotate: 0.4,
rightLashRotate: -0.4,
detailY: 1.0,
};
case "anxious":
return {
scaleY: 1.04,
upperY: -1.2,
lowerY: 0.4,
lashY: -0.8,
leftLashRotate: -1.5,
rightLashRotate: 1.5,
detailY: -0.2,
};
case "startled":
return {
scaleY: 1.18,
upperY: -5.8,
lowerY: 1.6,
lashY: -3.2,
leftLashRotate: 0,
rightLashRotate: 0,
detailY: -0.5,
};
default:
return {
scaleY: 1,
upperY: 0,
lowerY: 0,
lashY: 0,
leftLashRotate: 0,
rightLashRotate: 0,
detailY: 0,
};
}
}
function psbGeneratedEyeVariantFor(variant: RasterVariant): "joy" | "sad" | "startled" | null {
// Imagegen v1 eye parts drifted too far from the source PSB style.
void variant;
return null;
}
export function RasterBust({
artSet,
affect,
@ -96,43 +243,445 @@ export function RasterBust({
}: RasterBustProps) {
const variant = rasterVariantFor(affect);
const speaking = state === "speaking" && !reduced;
const blinkClosed = reduced ? 0 : clamp01((1 - blink) / 0.92);
const speakOverlay = speaking ? clamp01(mouth) : 0;
const browPose = browPoseFor(variant);
const stackTransform = `translate(${gazeX.toFixed(2)}px, ${(gazeY - breath).toFixed(
2,
)}px) rotate(${headTilt.toFixed(2)}deg)`;
const part = useMemo(
() => (name: string) => `${import.meta.env.BASE_URL}avatar/${artSet}/parts/${name}.png`,
const raster = useMemo(
() => (name: string) => `${import.meta.env.BASE_URL}avatar/${artSet}/${name}.png`,
[artSet],
);
const hairPhase = reduced ? 0 : breath;
const hairLeftTransform = layerTransform(
`translate(${(hairPhase * 0.36).toFixed(2)}px, ${(hairPhase * -0.05).toFixed(
2,
)}px) rotate(${(hairPhase * -0.16).toFixed(2)}deg)`,
);
const hairRightTransform = layerTransform(
`translate(${(hairPhase * -0.32).toFixed(2)}px, ${(hairPhase * -0.04).toFixed(
2,
)}px) rotate(${(hairPhase * 0.15).toFixed(2)}deg)`,
);
const bangsTransform = layerTransform(
`translate(${(hairPhase * 0.12).toFixed(2)}px, ${(hairPhase * 0.08).toFixed(
2,
)}px) rotate(${(hairPhase * 0.08).toFixed(2)}deg)`,
);
const eyesTransform = layerTransform(
`translate(${(gazeX * 0.22).toFixed(2)}px, ${(gazeY * 0.16).toFixed(2)}px)`,
);
const browTransform = layerTransform(
`translate(${(gazeX * 0.08).toFixed(2)}px, ${(browPose.y + gazeY * 0.05).toFixed(
2,
)}px) rotate(${browPose.rotate.toFixed(2)}deg)`,
);
if (PSB_GROUP_ART_SETS.has(artSet)) {
const part = (name: string) => `${import.meta.env.BASE_URL}avatar/${artSet}/parts/${name}.png`;
const speakingOpen = speaking ? clamp01(Math.max(mouth, 0.1)) : 0;
const wideOpen = clamp01((speakingOpen - 0.34) / 0.5);
const smallOpen = speakingOpen > 0 ? 1 - wideOpen : 0;
const closedEyes = reduced ? 0 : clamp01((0.55 - blink) / 0.47);
const openEyes = 1 - closedEyes;
const irisX = reduced ? 0 : gazeX * 0.75;
const irisY = reduced ? 0 : gazeY * 0.48;
const hairSway = reduced ? 0 : clamp(breath * 0.48 + headTilt * 0.22 + gazeX * 0.08, -2.2, 2.2);
const hairLag = reduced ? 0 : clamp(breath * 0.34 + headTilt * 0.18, -1.8, 1.8);
const talkBounce = speakingOpen * 1.25;
const browFloat = reduced ? 0 : breath * 0.22;
const brow = psbBrowPoseForVariant(variant);
const eyePose = psbEyePoseForVariant(variant);
const generatedEyeVariant = psbGeneratedEyeVariantFor(variant);
const blinkScale = 1 - closedEyes * 0.78;
const eyeScaleY = Math.max(0.06, eyePose.scaleY * blinkScale);
const upperLidY = eyePose.upperY + closedEyes * 10.8;
const lowerLidY = eyePose.lowerY - closedEyes * 2.4;
const lashY = eyePose.lashY + closedEyes * 2.1;
const expressionMouth = psbMouthForVariant(variant);
const eyeDetailLayers: Array<{ name: string; mask: string; zIndex: number }> = [
{ name: "iris-left", mask: "eye-white-left", zIndex: 9 },
{ name: "iris-right", mask: "eye-white-right", zIndex: 9 },
{ name: "pupil-left", mask: "eye-white-left", zIndex: 10 },
{ name: "pupil-right", mask: "eye-white-right", zIndex: 10 },
{ name: "highlight-left", mask: "eye-white-left", zIndex: 11 },
{ name: "highlight-right", mask: "eye-white-right", zIndex: 11 },
];
const renderLayer = (
name: string,
className: string,
zIndex: number,
style: CSSProperties = {},
) => (
<img
key={name}
className={`vg-raster__layer ${className}`}
src={part(name)}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: 1, zIndex, transform: layerTransform(), ...style }}
/>
);
return (
<div
className="vg-raster"
data-raster-art-set={artSet}
data-raster-variant={variant}
data-raster-speaking={speaking ? "true" : "false"}
data-raster-mode="psb-detailed"
style={{ transform: stackTransform, transformOrigin: "50% 58%" }}
aria-hidden="true"
>
{renderLayer("hair-back-base", "vg-raster__layer--hairback", 1)}
{renderLayer("hair-back-left", "vg-raster__layer--hairback", 2, {
transform: layerTransform(
`rotate(${(hairLag * -0.46).toFixed(2)}deg)`,
),
transformOrigin: "49% 5%",
})}
{renderLayer("hair-back-right", "vg-raster__layer--hairback", 2, {
transform: layerTransform(
`rotate(${(hairLag * 0.46).toFixed(2)}deg)`,
),
transformOrigin: "51% 5%",
})}
{renderLayer("body", "vg-raster__layer--neck", 3)}
{renderLayer("outfit", "vg-raster__layer--shoulders", 4)}
{renderLayer("ear-left", "vg-raster__layer--ear", 5)}
{renderLayer("ear-right", "vg-raster__layer--ear", 5)}
{renderLayer("face-base", "vg-raster__layer--face", 6)}
{renderLayer("blush-left", "vg-raster__layer--face", 7, { opacity: variant === "warm" ? 1 : 0.72 })}
{renderLayer("blush-right", "vg-raster__layer--face", 7, { opacity: variant === "warm" ? 1 : 0.72 })}
{generatedEyeVariant ? (
<>
{renderLayer(`eyegen-${generatedEyeVariant}-eye-left`, "vg-raster__layer--eyes", 8, {
opacity: openEyes,
transform: layerTransform(`translate(${irisX.toFixed(2)}px, ${irisY.toFixed(2)}px)`),
transformOrigin: "41.1% 32.2%",
})}
{renderLayer(`eyegen-${generatedEyeVariant}-eye-right`, "vg-raster__layer--eyes", 8, {
opacity: openEyes,
transform: layerTransform(`translate(${irisX.toFixed(2)}px, ${irisY.toFixed(2)}px)`),
transformOrigin: "60.6% 31%",
})}
{renderLayer("eyelid-upper-left", "vg-raster__layer--eyelid", 13, {
opacity: closedEyes,
transform: layerTransform(`translateY(${(closedEyes * 10.8).toFixed(2)}px)`),
transformOrigin: "39.7% 31.6%",
})}
{renderLayer("eyelid-upper-right", "vg-raster__layer--eyelid", 13, {
opacity: closedEyes,
transform: layerTransform(`translateY(${(closedEyes * 10.8).toFixed(2)}px)`),
transformOrigin: "62.1% 30.2%",
})}
{renderLayer(`eyegen-${generatedEyeVariant}-brow-left`, "vg-raster__layer--brow", 14, {
transform: layerTransform(
`translateY(${(browFloat - talkBounce * 0.2).toFixed(2)}px) rotate(${(-browFloat * 0.06).toFixed(2)}deg)`,
),
transformOrigin: "41.3% 27.1%",
})}
{renderLayer(`eyegen-${generatedEyeVariant}-brow-right`, "vg-raster__layer--brow", 14, {
transform: layerTransform(
`translateY(${(browFloat - talkBounce * 0.2).toFixed(2)}px) rotate(${(browFloat * 0.06).toFixed(2)}deg)`,
),
transformOrigin: "59.6% 25.9%",
})}
</>
) : (
<>
{["eye-white-left", "eye-white-right"].map((name) =>
renderLayer(name, "vg-raster__layer--eyes", 8, {
opacity: openEyes,
transform: layerTransform(`scaleY(${eyeScaleY.toFixed(3)})`),
transformOrigin: name.endsWith("left") ? "41.1% 32.2%" : "60.6% 31%",
...eyeApertureClipStyle(name.endsWith("left") ? "left" : "right", upperLidY, lowerLidY, closedEyes),
}),
)}
{eyeDetailLayers.map(({ name, mask, zIndex }) =>
renderLayer(name, "vg-raster__layer--eyes", zIndex, {
opacity: openEyes,
transform: layerTransform(
`translate(${irisX.toFixed(2)}px, ${(irisY + eyePose.detailY).toFixed(2)}px) scaleY(${eyeScaleY.toFixed(3)})`,
),
transformOrigin: name.endsWith("left") ? "41.8% 31.9%" : "59.8% 30.8%",
...eyeClipStyle(part(mask)),
...eyeApertureClipStyle(name.endsWith("left") ? "left" : "right", upperLidY, lowerLidY, closedEyes),
}),
)}
{renderLayer("lash-left", "vg-raster__layer--eyes", 12, {
transform: layerTransform(
`translateY(${lashY.toFixed(2)}px) rotate(${(eyePose.leftLashRotate + closedEyes * 0.3).toFixed(2)}deg)`,
),
transformOrigin: "41.4% 29.8%",
})}
{renderLayer("lash-right", "vg-raster__layer--eyes", 12, {
transform: layerTransform(
`translateY(${lashY.toFixed(2)}px) rotate(${(eyePose.rightLashRotate - closedEyes * 0.3).toFixed(2)}deg)`,
),
transformOrigin: "59.9% 29%",
})}
{renderLayer("eyelid-upper-left", "vg-raster__layer--eyelid", 13, {
opacity: 0.72 + closedEyes * 0.28,
transform: layerTransform(`translateY(${upperLidY.toFixed(2)}px)`),
transformOrigin: "39.7% 31.6%",
})}
{renderLayer("eyelid-upper-right", "vg-raster__layer--eyelid", 13, {
opacity: 0.72 + closedEyes * 0.28,
transform: layerTransform(`translateY(${upperLidY.toFixed(2)}px)`),
transformOrigin: "62.1% 30.2%",
})}
{renderLayer("eyelid-lower-left", "vg-raster__layer--eyelid", 13, {
opacity: 0.78,
transform: layerTransform(`translateY(${lowerLidY.toFixed(2)}px)`),
transformOrigin: "41% 33.9%",
})}
{renderLayer("eyelid-lower-right", "vg-raster__layer--eyelid", 13, {
opacity: 0.78,
transform: layerTransform(`translateY(${lowerLidY.toFixed(2)}px)`),
transformOrigin: "60.8% 32.4%",
})}
{renderLayer("brow-left", "vg-raster__layer--brow", 14, {
transform: layerTransform(
`translateY(${(brow.y + browFloat - talkBounce * 0.28).toFixed(2)}px) rotate(${(brow.leftRotate - speakingOpen * 0.35 - browFloat * 0.08).toFixed(2)}deg)`,
),
transformOrigin: "40% 27%",
})}
{renderLayer("brow-right", "vg-raster__layer--brow", 14, {
transform: layerTransform(
`translateY(${(brow.y + browFloat - talkBounce * 0.28).toFixed(2)}px) rotate(${(brow.rightRotate + speakingOpen * 0.35 + browFloat * 0.08).toFixed(2)}deg)`,
),
transformOrigin: "60% 26%",
})}
</>
)}
{renderLayer("nose", "vg-raster__layer--nose", 15)}
{renderLayer("hair-side-left-1", "vg-raster__layer--hair", 16, {
transform: layerTransform(
`rotate(${(hairSway * -0.8).toFixed(2)}deg)`,
),
transformOrigin: "37.2% 6.4%",
})}
{renderLayer("hair-side-right-1", "vg-raster__layer--hair", 16, {
transform: layerTransform(
`rotate(${(hairSway * 0.8).toFixed(2)}deg)`,
),
transformOrigin: "63.3% 6.3%",
})}
{renderLayer("hair-side-left-2", "vg-raster__layer--hair", 17, {
transform: layerTransform(
`rotate(${(hairSway * -1.16).toFixed(2)}deg)`,
),
transformOrigin: "31.7% 32.1%",
})}
{renderLayer("hair-side-right-2", "vg-raster__layer--hair", 17, {
transform: layerTransform(
`rotate(${(hairSway * 1.16).toFixed(2)}deg)`,
),
transformOrigin: "66.1% 33.5%",
})}
{renderLayer("hair-bangs", "vg-raster__layer--bangs", 18, {
transform: layerTransform(
`rotate(${(hairSway * 0.28).toFixed(2)}deg)`,
),
transformOrigin: "50.7% 7%",
})}
{renderLayer(expressionMouth, "vg-raster__layer--mouth", 19, {
opacity: 1 - speakingOpen * 0.92,
transform: layerTransform(`translateY(${(talkBounce * 0.18).toFixed(2)}px)`),
transformOrigin: "50% 44%",
})}
{renderLayer("mouth-open-small", "vg-raster__layer--mouthopen", 20, {
opacity: smallOpen * 0.92,
transform: layerTransform(`translateY(${(talkBounce * 0.16).toFixed(2)}px)`),
transformOrigin: "50% 44%",
})}
{renderLayer("mouth-open", "vg-raster__layer--mouthopen", 21, {
opacity: wideOpen,
transform: layerTransform(`translateY(${(talkBounce * 0.16).toFixed(2)}px)`),
transformOrigin: "50% 44%",
})}
</div>
);
}
if (LIVE2D_PARTS_ART_SETS.has(artSet)) {
const part = (name: string) => `${import.meta.env.BASE_URL}avatar/${artSet}/parts/${name}.png`;
const speakingOpen = speaking ? clamp01(Math.max(mouth, 0.08)) : 0;
const closedEyes = reduced ? 0 : clamp01((0.55 - blink) / 0.47);
const openEyes = 1 - closedEyes;
const irisX = reduced ? 0 : gazeX * 0.42;
const irisY = reduced ? 0 : gazeY * 0.28;
const hairSway = reduced ? 0 : breath * 0.24 + headTilt * 0.18;
const brow = browPoseForVariant(variant);
const expressionMouth = mouthForVariant(variant);
const eyeDetailLayers: Array<{ name: string; mask: string; zIndex: number }> = [
{ name: "iris-left", mask: "eye-white-left", zIndex: 9 },
{ name: "iris-right", mask: "eye-white-right", zIndex: 9 },
{ name: "pupil-left", mask: "eye-white-left", zIndex: 10 },
{ name: "pupil-right", mask: "eye-white-right", zIndex: 10 },
{ name: "highlight-left", mask: "eye-white-left", zIndex: 11 },
{ name: "highlight-right", mask: "eye-white-right", zIndex: 11 },
];
return (
<div
className="vg-raster"
data-raster-art-set={artSet}
data-raster-variant={variant}
data-raster-speaking={speaking ? "true" : "false"}
data-raster-mode="live2d-parts"
style={{ transform: stackTransform, transformOrigin: "50% 58%" }}
aria-hidden="true"
>
<img
className="vg-raster__layer vg-raster__layer--hairback"
src={part("hair-back")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: 1, zIndex: 1, transform: layerTransform(`translateX(${hairSway * -0.45}px)`) }}
/>
<img
className="vg-raster__layer vg-raster__layer--shoulders"
src={part("torso")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: 1, zIndex: 2, transform: layerTransform() }}
/>
<img
className="vg-raster__layer vg-raster__layer--face"
src={part("head-faceless")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: 1, zIndex: 3, transform: layerTransform() }}
/>
<img
className="vg-raster__layer vg-raster__layer--hair"
src={part("hair-left")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: 1,
zIndex: 4,
transform: layerTransform(`translate(${hairSway * -0.9}px, ${Math.abs(hairSway) * 0.18}px) rotate(${hairSway * -0.22}deg)`),
transformOrigin: "37% 30%",
}}
/>
<img
className="vg-raster__layer vg-raster__layer--hair"
src={part("hair-right")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: 1,
zIndex: 4,
transform: layerTransform(`translate(${hairSway * 0.9}px, ${Math.abs(hairSway) * 0.18}px) rotate(${hairSway * 0.22}deg)`),
transformOrigin: "63% 30%",
}}
/>
<img
className="vg-raster__layer vg-raster__layer--bangs"
src={part("hair-front")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: 1,
zIndex: 8,
transform: layerTransform(`translateY(${hairSway * 0.18}px) rotate(${hairSway * 0.08}deg)`),
transformOrigin: "50% 18%",
}}
/>
{["eye-white-left", "eye-white-right", "lash-left", "lash-right"].map((name) => (
<img
className="vg-raster__layer vg-raster__layer--eyes"
key={name}
src={part(name)}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: openEyes, zIndex: name.startsWith("lash") ? 12 : 9, transform: layerTransform() }}
/>
))}
{eyeDetailLayers.map(
({ name, mask, zIndex }) => (
<img
className="vg-raster__layer vg-raster__layer--eyes"
key={name}
src={part(name)}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: openEyes,
zIndex,
transform: layerTransform(`translate(${irisX.toFixed(2)}px, ${irisY.toFixed(2)}px)`),
...eyeClipStyle(part(mask)),
}}
/>
),
)}
{["eyelid-left-closed", "eyelid-right-closed"].map((name) => (
<img
className="vg-raster__layer vg-raster__layer--eyelid"
key={name}
src={part(name)}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: closedEyes, zIndex: 13, transform: layerTransform() }}
/>
))}
<img
className="vg-raster__layer vg-raster__layer--brow"
src={part("brow-left")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: 1,
zIndex: 14,
transform: layerTransform(`translateY(${brow.y}px) rotate(${brow.leftRotate}deg)`),
transformOrigin: "40% 28%",
}}
/>
<img
className="vg-raster__layer vg-raster__layer--brow"
src={part("brow-right")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: 1,
zIndex: 14,
transform: layerTransform(`translateY(${brow.y}px) rotate(${brow.rightRotate}deg)`),
transformOrigin: "60% 28%",
}}
/>
<img
className="vg-raster__layer vg-raster__layer--nose"
src={part("nose")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: 1, zIndex: 15, transform: layerTransform() }}
/>
<img
className="vg-raster__layer vg-raster__layer--mouth"
src={part(expressionMouth)}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: 1 - speakingOpen, zIndex: 16, transform: layerTransform() }}
/>
<img
className="vg-raster__layer vg-raster__layer--mouthopen"
src={part("mouth-open")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: speakingOpen, zIndex: 17, transform: layerTransform() }}
/>
</div>
);
}
return (
<div
@ -145,124 +694,13 @@ export function RasterBust({
>
<img
className="vg-raster__layer vg-raster__layer--base"
src={part("base-faceless")}
alt=""
loading="eager"
decoding="async"
draggable={false}
/>
<img
className="vg-raster__layer vg-raster__layer--hair"
src={part("hair-left")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: reduced ? 0 : 0.16,
transform: hairLeftTransform,
transformOrigin: "34% 28%",
transition: TX,
}}
/>
<img
className="vg-raster__layer vg-raster__layer--hair"
src={part("hair-right")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: reduced ? 0 : 0.16,
transform: hairRightTransform,
transformOrigin: "66% 28%",
transition: TX,
}}
/>
<img
className="vg-raster__layer vg-raster__layer--bangs"
src={part("hair-bangs")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: reduced ? 0 : 0.2,
transform: bangsTransform,
transformOrigin: "50% 22%",
transition: TX,
}}
/>
<img
className="vg-raster__layer vg-raster__layer--brow"
src={part(`brow-${variant}`)}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: 1, transform: browTransform, transformOrigin: "50% 30%", transition: TX }}
/>
<img
className="vg-raster__layer vg-raster__layer--eyes"
src={part(`eyes-${variant}`)}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: 1 - blinkClosed * 0.95,
transform: eyesTransform,
transformOrigin: "50% 32%",
transition: TX,
}}
/>
<img
className="vg-raster__layer vg-raster__layer--eyelid"
src={part("eyelid-closed")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ opacity: blinkClosed, transform: layerTransform(), transition: TX }}
/>
<img
className="vg-raster__layer vg-raster__layer--nose"
src={part("nose-neutral")}
src={raster("neutral")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{ transform: layerTransform() }}
/>
<img
className="vg-raster__layer vg-raster__layer--mouth"
src={part(mouthPartFor(variant))}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: 1 - speakOverlay * 0.78,
transform: layerTransform(),
transition: TX,
}}
/>
<img
className="vg-raster__layer vg-raster__layer--mouthopen"
src={part("mouth-open")}
alt=""
loading="eager"
decoding="async"
draggable={false}
style={{
opacity: speakOverlay,
transform: layerTransform(`translateY(${(speakOverlay * 1.2).toFixed(2)}px)`),
transformOrigin: "50% 40%",
transition: TX,
}}
/>
</div>
);
}