Remove bundled Live2D demo assets
This commit is contained in:
parent
a3483067ed
commit
435cbafbc5
52 changed files with 13 additions and 39568 deletions
|
|
@ -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);}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue