전 저장소 리팩터링과 SSOT 정비

This commit is contained in:
Yun Chan 2026-07-15 21:31:30 +09:00
parent 14ecbd4e7d
commit 3dfddcac6f
173 changed files with 19679 additions and 6952 deletions

View file

@ -35,7 +35,6 @@ import { Brows } from "./Brows";
import { Mouth } from "./Mouth";
import { live2dModel3Path, live2dModelForPersonaCode, live2dMotionForExpression } from "./live2dModel";
import { useExpressionTransition } from "./useExpressionTransition";
import { RasterBust } from "./RasterBust";
import "./client-avatar.css";
/* ( import )
@ -232,7 +231,6 @@ export function ClientAvatar({
const outfitColor = persona.outfitColor ?? hairColor;
const irisColor = persona.eyeColor ?? "#2B2B2B";
const realism = Math.min(0.45, Math.max(0.3, persona.realism)); // 안전 클램프
const useRaster = Boolean(persona.rasterArtSet);
const age = ageLookFor(persona.ageBand);
const expressionLabel = expressionLabelFor(affect);
const live2dModel = useMemo(() => live2dModelForPersonaCode(persona.code), [persona.code]);
@ -295,7 +293,7 @@ export function ClientAvatar({
data-live2d-expression-count={live2dModel.expressions.length}
data-persona-code={persona.code ?? ""}
data-avatar-animated={motionEnabled ? "true" : "false"}
data-render-mode={useRaster ? "raster" : "svg"}
data-render-mode="svg"
data-realism={realism} /* 사실성은 데이터로만 유지(시각 표식 비노출, §4.6) */
aria-label={`교육용 가상 내담자: ${persona.label}, ${STATE_TEXT[state]}, ${expressionLabel}`}
>
@ -319,28 +317,14 @@ export function ClientAvatar({
reduced={reduced}
/>
{useRaster ? (
<RasterBust
artSet={persona.rasterArtSet as string}
affect={affect}
state={state}
breath={breath}
blink={blink}
mouth={mouthOpen}
gazeX={gazeX}
gazeY={gazeY}
headTilt={params.headTilt}
reduced={reduced}
/>
) : (
<svg
className="vg-avatar__svg"
viewBox="0 0 200 200"
width={size}
height={size}
role="img"
aria-hidden="true"
>
<svg
className="vg-avatar__svg"
viewBox="0 0 200 200"
width={size}
height={size}
role="img"
aria-hidden="true"
>
{/* 흉상 그룹: 어깨 호흡(translateY) + 저항 시 미세 회전 */}
<g transform={`translate(0 ${-breath}) rotate(${shoulderRotate} 100 150)`}>
{/* 어깨/상반신 실루엣 */}
@ -381,8 +365,7 @@ export function ClientAvatar({
</g>
</g>
</g>
</svg>
)}
</svg>
</div>

View file

@ -43,6 +43,18 @@ const PERSONA_GENERATED_ART_SETS = [
];
const PSB_GROUP_ART_SETS = new Set(["seoyeon-live2d-psb", "seoyeon-live2d-psd-v2", ...PERSONA_GENERATED_ART_SETS]);
const PSB_CRYING_ART_SETS = new Set(["seoyeon-live2d-psd-v2", ...PERSONA_GENERATED_ART_SETS]);
const EYE_DETAIL_LAYERS: ReadonlyArray<{
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 },
];
export function rasterVariantFor(expression: AvatarExpression): RasterVariant {
return VARIANT_FOR_EXPRESSION[expression];
@ -294,14 +306,6 @@ export function RasterBust({
const browLeftPart = useSadCryingParts ? "brow-sad-left" : "brow-left";
const browRightPart = useSadCryingParts ? "brow-sad-right" : "brow-right";
const tearOpacity = useSadCryingParts ? clamp01(0.82 + openEyes * 0.18 - closedEyes * 0.35) : 0;
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,
@ -395,7 +399,7 @@ export function RasterBust({
...eyeApertureClipStyle(name.endsWith("left") ? "left" : "right", upperLidY, lowerLidY, closedEyes),
}),
)}
{eyeDetailLayers.map(({ name, mask, zIndex }) =>
{EYE_DETAIL_LAYERS.map(({ name, mask, zIndex }) =>
renderLayer(name, "vg-raster__layer--eyes", zIndex, {
opacity: openEyes,
transform: layerTransform(
@ -526,14 +530,6 @@ export function RasterBust({
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
@ -626,7 +622,7 @@ export function RasterBust({
style={{ opacity: openEyes, zIndex: name.startsWith("lash") ? 12 : 9, transform: layerTransform() }}
/>
))}
{eyeDetailLayers.map(
{EYE_DETAIL_LAYERS.map(
({ name, mask, zIndex }) => (
<img
className="vg-raster__layer vg-raster__layer--eyes"

View file

@ -118,12 +118,6 @@ export interface AvatarPersona {
eyeColor?: string;
/** 캐릭터 고유 보조색 */
accentColor?: string;
/**
* () . SVG
* `/avatar/<artSet>/<variant>.png` (Live2D식) .
* SVG . asset imagegen(gpt-image-2)+BiRefNet .
*/
rasterArtSet?: string;
/** 사실성 0.35~0.45 고정 (불쾌한 골짜기 회피) */
realism: number;
/**