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

@ -29,14 +29,9 @@ npm run typecheck # tsc --noEmit (타입 체크만)
| 변수 | 기본값 | 설명 |
|---|---|---|
| `VITE_API_BASE` | `/api` | API 베이스. 개발은 vite proxy, 프로덕션은 nginx가 백엔드로 라우팅 |
| `VITE_LIVE2D_CUBISM_CORE` | `/live2d/live2dcubismcore.min.js` | Cubism Core JS URL. 페르소나별 `live2dModelUrl`이 있을 때만 사용 |
Live2D 샘플 자산은 `public/live2d/` 아래에 로컬 통합 테스트용으로 남겨 둔다.
세션 아바타는 기본적으로 SVG/persona 렌더러를 사용하며, 실제 페르소나 전용
`AvatarPersona.live2dModelUrl`이 있을 때만 Cubism 모델을 렌더링한다.
Mao/Haru 샘플 모델을 모든 페르소나의 공용 프로덕션 폴백으로 쓰지 않는다.
프로덕션 빌드는 번들된 Mao/Haru 샘플 URL을 무시하고 SVG 아바타를 유지한다.
모델이 없거나 로딩에 실패하면 세션 화면은 기존 SVG 아바타를 그대로 렌더링한다.
세션 아바타는 SVG/persona 렌더러만 사용한다. Mao/Haru 같은 샘플 Live2D 자산과
Pixi/Cubism 런타임은 공개 배포물에서 제거했다.
## 라우트
@ -73,8 +68,7 @@ src/
ProgressBar Field/Input Dot Icon(inline svg) SectionHead.
배럴: index.ts → import { Button, ... } from "../components/ui"
shell/ AppShell Sidebar Topbar (§6.3 공통 셸).
avatar/ClientAvatar.tsx 가상 내담자 아바타(Live2D + SVG 폴백).
avatar/Live2DAvatar.tsx Cubism 4/Pixi 런타임 브리지.
avatar/ClientAvatar.tsx 가상 내담자 SVG 아바타.
pages/ Login + learner/teacher/admin/settings/session 화면.
```

File diff suppressed because it is too large Load diff

View file

@ -15,8 +15,6 @@
"e2e:ui": "playwright test --ui"
},
"dependencies": {
"pixi-live2d-display": "^0.4.0",
"pixi.js": "^6.5.10",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^6.30.1"

View file

@ -1,30 +0,0 @@
# Live2D Assets
This directory contains the Live2D runtime and optional sample models served by the Vite/nginx static web app.
Default runtime:
```text
/live2d/live2dcubismcore.min.js
```
Sample models:
```text
/live2d/mao/Mao.model3.json
/live2d/haru/haru_greeter_t03.model3.json
```
Sources:
- Cubism Core: https://cubism.live2d.com/sdk-web/cubismcore/live2dcubismcore.min.js
- Mao model: https://github.com/Live2D/CubismWebSamples/tree/develop/Samples/Resources/Mao
- Haru model: https://github.com/guansss/pixi-live2d-display/tree/master/test/assets/haru
The Live2D `CubismWebSamples` license lists `Samples/Resources/Mao` under Live2D's Free Material License.
The `pixi-live2d-display` README states that the Haru sample is redistributed under Live2D's Free Material License.
The local `model3.json` removes optional `DisplayInfo` and motion sound references that are absent from the copied asset set.
No sample model is loaded by default. Session avatars use the SVG/persona renderer unless the active `AvatarPersona.live2dModelUrl` points to a real persona-specific Cubism `*.model3.json` asset. The Mao and Haru samples are for local wiring checks only; production builds ignore those bundled sample URLs instead of rendering them as shared persona fallbacks.
The app also falls back to the SVG avatar when `live2dModelUrl` is omitted, set to `off`/`false`/`none`, or the model cannot be loaded.

View file

@ -1,10 +0,0 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamMouthOpenY",
"Value": 0.27,
"Blend": "Add"
}
]
}

View file

@ -1,35 +0,0 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamBrowLY",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamMouthOpenY",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeForm",
"Value": 0.54,
"Blend": "Add"
}
]
}

View file

@ -1,55 +0,0 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamBrowLY",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamMouthForm",
"Value": -2,
"Blend": "Add"
},
{
"Id": "ParamMouthOpenY",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeForm",
"Value": -1,
"Blend": "Add"
}
]
}

View file

@ -1,60 +0,0 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0.8,
"Blend": "Multiply"
},
{
"Id": "ParamEyeROpen",
"Value": 0.8,
"Blend": "Multiply"
},
{
"Id": "ParamBrowLY",
"Value": -0.56,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": -0.56,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0.35,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0.35,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -0.74,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -0.74,
"Blend": "Add"
},
{
"Id": "ParamMouthForm",
"Value": -1.76,
"Blend": "Add"
},
{
"Id": "ParamEyeForm",
"Value": 1,
"Blend": "Add"
}
]
}

View file

@ -1,35 +0,0 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Multiply"
},
{
"Id": "ParamEyeLSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Multiply"
},
{
"Id": "ParamEyeRSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0.32,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0.32,
"Blend": "Add"
}
]
}

View file

@ -1,35 +0,0 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 2,
"Blend": "Multiply"
},
{
"Id": "ParamEyeROpen",
"Value": 2,
"Blend": "Multiply"
},
{
"Id": "ParamBrowLY",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamMouthForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": -0.65,
"Blend": "Add"
}
]
}

View file

@ -1,65 +0,0 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0.89,
"Blend": "Multiply"
},
{
"Id": "ParamEyeROpen",
"Value": 0.89,
"Blend": "Multiply"
},
{
"Id": "ParamBrowLY",
"Value": -0.56,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": -0.56,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0.35,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0.35,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -0.74,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -0.74,
"Blend": "Add"
},
{
"Id": "ParamMouthForm",
"Value": -0.46,
"Blend": "Add"
},
{
"Id": "ParamTere",
"Value": 1,
"Blend": "Add"
}
]
}

View file

@ -1,30 +0,0 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0.8,
"Blend": "Multiply"
},
{
"Id": "ParamEyeROpen",
"Value": 0.8,
"Blend": "Multiply"
},
{
"Id": "ParamBrowLForm",
"Value": -0.33,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -0.33,
"Blend": "Add"
},
{
"Id": "ParamMouthForm",
"Value": -1.76,
"Blend": "Add"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -1,94 +0,0 @@
{
"Version": 3,
"FileReferences": {
"Moc": "haru_greeter_t03.moc3",
"Textures": [
"haru_greeter_t03.2048/texture_00.png",
"haru_greeter_t03.2048/texture_01.png"
],
"Physics": "haru_greeter_t03.physics3.json",
"Pose": "haru_greeter_t03.pose3.json",
"Expressions": [
{
"Name": "f00",
"File": "expressions/F01.exp3.json"
},
{
"Name": "f01",
"File": "expressions/F02.exp3.json"
},
{
"Name": "f02",
"File": "expressions/F03.exp3.json"
},
{
"Name": "f03",
"File": "expressions/F04.exp3.json"
},
{
"Name": "f04",
"File": "expressions/F05.exp3.json"
},
{
"Name": "f05",
"File": "expressions/F06.exp3.json"
},
{
"Name": "f06",
"File": "expressions/F07.exp3.json"
},
{
"Name": "f07",
"File": "expressions/F08.exp3.json"
}
],
"Motions": {
"Idle": [
{
"File": "motion/haru_g_idle.motion3.json"
},
{
"File": "motion/haru_g_m07.motion3.json"
},
{
"File": "motion/haru_g_m15.motion3.json"
}
],
"Tap": [
{
"File": "motion/haru_g_m14.motion3.json"
},
{
"File": "motion/haru_g_m05.motion3.json"
}
]
}
},
"Groups": [
{
"Target": "Parameter",
"Name": "EyeBlink",
"Ids": [
"ParamEyeLOpen",
"ParamEyeROpen"
]
},
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": [
"ParamMouthOpenY"
]
}
],
"HitAreas": [
{
"Id": "HitArea",
"Name": "Head"
},
{
"Id": "HitArea2",
"Name": "Body"
}
]
}

View file

@ -1,373 +0,0 @@
{
"Version": 3,
"Meta": {
"PhysicsSettingCount": 4,
"TotalInputCount": 14,
"TotalOutputCount": 4,
"VertexCount": 8,
"EffectiveForces": {
"Gravity": {
"X": 0,
"Y": -1
},
"Wind": {
"X": 0,
"Y": 0
}
},
"PhysicsDictionary": [
{
"Id": "PhysicsSetting1",
"Name": "前髪"
},
{
"Id": "PhysicsSetting2",
"Name": "横髪"
},
{
"Id": "PhysicsSetting3",
"Name": "後ろ髪"
},
{
"Id": "PhysicsSetting4",
"Name": "スカーフ"
}
]
},
"PhysicsSettings": [
{
"Id": "PhysicsSetting1",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleX"
},
"Weight": 60,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleZ"
},
"Weight": 60,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 40,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 40,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamHairFront"
},
"VertexIndex": 1,
"Scale": 1.821,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 8
},
"Mobility": 0.95,
"Delay": 0.8,
"Acceleration": 1.5,
"Radius": 8
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting2",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleX"
},
"Weight": 60,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleZ"
},
"Weight": 60,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 40,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 40,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamHairSide"
},
"VertexIndex": 1,
"Scale": 1.593,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 8
},
"Mobility": 0.95,
"Delay": 0.8,
"Acceleration": 1,
"Radius": 8
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting3",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleX"
},
"Weight": 60,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleZ"
},
"Weight": 60,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 40,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 40,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamHairBack"
},
"VertexIndex": 1,
"Scale": 1.943,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 8
},
"Mobility": 0.95,
"Delay": 0.8,
"Acceleration": 1.5,
"Radius": 8
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting4",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 100,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamScarf"
},
"VertexIndex": 1,
"Scale": 0.873,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 10
},
"Mobility": 0.9,
"Delay": 0.6,
"Acceleration": 1.5,
"Radius": 10
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
}
]
}

View file

@ -1,25 +0,0 @@
{
"Type": "Live2D Pose",
"Groups": [
[
{
"Id": "Part01ArmRA001",
"Link": []
},
{
"Id": "Part01ArmRB001",
"Link": []
}
],
[
{
"Id": "Part01ArmLA001",
"Link": []
},
{
"Id": "Part01ArmLB001",
"Link": []
}
]
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 MiB

View file

@ -1,915 +0,0 @@
{
"Version": 3,
"Parameters": [
{
"Id": "ParamAngleX",
"GroupId": "ParamGroupFace",
"Name": "角度 X"
},
{
"Id": "ParamAngleY",
"GroupId": "ParamGroupFace",
"Name": "角度 Y"
},
{
"Id": "ParamAngleZ",
"GroupId": "ParamGroupFace",
"Name": "角度 Z"
},
{
"Id": "ParamCheek",
"GroupId": "ParamGroupFace",
"Name": "照れ"
},
{
"Id": "ParamFaceInkOn",
"GroupId": "ParamGroupFace",
"Name": "顔インク 表示"
},
{
"Id": "ParamEyeLOpen",
"GroupId": "ParamGroupEyes",
"Name": "左目 開閉"
},
{
"Id": "ParamEyeLSmile",
"GroupId": "ParamGroupEyes",
"Name": "左目 笑顔"
},
{
"Id": "ParamEyeLForm",
"GroupId": "ParamGroupEyes",
"Name": "左目 変形"
},
{
"Id": "ParamEyeROpen",
"GroupId": "ParamGroupEyes",
"Name": "右目 開閉"
},
{
"Id": "ParamEyeRSmile",
"GroupId": "ParamGroupEyes",
"Name": "右目 笑顔"
},
{
"Id": "ParamEyeRForm",
"GroupId": "ParamGroupEyes",
"Name": "右目 変形"
},
{
"Id": "ParamEyeBallX",
"GroupId": "ParamGroupEyeballs",
"Name": "目玉 X"
},
{
"Id": "ParamEyeBallY",
"GroupId": "ParamGroupEyeballs",
"Name": "目玉 Y"
},
{
"Id": "ParamEyeBallForm",
"GroupId": "ParamGroupEyeballs",
"Name": "目玉 縮小"
},
{
"Id": "ParamEyeEffect",
"GroupId": "ParamGroupEyeballs",
"Name": "目 エフェクト"
},
{
"Id": "ParamBrowLY",
"GroupId": "ParamGroupBrows",
"Name": "左眉 上下"
},
{
"Id": "ParamBrowRY",
"GroupId": "ParamGroupBrows",
"Name": "右眉 上下"
},
{
"Id": "ParamBrowLX",
"GroupId": "ParamGroupBrows",
"Name": "左眉 左右"
},
{
"Id": "ParamBrowRX",
"GroupId": "ParamGroupBrows",
"Name": "右眉 左右"
},
{
"Id": "ParamBrowLAngle",
"GroupId": "ParamGroupBrows",
"Name": "左眉 角度"
},
{
"Id": "ParamBrowRAngle",
"GroupId": "ParamGroupBrows",
"Name": "右眉 角度"
},
{
"Id": "ParamBrowLForm",
"GroupId": "ParamGroupBrows",
"Name": "左眉 変形"
},
{
"Id": "ParamBrowRForm",
"GroupId": "ParamGroupBrows",
"Name": "右眉 変形"
},
{
"Id": "ParamA",
"GroupId": "ParamGroupMouth",
"Name": "あ"
},
{
"Id": "ParamI",
"GroupId": "ParamGroupMouth",
"Name": "い"
},
{
"Id": "ParamU",
"GroupId": "ParamGroupMouth",
"Name": "う"
},
{
"Id": "ParamE",
"GroupId": "ParamGroupMouth",
"Name": "え"
},
{
"Id": "ParamO",
"GroupId": "ParamGroupMouth",
"Name": "お"
},
{
"Id": "ParamMouthUp",
"GroupId": "ParamGroupMouth",
"Name": "上がり口"
},
{
"Id": "ParamMouthDown",
"GroupId": "ParamGroupMouth",
"Name": "下がり口"
},
{
"Id": "ParamMouthAngry",
"GroupId": "ParamGroupMouth",
"Name": "むくれ口"
},
{
"Id": "ParamMouthAngryLine",
"GroupId": "ParamGroupMouth",
"Name": "むくれ口線"
},
{
"Id": "ParamBodyAngleX",
"GroupId": "ParamGroupBody",
"Name": "体の回転 X"
},
{
"Id": "ParamBodyAngleY",
"GroupId": "ParamGroupBody",
"Name": "体の回転 Y"
},
{
"Id": "ParamBodyAngleZ",
"GroupId": "ParamGroupBody",
"Name": "体の回転 Z"
},
{
"Id": "ParamBreath",
"GroupId": "ParamGroupBody",
"Name": "呼吸"
},
{
"Id": "ParamLeftShoulderUp",
"GroupId": "ParamGroupBody",
"Name": "左肩の上下"
},
{
"Id": "ParamRightShoulderUp",
"GroupId": "ParamGroupBody",
"Name": "右肩の上下"
},
{
"Id": "ParamArmLA01",
"GroupId": "ParamGroupArmLA",
"Name": "左腕A 肩の回転"
},
{
"Id": "ParamArmLA02",
"GroupId": "ParamGroupArmLA",
"Name": "左腕A 肘の回転"
},
{
"Id": "ParamArmLA03",
"GroupId": "ParamGroupArmLA",
"Name": "左腕A 手首の回転"
},
{
"Id": "ParamHandLA",
"GroupId": "ParamGroupArmLA",
"Name": "左手A"
},
{
"Id": "ParamArmRA01",
"GroupId": "ParamGroupArmRA",
"Name": "右腕A 肩の回転"
},
{
"Id": "ParamArmRA02",
"GroupId": "ParamGroupArmRA",
"Name": "右腕A 肘の回転"
},
{
"Id": "ParamArmRA03",
"GroupId": "ParamGroupArmRA",
"Name": "右腕A 手首の回転"
},
{
"Id": "ParamWandRotate",
"GroupId": "ParamGroupArmRA",
"Name": "杖の回転"
},
{
"Id": "ParamHandRA",
"GroupId": "ParamGroupArmRA",
"Name": "右手A"
},
{
"Id": "ParamInkDrop",
"GroupId": "ParamGroupArmRA",
"Name": "インク垂れ"
},
{
"Id": "ParamInkDropRotate",
"GroupId": "ParamGroupArmRA",
"Name": "インク垂れ 回転"
},
{
"Id": "ParamInkDropOn",
"GroupId": "ParamGroupArmRA",
"Name": "インク垂れ 表示"
},
{
"Id": "ParamArmLB01",
"GroupId": "ParamGroupArmLB",
"Name": "左腕B 肩の回転"
},
{
"Id": "ParamArmLB02",
"GroupId": "ParamGroupArmLB",
"Name": "左腕B 肘の回転"
},
{
"Id": "ParamArmLB03",
"GroupId": "ParamGroupArmLB",
"Name": "左腕B 手首の回転"
},
{
"Id": "ParamHandLB",
"GroupId": "ParamGroupArmLB",
"Name": "左手B"
},
{
"Id": "ParamHatForm",
"GroupId": "ParamGroupArmLB",
"Name": "帽子の変形"
},
{
"Id": "ParamArmRB01",
"GroupId": "ParamGroupArmRB",
"Name": "右腕B 肩の回転"
},
{
"Id": "ParamArmRB02",
"GroupId": "ParamGroupArmRB",
"Name": "右腕B 肘の回転"
},
{
"Id": "ParamArmRB02Y",
"GroupId": "ParamGroupArmRB",
"Name": "右腕B 腕のY"
},
{
"Id": "ParamArmRB03",
"GroupId": "ParamGroupArmRB",
"Name": "右腕B 手首の回転"
},
{
"Id": "ParamHandRB",
"GroupId": "ParamGroupArmRB",
"Name": "右手B"
},
{
"Id": "ParamAllX",
"GroupId": "ParamGroupOverall",
"Name": "全体の移動 X"
},
{
"Id": "ParamAllY",
"GroupId": "ParamGroupOverall",
"Name": "全体の移動 Y"
},
{
"Id": "ParamAllRotate",
"GroupId": "ParamGroupOverall",
"Name": "全体の回転"
},
{
"Id": "ParamHairFront",
"GroupId": "ParamGroupSway",
"Name": "髪揺れ 前"
},
{
"Id": "ParamHairSideL",
"GroupId": "ParamGroupSway",
"Name": "髪揺れ 左横"
},
{
"Id": "ParamHairSideR",
"GroupId": "ParamGroupSway",
"Name": "髪揺れ 右横"
},
{
"Id": "ParamHairBack",
"GroupId": "ParamGroupSway",
"Name": "髪揺れ 後"
},
{
"Id": "ParamHairBackR",
"GroupId": "ParamGroupSway",
"Name": "髪揺れ 右後"
},
{
"Id": "ParamHairBackL",
"GroupId": "ParamGroupSway",
"Name": "髪揺れ 左後"
},
{
"Id": "ParamoHairMesh",
"GroupId": "ParamGroupSway",
"Name": "メッシュの揺れ"
},
{
"Id": "ParamHairFrontFuwa",
"GroupId": "ParamGroupSway",
"Name": "前髪 ふわ"
},
{
"Id": "ParamHairSideFuwa",
"GroupId": "ParamGroupSway",
"Name": "横髪 ふわ"
},
{
"Id": "ParamHairBackFuwa",
"GroupId": "ParamGroupSway",
"Name": "後ろ髪 ふわ"
},
{
"Id": "ParamWing",
"GroupId": "ParamGroupSway",
"Name": "羽の揺れ"
},
{
"Id": "ParamRibbon",
"GroupId": "ParamGroupSway",
"Name": "帽子リボンの揺れ"
},
{
"Id": "ParamHatBrim",
"GroupId": "ParamGroupSway",
"Name": "帽子つばの揺れ"
},
{
"Id": "ParamHatTop",
"GroupId": "ParamGroupSway",
"Name": "帽子 上の揺れ"
},
{
"Id": "ParamAccessory1",
"GroupId": "ParamGroupSway",
"Name": "首飾りの揺れ1"
},
{
"Id": "ParamAccessory2",
"GroupId": "ParamGroupSway",
"Name": "首飾りの揺れ2"
},
{
"Id": "ParamString",
"GroupId": "ParamGroupSway",
"Name": "パーカーひもの揺れ"
},
{
"Id": "ParamRobeL",
"GroupId": "ParamGroupSway",
"Name": "ローブの揺れ 左"
},
{
"Id": "ParamRobeR",
"GroupId": "ParamGroupSway",
"Name": "ローブの揺れ 右"
},
{
"Id": "ParamRobeFuwa",
"GroupId": "ParamGroupSway",
"Name": "ローブのふわ"
},
{
"Id": "ParamHeartMissOn",
"GroupId": "ParamGroupHeart",
"Name": "ハート失敗 表示"
},
{
"Id": "ParamHeartBackMissOn",
"GroupId": "ParamGroupHeart",
"Name": "ハート失敗後ろ 表示"
},
{
"Id": "ParamHeartColorRainbow",
"GroupId": "ParamGroupHeart",
"Name": "ハート失敗 虹色"
},
{
"Id": "ParamHeartHealOn",
"GroupId": "ParamGroupHeart",
"Name": "ハート回復 表示"
},
{
"Id": "ParamHeartBackHealOn",
"GroupId": "ParamGroupHeart",
"Name": "ハート回復後ろ 表示"
},
{
"Id": "ParamHeartColorHeal",
"GroupId": "ParamGroupHeart",
"Name": "ハート回復 緑色"
},
{
"Id": "ParamHeartDrow",
"GroupId": "ParamGroupHeart",
"Name": "ハート 描画"
},
{
"Id": "ParamHeartSize",
"GroupId": "ParamGroupHeart",
"Name": "ハート 拡縮"
},
{
"Id": "ParamHeartColorLight",
"GroupId": "ParamGroupHeart",
"Name": "ハート 色変化"
},
{
"Id": "ParamWandInkColorRainbow",
"GroupId": "ParamGroupInk",
"Name": "杖インク 虹色"
},
{
"Id": "ParamWandInkColorHeal",
"GroupId": "ParamGroupInk",
"Name": "杖インク 緑色"
},
{
"Id": "ParamWandInk",
"GroupId": "ParamGroupInk",
"Name": "杖インク"
},
{
"Id": "ParamSmokeOn",
"GroupId": "ParamGroupExplosion",
"Name": "煙 表示"
},
{
"Id": "ParamSmoke",
"GroupId": "ParamGroupExplosion",
"Name": "煙"
},
{
"Id": "ParamExplosionChargeOn",
"GroupId": "ParamGroupExplosion",
"Name": "爆発光溜め 表示"
},
{
"Id": "ParamExplosionLightCharge",
"GroupId": "ParamGroupExplosion",
"Name": "爆発光溜め"
},
{
"Id": "ParamExplosionOn",
"GroupId": "ParamGroupExplosion",
"Name": "爆発 表示"
},
{
"Id": "ParamExplosion",
"GroupId": "ParamGroupExplosion",
"Name": "爆発"
},
{
"Id": "ParamRabbitElimination",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎ 消滅"
},
{
"Id": "ParamRabbitAppearance",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎ 出現"
},
{
"Id": "ParamRabbitSize",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎ 拡縮"
},
{
"Id": "ParamRabbitDraworder",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎ 描画順"
},
{
"Id": "ParamRabbitEar",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎ 耳"
},
{
"Id": "ParamRabbitDirection",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎ 向き"
},
{
"Id": "ParamRabbitLight",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎ 色変化"
},
{
"Id": "ParamRabbitLightSize",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎ光 拡縮"
},
{
"Id": "ParamRabbitX",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎの移動 X"
},
{
"Id": "ParamRabbitY",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎの移動 Y"
},
{
"Id": "ParamRabbitRotate",
"GroupId": "ParamGroupRabbit",
"Name": "うさぎの回転"
},
{
"Id": "ParamAuraOn",
"GroupId": "ParamGroupAura",
"Name": "オーラ 表示"
},
{
"Id": "ParamAura",
"GroupId": "ParamGroupAura",
"Name": "オーラ"
},
{
"Id": "ParamAuraColor1",
"GroupId": "ParamGroupAura",
"Name": "オーラ 色変化1"
},
{
"Id": "ParamAuraColor2",
"GroupId": "ParamGroupAura",
"Name": "オーラ 色変化2"
},
{
"Id": "ParamHeartLightOn",
"GroupId": "ParamGroupLight",
"Name": "光 表示"
},
{
"Id": "ParamHeartLight",
"GroupId": "ParamGroupLight",
"Name": "光 星"
},
{
"Id": "ParamHeartLightColor",
"GroupId": "ParamGroupLight",
"Name": "光 色変化"
},
{
"Id": "ParamHealLightOn",
"GroupId": "ParamGroupLight",
"Name": "回復魔法光 表示"
},
{
"Id": "ParamHealLight",
"GroupId": "ParamGroupLight",
"Name": "回復魔法光"
},
{
"Id": "ParamStrengthenLightOn",
"GroupId": "ParamGroupLight",
"Name": "強化魔法光 表示"
},
{
"Id": "ParamStrengthenLight",
"GroupId": "ParamGroupLight",
"Name": "強化魔法光"
},
{
"Id": "ParamStrengthenLightMove",
"GroupId": "ParamGroupLight",
"Name": "強化魔法光 移動"
},
{
"Id": "ParamMagicPositionX",
"GroupId": "ParamGroupAllEffects",
"Name": "魔法の位置X"
},
{
"Id": "ParamMagicPositionY",
"GroupId": "ParamGroupAllEffects",
"Name": "魔法の位置Y"
},
{
"Id": "ParamAllColor1",
"GroupId": "ParamGroupAllEffects",
"Name": "全体の色1"
},
{
"Id": "ParamAllColor2",
"GroupId": "ParamGroupAllEffects",
"Name": "全体の色2"
},
{
"Id": "ParamSphereOn",
"GroupId": "ParamGroupColorSample",
"Name": "玉 表示"
},
{
"Id": "ParamSphereMove",
"GroupId": "ParamGroupColorSample",
"Name": "玉 移動"
},
{
"Id": "ParamSphereMultiplyColor",
"GroupId": "ParamGroupColorSample",
"Name": "玉 乗算色"
},
{
"Id": "ParamSphereScreenColor",
"GroupId": "ParamGroupColorSample",
"Name": "玉 スクリーン色"
}
],
"ParameterGroups": [
{
"Id": "ParamGroupFace",
"GroupId": "",
"Name": "顔"
},
{
"Id": "ParamGroupEyes",
"GroupId": "",
"Name": "目"
},
{
"Id": "ParamGroupEyeballs",
"GroupId": "",
"Name": "目玉"
},
{
"Id": "ParamGroupBrows",
"GroupId": "",
"Name": "眉"
},
{
"Id": "ParamGroupMouth",
"GroupId": "",
"Name": "口"
},
{
"Id": "ParamGroupBody",
"GroupId": "",
"Name": "体"
},
{
"Id": "ParamGroupArmLA",
"GroupId": "",
"Name": "左腕A"
},
{
"Id": "ParamGroupArmRA",
"GroupId": "",
"Name": "右腕A"
},
{
"Id": "ParamGroupArmLB",
"GroupId": "",
"Name": "左腕B"
},
{
"Id": "ParamGroupArmRB",
"GroupId": "",
"Name": "右腕B"
},
{
"Id": "ParamGroupOverall",
"GroupId": "",
"Name": "全体"
},
{
"Id": "ParamGroupSway",
"GroupId": "",
"Name": "揺れ"
},
{
"Id": "ParamGroupHeart",
"GroupId": "",
"Name": "ハート"
},
{
"Id": "ParamGroupInk",
"GroupId": "",
"Name": "インク"
},
{
"Id": "ParamGroupExplosion",
"GroupId": "",
"Name": "爆発"
},
{
"Id": "ParamGroupRabbit",
"GroupId": "",
"Name": "うさぎ"
},
{
"Id": "ParamGroupAura",
"GroupId": "",
"Name": "オーラ"
},
{
"Id": "ParamGroupLight",
"GroupId": "",
"Name": "光"
},
{
"Id": "ParamGroupAllEffects",
"GroupId": "",
"Name": "全体エフェクト"
},
{
"Id": "ParamGroupColorSample",
"GroupId": "",
"Name": "乗算色・スクリーン色サンプル"
}
],
"Parts": [
{
"Id": "PartCore",
"Name": "コア"
},
{
"Id": "PartRabbit",
"Name": "うさぎ"
},
{
"Id": "PartEffect",
"Name": "エフェクト"
},
{
"Id": "PartInk",
"Name": "インク"
},
{
"Id": "PartSmoke",
"Name": "煙"
},
{
"Id": "PartExplosionLight",
"Name": "爆発光"
},
{
"Id": "Partaura",
"Name": "オーラ"
},
{
"Id": "PartLight",
"Name": "光"
},
{
"Id": "PartHeart",
"Name": "ハート"
},
{
"Id": "PartHat",
"Name": "帽子"
},
{
"Id": "PartHairSide",
"Name": "横髪"
},
{
"Id": "PartHairFront",
"Name": "前髪"
},
{
"Id": "PartHairBack",
"Name": "後ろ髪"
},
{
"Id": "PartBrow",
"Name": "眉毛"
},
{
"Id": "PartEye",
"Name": "目"
},
{
"Id": "PartCheek",
"Name": "頬"
},
{
"Id": "PartNose",
"Name": "鼻"
},
{
"Id": "PartMouth",
"Name": "口"
},
{
"Id": "PartFace",
"Name": "顔"
},
{
"Id": "PartEar",
"Name": "耳"
},
{
"Id": "PartNeck",
"Name": "首"
},
{
"Id": "PartRobe",
"Name": "ローブ"
},
{
"Id": "PartHoodie",
"Name": "パーカー"
},
{
"Id": "PartLeg",
"Name": "脚"
},
{
"Id": "PartArmLA",
"Name": "左腕A"
},
{
"Id": "PartArmRA",
"Name": "右腕A"
},
{
"Id": "PartArmLB",
"Name": "左腕B"
},
{
"Id": "PartArmRB",
"Name": "右腕B"
},
{
"Id": "PartColorSample",
"Name": "乗算色・スクリーン色サンプル"
},
{
"Id": "PartSketch",
"Name": "[ 下絵 ]"
},
{
"Id": "PartEyeBall",
"Name": "目玉"
},
{
"Id": "PartWandA",
"Name": "杖A"
},
{
"Id": "PartWandB",
"Name": "杖B"
}
],
"CombinedParameters": [
[
"ParamAngleX",
"ParamAngleY"
],
[
"ParamAllX",
"ParamAllY"
],
[
"ParamMagicPositionX",
"ParamMagicPositionY"
]
]
}

Binary file not shown.

View file

@ -1,103 +0,0 @@
{
"Version": 3,
"FileReferences": {
"Moc": "Mao.moc3",
"Textures": [
"Mao.2048/texture_00.png"
],
"Physics": "Mao.physics3.json",
"Pose": "Mao.pose3.json",
"DisplayInfo": "Mao.cdi3.json",
"Expressions": [
{
"Name": "exp_01",
"File": "expressions/exp_01.exp3.json"
},
{
"Name": "exp_02",
"File": "expressions/exp_02.exp3.json"
},
{
"Name": "exp_03",
"File": "expressions/exp_03.exp3.json"
},
{
"Name": "exp_04",
"File": "expressions/exp_04.exp3.json"
},
{
"Name": "exp_05",
"File": "expressions/exp_05.exp3.json"
},
{
"Name": "exp_06",
"File": "expressions/exp_06.exp3.json"
},
{
"Name": "exp_07",
"File": "expressions/exp_07.exp3.json"
},
{
"Name": "exp_08",
"File": "expressions/exp_08.exp3.json"
}
],
"Motions": {
"Idle": [
{
"File": "motions/mtn_01.motion3.json"
},
{
"File": "motions/sample_01.motion3.json"
}
],
"TapBody": [
{
"File": "motions/mtn_02.motion3.json"
},
{
"File": "motions/mtn_03.motion3.json"
},
{
"File": "motions/mtn_04.motion3.json"
},
{
"File": "motions/special_01.motion3.json"
},
{
"File": "motions/special_02.motion3.json"
},
{
"File": "motions/special_03.motion3.json"
}
]
}
},
"Groups": [
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": [
"ParamA"
]
},
{
"Target": "Parameter",
"Name": "EyeBlink",
"Ids": [
"ParamEyeLOpen",
"ParamEyeROpen"
]
}
],
"HitAreas": [
{
"Id": "HitAreaHead",
"Name": "Head"
},
{
"Id": "HitAreaBody",
"Name": "Body"
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -1,25 +0,0 @@
{
"Type": "Live2D Pose",
"Groups": [
[
{
"Id": "PartArmLA",
"Link": []
},
{
"Id": "PartArmLB",
"Link": []
}
],
[
{
"Id": "PartArmRA",
"Link": []
},
{
"Id": "PartArmRB",
"Link": []
}
]
]
}

View file

@ -1,147 +0,0 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.5,
"FadeOutTime": 0.5,
"Parameters": [
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLOpen",
"Value": 1,
"Blend": "Multiply"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 1,
"Blend": "Multiply"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeEffect",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamA",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamI",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamU",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamE",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamO",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthUp",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthDown",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngry",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngryLine",
"Value": 0,
"Blend": "Add"
}
]
}

View file

@ -1,147 +0,0 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.5,
"FadeOutTime": 0.5,
"Parameters": [
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Multiply"
},
{
"Id": "ParamEyeLSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Multiply"
},
{
"Id": "ParamEyeRSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeEffect",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamA",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamI",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamU",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamE",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamO",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthUp",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthDown",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngry",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngryLine",
"Value": 0,
"Blend": "Add"
}
]
}

View file

@ -1,147 +0,0 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.5,
"FadeOutTime": 0.5,
"Parameters": [
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Multiply"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Multiply"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeEffect",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamA",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamI",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamU",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamE",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamO",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthUp",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthDown",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngry",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngryLine",
"Value": 0,
"Blend": "Add"
}
]
}

View file

@ -1,147 +0,0 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.5,
"FadeOutTime": 0.5,
"Parameters": [
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLOpen",
"Value": 1.2,
"Blend": "Multiply"
},
{
"Id": "ParamEyeLSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 1.2,
"Blend": "Multiply"
},
{
"Id": "ParamEyeRSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeEffect",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamA",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamI",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamU",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamE",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamO",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthUp",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthDown",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngry",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngryLine",
"Value": 0,
"Blend": "Add"
}
]
}

View file

@ -1,147 +0,0 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.5,
"FadeOutTime": 0.5,
"Parameters": [
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLOpen",
"Value": 1,
"Blend": "Multiply"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 1,
"Blend": "Multiply"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeEffect",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamA",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamI",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamU",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamE",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamO",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthUp",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamMouthDown",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamMouthAngry",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngryLine",
"Value": 0,
"Blend": "Add"
}
]
}

View file

@ -1,147 +0,0 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.5,
"FadeOutTime": 0.5,
"Parameters": [
{
"Id": "ParamCheek",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeLOpen",
"Value": 1,
"Blend": "Multiply"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 1,
"Blend": "Multiply"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeEffect",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamA",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamI",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamU",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamE",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamO",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthUp",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthDown",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngry",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngryLine",
"Value": 0,
"Blend": "Add"
}
]
}

View file

@ -1,147 +0,0 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.5,
"FadeOutTime": 0.5,
"Parameters": [
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLOpen",
"Value": 1.2,
"Blend": "Multiply"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 1.2,
"Blend": "Multiply"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeEffect",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamA",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamI",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamU",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamE",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamO",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthUp",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamMouthDown",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamMouthAngry",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngryLine",
"Value": 0,
"Blend": "Add"
}
]
}

View file

@ -1,147 +0,0 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.5,
"FadeOutTime": 0.5,
"Parameters": [
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLOpen",
"Value": 1,
"Blend": "Multiply"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 1,
"Blend": "Multiply"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeBallX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeEffect",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamA",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamI",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamU",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamE",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamO",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthUp",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamMouthDown",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamMouthAngry",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamMouthAngryLine",
"Value": 1,
"Blend": "Add"
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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);}

View file

@ -1,383 +0,0 @@
import { useEffect, useRef } from "react";
import type { AffectParams, AvatarAffect, AvatarState } from "./persona";
type PixiNamespace = typeof import("pixi.js");
type PixiApplication = import("pixi.js").Application;
type Live2DModule = typeof import("pixi-live2d-display/cubism4");
type Live2DModelCtor = Live2DModule["Live2DModel"];
type Live2DModelInstance = import("pixi-live2d-display/cubism4").Live2DModel;
interface Live2DAvatarProps {
modelUrl: string;
state: AvatarState;
affect: AvatarAffect;
params: AffectParams;
analyser: AnalyserNode | null;
speakingProgress: number | null;
reduced: boolean;
size: number;
onReady: () => void;
onUnavailable: (reason: string) => void;
}
type MutableLive2DModel = Live2DModelInstance & {
internalModel?: {
coreModel?: {
setParameterValueById?: (id: string, value: number, weight?: number) => void;
addParameterValueById?: (id: string, value: number, weight?: number) => void;
};
};
};
type Live2DRefState = Pick<
Live2DAvatarProps,
"state" | "affect" | "params" | "analyser" | "speakingProgress" | "reduced" | "size"
>;
declare global {
interface Window {
PIXI?: PixiNamespace;
Live2DCubismCore?: unknown;
}
}
const DEFAULT_CUBISM_CORE = "/live2d/live2dcubismcore.min.js";
const CORE_SCRIPT =
import.meta.env.VITE_LIVE2D_CUBISM_CORE?.trim() || DEFAULT_CUBISM_CORE;
let pixiRegistered = false;
let cubismCorePromise: Promise<void> | null = null;
let runtimePromise:
| Promise<{ PIXI: PixiNamespace; Live2DModel: Live2DModelCtor; MotionPreloadStrategy: Live2DModule["MotionPreloadStrategy"] }>
| null = null;
function registerPixi(PIXI: PixiNamespace, Live2DModel: Live2DModelCtor) {
if (pixiRegistered) return;
window.PIXI = PIXI;
Live2DModel.registerTicker(PIXI.Ticker);
pixiRegistered = true;
}
function loadLive2DRuntime() {
if (!runtimePromise) {
runtimePromise = Promise.all([
import("pixi.js"),
import("pixi-live2d-display/cubism4"),
]).then(([PIXI, live2d]) => {
registerPixi(PIXI, live2d.Live2DModel);
return {
PIXI,
Live2DModel: live2d.Live2DModel,
MotionPreloadStrategy: live2d.MotionPreloadStrategy,
};
});
}
return runtimePromise;
}
function hasCubismCore(): boolean {
return typeof window !== "undefined" && Boolean(window.Live2DCubismCore);
}
function appendScript(src: string): Promise<void> {
if (hasCubismCore()) return Promise.resolve();
const existing = Array.from(document.scripts).find(
(script) => script.dataset.vgLive2dCore === src,
);
if (existing) {
return new Promise((resolve, reject) => {
existing.addEventListener("load", () => resolve(), { once: true });
existing.addEventListener("error", () => reject(new Error(`Cubism Core load failed: ${src}`)), {
once: true,
});
});
}
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = src;
script.async = true;
script.dataset.vgLive2dCore = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Cubism Core load failed: ${src}`));
document.head.appendChild(script);
});
}
function ensureCubismCore(): Promise<void> {
if (hasCubismCore()) return Promise.resolve();
if (!cubismCorePromise) {
cubismCorePromise = (async () => {
await appendScript(CORE_SCRIPT);
if (!hasCubismCore()) throw new Error("Cubism Core did not initialize.");
})().catch((err) => {
cubismCorePromise = null;
throw err;
});
}
return cubismCorePromise;
}
function fitModel(model: Live2DModelInstance, size: number) {
model.anchor.set(0.5, 0.5);
model.scale.set(1);
const bounds = model.getLocalBounds();
const boundsWidth = Math.max(1, bounds.width);
const boundsHeight = Math.max(1, bounds.height);
const scale = Math.min((size * 0.86) / boundsWidth, (size * 0.98) / boundsHeight);
model.scale.set(scale);
model.x = size / 2;
model.y = size * 0.58;
}
function fallbackMouthTarget(tSec: number, progress: number | null): number {
if (progress !== null && progress >= 1) return 0;
const syl = 0.5 + 0.5 * Math.sin(tSec * 2 * Math.PI * 5.5);
const jitter = 0.5 + 0.5 * Math.sin(tSec * 2 * Math.PI * 11 + 1.3);
return Math.min(0.85, 0.18 + 0.42 * syl * jitter);
}
function mouthFromAnalyser(
analyser: AnalyserNode | null,
buffer: Float32Array<ArrayBuffer>,
previous: number,
dtSec: number,
tSec: number,
progress: number | null,
): number {
let target = fallbackMouthTarget(tSec, progress);
if (analyser) {
analyser.getFloatTimeDomainData(buffer);
let sum = 0;
for (let i = 0; i < buffer.length; i++) sum += buffer[i] * buffer[i];
const rms = Math.sqrt(sum / buffer.length);
target = rms < 0.04 ? 0 : Math.min(1, rms * 3.2);
}
const tau = 0.16;
const alpha = 1 - Math.exp(-dtSec / tau);
return previous + (target - previous) * alpha;
}
function setParam(model: MutableLive2DModel, id: string, value: number, weight = 0.85) {
try {
model.internalModel?.coreModel?.setParameterValueById?.(id, value, weight);
} catch {
/* Some models omit optional standard parameters. */
}
}
function applyLive2DParams(
model: MutableLive2DModel,
snapshot: Live2DRefState,
mouth: number,
tSec: number,
) {
const { state, affect, params, reduced } = snapshot;
if (reduced) return;
const gazeBase = state === "listening" ? -params.gazeAvert * 0.15 : -params.gazeAvert * 0.22;
const thinkingDrift = state === "thinking" ? Math.sin(tSec * 0.9) * 0.08 : 0;
const speakEnergy = state === "speaking" ? mouth : 0;
const smile = Math.max(0, params.mouthCurve);
const downturn = Math.max(0, -params.mouthCurve);
setParam(model, "ParamMouthOpenY", speakEnergy, 1);
setParam(model, "ParamA", speakEnergy, 1);
setParam(model, "ParamMouthForm", params.mouthCurve * 0.25, 0.35);
setParam(model, "ParamMouthUp", smile * 0.45, 0.25);
setParam(model, "ParamMouthDown", downturn * 0.45, 0.25);
setParam(model, "ParamMouthAngry", affect === "resistant" ? 0.55 : 0, 0.25);
setParam(model, "ParamEyeBallX", gazeBase + thinkingDrift, 0.5);
setParam(model, "ParamEyeBallY", state === "thinking" ? -0.12 : 0.03, 0.45);
setParam(model, "ParamAngleX", gazeBase * 14, 0.35);
setParam(model, "ParamAngleY", state === "thinking" ? -3 : 1, 0.3);
setParam(model, "ParamAngleZ", -params.shoulderTurn * 0.25, 0.25);
setParam(model, "ParamBodyAngleX", -params.shoulderTurn * 0.45, 0.35);
setParam(model, "ParamBreath", 0.5 + Math.sin(tSec * 2 * Math.PI / params.breathPeriod) * 0.22, 0.35);
}
function focusPointFor(state: AvatarState, params: AffectParams, size: number): [number, number] {
if (state === "thinking") return [size * 0.42, size * 0.62];
if (state === "listening") return [size * 0.56, size * 0.48];
if (state === "speaking") return [size * 0.5, size * 0.46];
return [size * (0.5 - params.gazeAvert * 0.008), size * 0.52];
}
function expressionsFor(affect: AvatarAffect): string[] {
if (affect === "depressed") return ["sad", "f02", "f03", "exp_05", "exp_06"];
if (affect === "anxious") return ["surprised", "f03", "f04", "exp_03", "exp_04"];
if (affect === "resistant") return ["angry", "f06", "f07", "exp_07", "exp_08"];
return ["normal", "f00", "f01", "exp_01", "exp_02"];
}
async function tryMotion(model: Live2DModelInstance, groups: string[]) {
for (const group of groups) {
const ok = await model.motion(group).catch(() => false);
if (ok) return;
}
}
async function tryExpression(model: Live2DModelInstance, names: string[]) {
for (const name of names) {
const ok = await model.expression(name).catch(() => false);
if (ok) return;
}
}
export function Live2DAvatar({
modelUrl,
state,
affect,
params,
analyser,
speakingProgress,
reduced,
size,
onReady,
onUnavailable,
}: Live2DAvatarProps) {
const hostRef = useRef<HTMLDivElement>(null);
const appRef = useRef<PixiApplication | null>(null);
const modelRef = useRef<MutableLive2DModel | null>(null);
const snapshotRef = useRef<Live2DRefState>({
state,
affect,
params,
analyser,
speakingProgress,
reduced,
size,
});
useEffect(() => {
snapshotRef.current = { state, affect, params, analyser, speakingProgress, reduced, size };
}, [state, affect, params, analyser, speakingProgress, reduced, size]);
useEffect(() => {
const model = modelRef.current;
if (!model) return;
model.autoUpdate = !reduced;
}, [reduced]);
useEffect(() => {
const model = modelRef.current;
if (!model || reduced) return;
const [x, y] = focusPointFor(state, params, size);
model.focus(x, y);
void tryExpression(model, expressionsFor(affect));
if (state === "speaking") void tryMotion(model, ["Speak", "Speaking", "TapBody"]);
else if (state === "thinking") void tryMotion(model, ["Think", "Thinking"]);
else if (state === "idle") void tryMotion(model, ["Idle", "idle"]);
}, [affect, params, reduced, size, state]);
useEffect(() => {
const app = appRef.current;
const model = modelRef.current;
if (!app || !model) return;
app.renderer.resize(size, size);
fitModel(model, size);
}, [size]);
useEffect(() => {
const host = hostRef.current;
if (!host) return;
let cancelled = false;
let mouth = 0;
let lastT = performance.now();
const t0 = lastT;
const audioBuffer: Float32Array<ArrayBuffer> = new Float32Array(1024);
const tick = () => {
const model = modelRef.current;
if (!model) return;
const now = performance.now();
const dtSec = Math.min(0.05, (now - lastT) / 1000);
const tSec = (now - t0) / 1000;
lastT = now;
const snapshot = snapshotRef.current;
if (snapshot.state === "speaking") {
mouth = mouthFromAnalyser(
snapshot.analyser,
audioBuffer,
mouth,
dtSec,
tSec,
snapshot.speakingProgress,
);
} else if (mouth > 0.001) {
mouth = mouth * Math.exp(-dtSec / 0.1);
if (mouth < 0.005) mouth = 0;
}
applyLive2DParams(model, snapshot, mouth, tSec);
};
(async () => {
try {
await ensureCubismCore();
const runtime = await loadLive2DRuntime();
if (cancelled) return;
const app = new runtime.PIXI.Application({
width: size,
height: size,
antialias: true,
autoDensity: true,
backgroundAlpha: 0,
resolution: Math.min(window.devicePixelRatio || 1, 2),
});
appRef.current = app;
const canvas = app.view as HTMLCanvasElement;
canvas.className = "vg-avatar__live2d-canvas";
canvas.setAttribute("aria-hidden", "true");
host.appendChild(canvas);
const model = (await runtime.Live2DModel.from(modelUrl, {
autoInteract: false,
autoUpdate: !snapshotRef.current.reduced,
motionPreload: runtime.MotionPreloadStrategy.IDLE,
})) as MutableLive2DModel;
if (cancelled) {
model.destroy({ children: true, texture: true, baseTexture: true });
app.destroy(true, { children: true, texture: true, baseTexture: true });
return;
}
modelRef.current = model;
fitModel(model, size);
app.stage.addChild(model);
app.ticker.add(tick);
void tryMotion(model, ["Idle", "idle"]);
onReady();
} catch (err) {
if (!cancelled) {
const reason = err instanceof Error ? err.message : "Live2D model load failed.";
onUnavailable(reason);
}
}
})();
return () => {
cancelled = true;
const app = appRef.current;
const model = modelRef.current;
if (app) app.ticker.remove(tick);
modelRef.current = null;
appRef.current = null;
if (model && !model.destroyed) {
model.destroy({ children: true, texture: true, baseTexture: true });
}
if (app) {
app.destroy(true, { children: true, texture: true, baseTexture: true });
}
host.replaceChildren();
};
}, [modelUrl, onReady, onUnavailable, size]);
return <div className="vg-avatar__live2d" ref={hostRef} aria-hidden="true" />;
}

View file

@ -34,8 +34,6 @@ export interface AvatarPersona {
* affect="resistant" 0.6, 0 .
*/
resistance?: number;
/** Optional persona-specific Live2D Cubism 3/4 model3.json URL. Omit for SVG fallback. */
live2dModelUrl?: string | null;
}
/* 6 (§4.5)

View file

@ -3,8 +3,6 @@
interface ImportMetaEnv {
/** API 베이스 URL. 기본 "/api" (vite proxy / nginx 가 백엔드로 라우팅). */
readonly VITE_API_BASE?: string;
/** 선택 Cubism Core JS URL. 기본 /live2d/live2dcubismcore.min.js. */
readonly VITE_LIVE2D_CUBISM_CORE?: string;
}
interface ImportMeta {