feat(pets): replace static cats with Live2D LittleCat model (black texture)

- Copied LittleCat model files to frontend/public/live2d/models/little-cat/
- Using the black alternate texture as default
- Created Live2DCat component that renders the model in a small canvas
- PetZone now shows a single Live2D cat instead of two SVG cats
This commit is contained in:
2026-06-05 12:55:24 -04:00
parent 15199dfdee
commit 017c81cffa
22 changed files with 2040 additions and 69 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

@@ -0,0 +1,271 @@
{
"Version": 3,
"Parameters": [
{
"Id": "ParamAngleX",
"GroupId": "",
"Name": "Angle X"
},
{
"Id": "ParamAngleY",
"GroupId": "",
"Name": "Angle Y"
},
{
"Id": "ParamAngleZ",
"GroupId": "",
"Name": "Angle Z"
},
{
"Id": "ParamEyeLOpen",
"GroupId": "",
"Name": "EyeL Open"
},
{
"Id": "ParamEyeLSmile",
"GroupId": "",
"Name": "EyeL Smile"
},
{
"Id": "ParamEyeROpen",
"GroupId": "",
"Name": "EyeR Open"
},
{
"Id": "ParamEyeRSmile",
"GroupId": "",
"Name": "EyeR Smile"
},
{
"Id": "ParamEyeBallX",
"GroupId": "",
"Name": "Eyeball X"
},
{
"Id": "ParamEyeBallY",
"GroupId": "",
"Name": "Eyeball Y"
},
{
"Id": "ParamBrowLY",
"GroupId": "",
"Name": "BrowL Y"
},
{
"Id": "ParamBrowRY",
"GroupId": "",
"Name": "BrowR Y"
},
{
"Id": "ParamBrowLX",
"GroupId": "",
"Name": "BrowL X"
},
{
"Id": "ParamBrowRX",
"GroupId": "",
"Name": "BrowR X"
},
{
"Id": "ParamBrowLAngle",
"GroupId": "",
"Name": "BrowL Angle"
},
{
"Id": "ParamBrowRAngle",
"GroupId": "",
"Name": "BrowR Angle"
},
{
"Id": "ParamBrowLForm",
"GroupId": "",
"Name": "BrowL Form"
},
{
"Id": "ParamBrowRForm",
"GroupId": "",
"Name": "BrowR Form"
},
{
"Id": "ParamMouthForm",
"GroupId": "",
"Name": "Mouth Form"
},
{
"Id": "ParamMouthOpenY",
"GroupId": "",
"Name": "Mouth Open"
},
{
"Id": "ParamCheek",
"GroupId": "",
"Name": "Cheek"
},
{
"Id": "ParamBodyAngleX",
"GroupId": "",
"Name": "Body X"
},
{
"Id": "ParamBodyAngleY",
"GroupId": "",
"Name": "Body Y"
},
{
"Id": "ParamBodyAngleZ",
"GroupId": "",
"Name": "Body Z"
},
{
"Id": "ParamBreath",
"GroupId": "",
"Name": "Breathing"
},
{
"Id": "ParamArms",
"GroupId": "",
"Name": "Arms"
},
{
"Id": "Param_Angle_Rotation2",
"GroupId": "ParamGroup",
"Name": "[0]tail wiggle"
},
{
"Id": "Param_Angle_Rotation3",
"GroupId": "ParamGroup",
"Name": "[1]tail wiggle"
},
{
"Id": "Param_Angle_Rotation4",
"GroupId": "ParamGroup",
"Name": "[2]tail wiggle"
},
{
"Id": "Param_Angle_Rotation5",
"GroupId": "ParamGroup",
"Name": "[3]tail wiggle"
},
{
"Id": "Param_Angle_Rotation6",
"GroupId": "ParamGroup",
"Name": "[4]tail wiggle"
},
{
"Id": "Param_Angle_Rotation7",
"GroupId": "ParamGroup",
"Name": "[5]tail wiggle"
},
{
"Id": "Param_Angle_Rotation10",
"GroupId": "ParamGroup2",
"Name": "[0]ear L wiggle"
},
{
"Id": "Param_Angle_Rotation11",
"GroupId": "ParamGroup2",
"Name": "[1]ear L wiggle"
},
{
"Id": "Param_Angle_Rotation12",
"GroupId": "ParamGroup3",
"Name": "[0]ear R wiggle"
},
{
"Id": "Param_Angle_Rotation13",
"GroupId": "ParamGroup3",
"Name": "[1]ear R wiggle"
}
],
"ParameterGroups": [
{
"Id": "ParamGroup",
"GroupId": "",
"Name": "tail wiggle"
},
{
"Id": "ParamGroup2",
"GroupId": "",
"Name": "ear L wiggle"
},
{
"Id": "ParamGroup3",
"GroupId": "",
"Name": "ear L wiggle"
}
],
"Parts": [
{
"Id": "Part",
"Name": "ear L"
},
{
"Id": "headfolder",
"Name": "head"
},
{
"Id": "Part3",
"Name": "ear R"
},
{
"Id": "tail_Skinning",
"Name": "tail(Skinning)"
},
{
"Id": "PartSketch0",
"Name": "[ Guide Image]"
},
{
"Id": "ArtMesh_Skinning2",
"Name": "inner ear L(Skinning)"
},
{
"Id": "ArtMesh_Skinning",
"Name": "inner ear L(Skinning)"
},
{
"Id": "ArtMesh0_Skinning2",
"Name": "ear L(Skinning)"
},
{
"Id": "ArtMesh0_Skinning",
"Name": "ear L(Skinning)"
},
{
"Id": "face",
"Name": "face"
},
{
"Id": "ArtMesh4_Skinning2",
"Name": "inner ear R(Skinning)"
},
{
"Id": "ArtMesh4_Skinning",
"Name": "inner ear R(Skinning)"
},
{
"Id": "ArtMesh5_Skinning2",
"Name": "ear R(Skinning)"
},
{
"Id": "ArtMesh5_Skinning",
"Name": "ear R(Skinning)"
},
{
"Id": "mouth",
"Name": "mouth"
},
{
"Id": "base",
"Name": "base"
},
{
"Id": "open",
"Name": "open"
},
{
"Id": "Part2",
"Name": "inner mouth"
}
]
}
@@ -0,0 +1,29 @@
{
"Version": 3,
"FileReferences": {
"Moc": "LittleCat.moc3",
"Textures": [
"LittleCat.2048/texture_00.png"
],
"Physics": "LittleCat.physics3.json",
"DisplayInfo": "LittleCat.cdi3.json"
},
"Groups": [
{
"Target": "Parameter",
"Name": "EyeBlink",
"Ids": [
"ParamEyeLOpen",
"ParamEyeROpen"
]
},
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": [
"ParamMouthForm",
"ParamMouthOpenY"
]
}
]
}
@@ -0,0 +1,451 @@
{
"Version": 3,
"Meta": {
"PhysicsSettingCount": 3,
"TotalInputCount": 8,
"TotalOutputCount": 11,
"VertexCount": 16,
"EffectiveForces": {
"Gravity": {
"X": 0,
"Y": -1
},
"Wind": {
"X": 0,
"Y": 0
}
},
"PhysicsDictionary": [
{
"Id": "PhysicsSetting1",
"Name": "tail wiggle"
},
{
"Id": "PhysicsSetting2",
"Name": "ear wiggle"
},
{
"Id": "PhysicsSetting3",
"Name": "arms"
}
]
},
"PhysicsSettings": [
{
"Id": "PhysicsSetting1",
"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": "Param_Angle_Rotation2"
},
"VertexIndex": 1,
"Scale": 33.072,
"Weight": 100,
"Type": "Angle",
"Reflect": false
},
{
"Destination": {
"Target": "Parameter",
"Id": "Param_Angle_Rotation3"
},
"VertexIndex": 2,
"Scale": 33.978,
"Weight": 100,
"Type": "Angle",
"Reflect": false
},
{
"Destination": {
"Target": "Parameter",
"Id": "Param_Angle_Rotation4"
},
"VertexIndex": 3,
"Scale": 34.41,
"Weight": 100,
"Type": "Angle",
"Reflect": false
},
{
"Destination": {
"Target": "Parameter",
"Id": "Param_Angle_Rotation5"
},
"VertexIndex": 4,
"Scale": 32.674,
"Weight": 100,
"Type": "Angle",
"Reflect": false
},
{
"Destination": {
"Target": "Parameter",
"Id": "Param_Angle_Rotation6"
},
"VertexIndex": 5,
"Scale": 27.941,
"Weight": 100,
"Type": "Angle",
"Reflect": false
},
{
"Destination": {
"Target": "Parameter",
"Id": "Param_Angle_Rotation7"
},
"VertexIndex": 6,
"Scale": 24.302,
"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.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 20
},
"Mobility": 0.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 30
},
"Mobility": 0.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 40
},
"Mobility": 0.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 50
},
"Mobility": 0.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 60
},
"Mobility": 0.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 70
},
"Mobility": 0.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 80
},
"Mobility": 0.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 90
},
"Mobility": 0.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 100
},
"Mobility": 0.85,
"Delay": 0.85,
"Acceleration": 1.2,
"Radius": 10
}
],
"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": "Param_Angle_Rotation10"
},
"VertexIndex": 1,
"Scale": 7.857,
"Weight": 100,
"Type": "Angle",
"Reflect": false
},
{
"Destination": {
"Target": "Parameter",
"Id": "Param_Angle_Rotation11"
},
"VertexIndex": 2,
"Scale": 8.996,
"Weight": 100,
"Type": "Angle",
"Reflect": false
},
{
"Destination": {
"Target": "Parameter",
"Id": "Param_Angle_Rotation12"
},
"VertexIndex": 1,
"Scale": 7.857,
"Weight": 100,
"Type": "Angle",
"Reflect": false
},
{
"Destination": {
"Target": "Parameter",
"Id": "Param_Angle_Rotation13"
},
"VertexIndex": 2,
"Scale": 8.996,
"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.97,
"Delay": 0.8,
"Acceleration": 0.5,
"Radius": 10
},
{
"Position": {
"X": 0,
"Y": 18
},
"Mobility": 0.95,
"Delay": 0.8,
"Acceleration": 0.8,
"Radius": 8
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting3",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleY"
},
"Weight": 100,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamArms"
},
"VertexIndex": 1,
"Scale": 32.201,
"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
}
}
}
]
}
+87
View File
@@ -0,0 +1,87 @@
import { useEffect, useRef } from 'react';
interface Props {
className?: string;
}
export default function Live2DCat({ className }: Props) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let mounted = true;
const canvas = canvasRef.current;
if (!canvas) return;
let app: any = null;
const init = async () => {
try {
await loadScript('/live2d/cubism/live2dcubismcore.min.js');
const { Application, Ticker } = await import('pixi.js');
const { Live2DModel } = await import('pixi-live2d-display/cubism4');
(Live2DModel as any).registerTicker(Ticker as any);
const w = canvas.clientWidth || 120;
const h = canvas.clientHeight || 120;
app = new Application({
view: canvas,
width: w,
height: h,
antialias: true,
resolution: Math.min(window.devicePixelRatio || 1, 2),
backgroundAlpha: 0,
autoDensity: true,
});
if (!mounted) { app.destroy(true); return; }
const model = await Live2DModel.from('/live2d/models/little-cat/LittleCat.model3.json', {
autoInteract: false,
});
if (!mounted) { app.destroy(true); return; }
// Scale to fit the small canvas
const sw = app.screen.width;
const sh = app.screen.height;
const s = Math.min((sw * 0.85) / model.width, (sh * 0.85) / model.height);
model.scale.set(s);
model.anchor.set(0.5, 0.5);
model.position.set(sw / 2, sh / 2);
app.stage.addChild(model as any);
(model as any).isInteractive = () => false;
try { model.motion('Idle'); } catch { /* */ }
} catch (e) {
console.warn('[Live2DCat]', e);
}
};
init();
return () => {
mounted = false;
if (app) { app.destroy(true, { children: true }); }
};
}, []);
return (
<canvas
ref={canvasRef}
className={className}
style={{ display: 'block' }}
/>
);
}
function loadScript(src: string): Promise<void> {
return new Promise((resolve, reject) => {
if (document.querySelector('script[src="' + src + '"]')) { resolve(); return; }
const s = document.createElement('script');
s.src = src;
s.onload = () => resolve();
s.onerror = () => reject(new Error('Failed ' + src));
document.head.appendChild(s);
});
}
+6 -68
View File
@@ -1,79 +1,17 @@
import Live2DCat from './Live2DCat';
export default function PetZone() {
return (
<div className="p-4 relative overflow-hidden" style={{ minHeight: 120 }}>
<div className="p-4 relative overflow-hidden" style={{ minHeight: 140 }}>
<h3 className="text-sm font-bold text-kira-plum mb-2 flex items-center gap-2">
<span>🐱</span> Pet Zone
</h3>
<div className="flex items-end justify-around gap-4">
{/* Orange fluffy cat */}
<div className="flex flex-col items-center animate-bounce-slow">
<div className="relative">
{/* Body */}
<div className="w-14 h-10 bg-orange-300 rounded-3xl rounded-br-xl relative">
{/* Head */}
<div className="w-10 h-9 bg-orange-300 rounded-full absolute -top-5 left-2">
{/* Ears */}
<div className="absolute -top-2 left-0 w-3 h-3 bg-orange-300 rounded-tl-full transform -rotate-12" />
<div className="absolute -top-2 right-0 w-3 h-3 bg-orange-300 rounded-tr-full transform rotate-12" />
{/* Eyes */}
<div className="absolute top-2 left-1.5 w-2 h-2.5 bg-amber-800 rounded-full" />
<div className="absolute top-2 right-1.5 w-2 h-2.5 bg-amber-800 rounded-full" />
{/* Nose */}
<div className="absolute top-3.5 left-3.5 w-1.5 h-1 bg-pink-300 rounded-full" />
</div>
{/* Tail */}
<div className="absolute -right-3 top-1 w-8 h-2.5 bg-orange-300 rounded-full origin-left rotate-12" />
</div>
{/* Paws */}
<div className="flex gap-3 mt-0.5 ml-2">
<div className="w-2.5 h-1.5 bg-orange-200 rounded-full" />
<div className="w-2.5 h-1.5 bg-orange-200 rounded-full" />
</div>
</div>
<div className="flex items-center justify-center">
<div className="flex flex-col items-center">
<Live2DCat className="w-28 h-28" />
<span className="text-[10px] text-kira-plum/60 mt-1 font-medium">Mochi</span>
</div>
{/* Black shorthair cat - sleeping */}
<div className="flex flex-col items-center">
<div className="relative animate-float-slow">
{/* Sleeping body (curled up) */}
<div className="w-16 h-8 bg-gray-900 rounded-full relative overflow-hidden">
{/* Head tucked in */}
<div className="w-8 h-6 bg-gray-800 rounded-full absolute -left-2 -top-1">
{/* Ears */}
<div className="absolute -top-1.5 left-1 w-2.5 h-2.5 bg-gray-900 rounded-tl-full transform -rotate-12" />
<div className="absolute -top-1.5 right-1 w-2.5 h-2.5 bg-gray-900 rounded-tr-full transform rotate-12" />
{/* Closed eyes (sleeping) */}
<div className="absolute top-2 left-1 w-2 h-0.5 bg-gray-600 rounded-full" />
<div className="absolute top-2 right-1 w-2 h-0.5 bg-gray-600 rounded-full" />
</div>
{/* Tail curled around */}
<div className="absolute -right-2 top-2 w-6 h-2 bg-gray-900 rounded-full" />
</div>
{/* ZZZ */}
<div className="absolute -top-3 -right-2 text-[10px] text-kira-lav font-bold opacity-60 animate-zzz">Z z z</div>
</div>
<span className="text-[10px] text-kira-plum/60 mt-1 font-medium">Luna</span>
</div>
</div>
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
@keyframes float-slow {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-2px) scale(1.01); }
}
@keyframes zzz {
0%, 100% { opacity: 0.3; transform: translateX(0); }
50% { opacity: 0.8; transform: translateX(3px); }
}
.animate-bounce-slow { animation: bounce-slow 3s ease-in-out infinite; }
.animate-float-slow { animation: float-slow 4s ease-in-out infinite; }
.animate-zzz { animation: zzz 2s ease-in-out infinite; }
`}</style>
</div>
);
}
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AnimatedAvatar.tsx","./src/components/BackgroundScene.tsx","./src/components/ChatBubble.tsx","./src/components/Clock.tsx","./src/components/KiraAvatar.tsx","./src/components/MusicPlayer.tsx","./src/components/Notes.tsx","./src/components/Particles.tsx","./src/components/PetZone.tsx","./src/components/Timer.tsx","./src/components/Toolbar.tsx","./src/components/Wardrobe.tsx","./src/components/WelcomeScreen.tsx","./src/components/WhiteNoise.tsx","./src/components/scenes.ts","./src/hooks/useConversation.ts","./src/types/index.ts"],"version":"6.0.3"}
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AnimatedAvatar.tsx","./src/components/BackgroundScene.tsx","./src/components/ChatBubble.tsx","./src/components/Clock.tsx","./src/components/KiraAvatar.tsx","./src/components/Live2DCat.tsx","./src/components/MusicPlayer.tsx","./src/components/Notes.tsx","./src/components/Particles.tsx","./src/components/PetZone.tsx","./src/components/Timer.tsx","./src/components/Toolbar.tsx","./src/components/Wardrobe.tsx","./src/components/WelcomeScreen.tsx","./src/components/WhiteNoise.tsx","./src/components/scenes.ts","./src/hooks/useConversation.ts","./src/types/index.ts"],"version":"6.0.3"}