Remove bundled Live2D demo assets

This commit is contained in:
Yun Chan 2026-06-26 15:09:32 +09:00
parent a3483067ed
commit 435cbafbc5
52 changed files with 13 additions and 39568 deletions

View file

@ -6,7 +6,7 @@
( 2),
(·· , ), "감지되되 단언되지 않게".
(§4.7): Live2D Cubism 4 + SVG/CSS/Web Audio .
(§4.7): SVG/CSS/Web Audio .
- rAF useAvatarMotion: 호흡 + + (RMS) + saccade
- 6 (persona.ts): eyelidDrop/gazeAvert/shoulderTurn/
breathRate/auraHue/blinkInterval. 8°0° ( ).
@ -14,14 +14,9 @@
- prefers-reduced-motion: 호흡/ + .
- analyser=null: .
Live2D
.model3.json ,
(persona / state / affect / analyser / rapport)
Live2DAvatar Cubism model parameter wiring . /
SVG .
===================================================================== */
import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
ageLookFor,
baseResistanceOf,
@ -41,10 +36,6 @@ import { Mouth } from "./Mouth";
Session.tsx ClientAvatar . */
export type { AvatarState, AvatarAffect, AvatarPersona } from "./persona";
const Live2DAvatar = lazy(() =>
import("./Live2DAvatar").then((mod) => ({ default: mod.Live2DAvatar })),
);
export interface ClientAvatarProps {
persona: AvatarPersona;
state: AvatarState;
@ -93,29 +84,6 @@ const STATE_TEXT: Record<AvatarState, string> = {
speaking: "이야기하는 중",
};
const DISABLED_LIVE2D_MODEL_URLS = new Set(["off", "false", "none"]);
const BUNDLED_DEMO_LIVE2D_MODEL_PATHS = new Set([
"/live2d/mao/Mao.model3.json",
"/live2d/haru/haru_greeter_t03.model3.json",
]);
function isBundledDemoLive2DModel(url: string): boolean {
try {
return BUNDLED_DEMO_LIVE2D_MODEL_PATHS.has(
new URL(url, "http://vignette.local").pathname,
);
} catch {
return false;
}
}
function resolveLive2DModelUrl(persona: AvatarPersona): string | null {
const url = persona.live2dModelUrl?.trim();
if (!url || DISABLED_LIVE2D_MODEL_URLS.has(url.toLowerCase())) return null;
if (import.meta.env.PROD && isBundledDemoLive2DModel(url)) return null;
return url;
}
export function ClientAvatar({
persona,
state,
@ -142,29 +110,6 @@ export function ClientAvatar({
() => resolveAffectParams(affect, state, effectiveRapport),
[affect, state, effectiveRapport],
);
const live2DModelUrl = useMemo(() => resolveLive2DModelUrl(persona), [persona]);
const [live2DReady, setLive2DReady] = useState(false);
const [live2DFailed, setLive2DFailed] = useState(false);
const [live2DError, setLive2DError] = useState<string | null>(null);
useEffect(() => {
setLive2DReady(false);
setLive2DFailed(false);
setLive2DError(null);
}, [live2DModelUrl]);
const handleLive2DReady = useCallback(() => {
setLive2DReady(true);
setLive2DError(null);
}, []);
const handleLive2DUnavailable = useCallback((reason: string) => {
setLive2DReady(false);
setLive2DFailed(true);
setLive2DError(reason);
}, []);
const shouldUseLive2D = Boolean(live2DModelUrl) && !live2DFailed;
// 모션 루프 (reduced 면 정지)
const frame = useAvatarMotion({
@ -198,8 +143,6 @@ export function ClientAvatar({
style={{ width: size }}
data-state={state}
data-affect={affect}
data-live2d={live2DReady ? "ready" : shouldUseLive2D ? "loading" : "off"}
data-live2d-error={live2DError ?? undefined}
data-realism={realism} /* 사실성은 데이터로만 유지(시각 표식 비노출, §4.6) */
aria-label={`교육용 가상 내담자: ${persona.label}, ${STATE_TEXT[state]}`}
>
@ -223,7 +166,7 @@ export function ClientAvatar({
/>
<svg
className={"vg-avatar__svg" + (live2DReady ? " is-live2d-covered" : "")}
className="vg-avatar__svg"
viewBox="0 0 200 200"
width={size}
height={size}
@ -258,22 +201,6 @@ export function ClientAvatar({
</g>
</svg>
{shouldUseLive2D && live2DModelUrl ? (
<Suspense fallback={null}>
<Live2DAvatar
modelUrl={live2DModelUrl}
state={state}
affect={affect}
params={params}
analyser={analyser}
speakingProgress={speakingProgress}
reduced={reduced}
size={size}
onReady={handleLive2DReady}
onUnavailable={handleLive2DUnavailable}
/>
</Suspense>
) : null}
</div>
{/* 페르소나 메타 + 상태 텍스트 */}
@ -307,9 +234,6 @@ const AVATAR_CSS = `
.vg-avatar__aura.is-reduced{animation:none;}
@keyframes vgAuraBreathe{0%,100%{opacity:.85;transform:scale(1)}50%{opacity:1;transform:scale(1.04)}}
.vg-avatar__svg{position:relative;z-index:1;display:block;transition:opacity var(--dur-base) var(--ease-out);}
.vg-avatar__svg.is-live2d-covered{opacity:0;}
.vg-avatar__live2d{position:absolute;inset:0;z-index:2;display:flex;align-items:center;justify-content:center;pointer-events:none;}
.vg-avatar__live2d-canvas{width:100%;height:100%;display:block;}
.vg-avatar__meta{display:flex;flex-direction:column;align-items:center;gap:3px;text-align:center;}
.vg-avatar__persona{font-size:var(--fs-sm);font-weight:600;color:var(--text-strong);}
.vg-avatar__state{font-size:var(--fs-xs);color:var(--text-muted);}