@umituz/react-native-mascot 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/package.json +89 -0
- package/skills/SKILL.md +188 -0
- package/src/assets/index.ts +104 -0
- package/src/domain/entities/Mascot.ts +216 -0
- package/src/domain/interfaces/IAnimationController.ts +78 -0
- package/src/domain/interfaces/IAssetManager.ts +51 -0
- package/src/domain/interfaces/IMascotRepository.ts +39 -0
- package/src/domain/types/MascotTypes.ts +75 -0
- package/src/index.ts +99 -0
- package/src/infrastructure/assets/lottie/dance.json +61 -0
- package/src/infrastructure/assets/lottie/error.json +48 -0
- package/src/infrastructure/assets/lottie/idle.json +49 -0
- package/src/infrastructure/assets/lottie/jump.json +48 -0
- package/src/infrastructure/assets/lottie/success.json +46 -0
- package/src/infrastructure/assets/lottie/wave.json +46 -0
- package/src/infrastructure/assets/svg/cartoon-bot.svg +20 -0
- package/src/infrastructure/assets/svg/minimal-cat.svg +19 -0
- package/src/infrastructure/controllers/AnimationController.ts +163 -0
- package/src/infrastructure/managers/AssetManager.ts +159 -0
- package/src/infrastructure/managers/MascotFactory.ts +239 -0
- package/src/infrastructure/repositories/MascotRepository.ts +93 -0
- package/src/presentation/components/MascotView.tsx +296 -0
- package/src/presentation/contexts/MascotContext.tsx +141 -0
- package/src/presentation/hooks/useMascot.ts +224 -0
- package/src/presentation/hooks/useMascotAnimation.ts +168 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"v": "5.9.0",
|
|
3
|
+
"fr": 60,
|
|
4
|
+
"ip": 0,
|
|
5
|
+
"op": 120,
|
|
6
|
+
"w": 200,
|
|
7
|
+
"h": 200,
|
|
8
|
+
"nm": "Dance",
|
|
9
|
+
"ddd": 0,
|
|
10
|
+
"assets": [],
|
|
11
|
+
"layers": [
|
|
12
|
+
{
|
|
13
|
+
"ddd": 0,
|
|
14
|
+
"ind": 1,
|
|
15
|
+
"ty": 4,
|
|
16
|
+
"nm": "Body",
|
|
17
|
+
"ks": {
|
|
18
|
+
"o": { "a": 0, "k": 100 },
|
|
19
|
+
"r": { "a": 1, "k": [
|
|
20
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 0, "s": [0] },
|
|
21
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 15, "s": [15] },
|
|
22
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 30, "s": [-15] },
|
|
23
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 45, "s": [15] },
|
|
24
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 60, "s": [-15] },
|
|
25
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 75, "s": [15] },
|
|
26
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 90, "s": [-15] },
|
|
27
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 105, "s": [15] },
|
|
28
|
+
{ "t": 120, "s": [0] }
|
|
29
|
+
]},
|
|
30
|
+
"p": {
|
|
31
|
+
"a": 1,
|
|
32
|
+
"k": [
|
|
33
|
+
{ "i": { "x": 0.833, "y": 0.833 }, "o": { "x": 0.167, "y": 0.167 }, "t": 0, "s": [100, 100] },
|
|
34
|
+
{ "i": { "x": 0.833, "y": 0 }, "o": { "x": 0.167, "y": 1 }, "t": 30, "s": [100, 90] },
|
|
35
|
+
{ "i": { "x": 0.833, "y": 0.833 }, "o": { "x": 0.167, "y": 0.167 }, "t": 60, "s": [100, 100] },
|
|
36
|
+
{ "i": { "x": 0.833, "y": 0 }, "o": { "x": 0.167, "y": 1 }, "t": 90, "s": [100, 90] },
|
|
37
|
+
{ "t": 120, "s": [100, 100] }
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"a": { "a": 0, "k": [0, 0] },
|
|
41
|
+
"s": { "a": 0, "k": [100, 100] }
|
|
42
|
+
},
|
|
43
|
+
"shapes": [
|
|
44
|
+
{
|
|
45
|
+
"ty": "gr",
|
|
46
|
+
"it": [
|
|
47
|
+
{
|
|
48
|
+
"ty": "el",
|
|
49
|
+
"s": { "a": 0, "k": [150, 150] },
|
|
50
|
+
"p": { "a": 0, "k": [0, 0] }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"ty": "fl",
|
|
54
|
+
"c": { "a": 0, "k": [0.95, 0.5, 0.35, 1] }
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"v": "5.9.0",
|
|
3
|
+
"fr": 60,
|
|
4
|
+
"ip": 0,
|
|
5
|
+
"op": 60,
|
|
6
|
+
"w": 200,
|
|
7
|
+
"h": 200,
|
|
8
|
+
"nm": "Error",
|
|
9
|
+
"ddd": 0,
|
|
10
|
+
"assets": [],
|
|
11
|
+
"layers": [
|
|
12
|
+
{
|
|
13
|
+
"ddd": 0,
|
|
14
|
+
"ind": 1,
|
|
15
|
+
"ty": 4,
|
|
16
|
+
"nm": "Body",
|
|
17
|
+
"ks": {
|
|
18
|
+
"o": { "a": 0, "k": 100 },
|
|
19
|
+
"r": { "a": 1, "k": [
|
|
20
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 0, "s": [0] },
|
|
21
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 15, "s": [-10] },
|
|
22
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 30, "s": [10] },
|
|
23
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 45, "s": [-10] },
|
|
24
|
+
{ "t": 60, "s": [0] }
|
|
25
|
+
]},
|
|
26
|
+
"p": { "a": 0, "k": [100, 100] },
|
|
27
|
+
"a": { "a": 0, "k": [0, 0] },
|
|
28
|
+
"s": { "a": 0, "k": [100, 100] }
|
|
29
|
+
},
|
|
30
|
+
"shapes": [
|
|
31
|
+
{
|
|
32
|
+
"ty": "gr",
|
|
33
|
+
"it": [
|
|
34
|
+
{
|
|
35
|
+
"ty": "el",
|
|
36
|
+
"s": { "a": 0, "k": [150, 150] },
|
|
37
|
+
"p": { "a": 0, "k": [0, 0] }
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"ty": "fl",
|
|
41
|
+
"c": { "a": 0, "k": [0.9, 0.3, 0.3, 1] }
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"v": "5.9.0",
|
|
3
|
+
"fr": 60,
|
|
4
|
+
"ip": 0,
|
|
5
|
+
"op": 60,
|
|
6
|
+
"w": 200,
|
|
7
|
+
"h": 200,
|
|
8
|
+
"nm": "Idle",
|
|
9
|
+
"ddd": 0,
|
|
10
|
+
"assets": [],
|
|
11
|
+
"layers": [
|
|
12
|
+
{
|
|
13
|
+
"ddd": 0,
|
|
14
|
+
"ind": 1,
|
|
15
|
+
"ty": 4,
|
|
16
|
+
"nm": "Body",
|
|
17
|
+
"sr": 1,
|
|
18
|
+
"ks": {
|
|
19
|
+
"o": { "a": 0, "k": 100 },
|
|
20
|
+
"r": {
|
|
21
|
+
"a": 1,
|
|
22
|
+
"k": [
|
|
23
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 0, "s": [0] },
|
|
24
|
+
{ "t": 60, "s": [5] }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"p": { "a": 0, "k": [100, 100] },
|
|
28
|
+
"a": { "a": 0, "k": [0, 0] },
|
|
29
|
+
"s": { "a": 0, "k": [100, 100] }
|
|
30
|
+
},
|
|
31
|
+
"shapes": [
|
|
32
|
+
{
|
|
33
|
+
"ty": "gr",
|
|
34
|
+
"it": [
|
|
35
|
+
{
|
|
36
|
+
"ty": "el",
|
|
37
|
+
"s": { "a": 0, "k": [150, 150] },
|
|
38
|
+
"p": { "a": 0, "k": [0, 0] }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"ty": "fl",
|
|
42
|
+
"c": { "a": 0, "k": [0.95, 0.5, 0.35, 1] }
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"v": "5.9.0",
|
|
3
|
+
"fr": 60,
|
|
4
|
+
"ip": 0,
|
|
5
|
+
"op": 60,
|
|
6
|
+
"w": 200,
|
|
7
|
+
"h": 200,
|
|
8
|
+
"nm": "Jump",
|
|
9
|
+
"ddd": 0,
|
|
10
|
+
"assets": [],
|
|
11
|
+
"layers": [
|
|
12
|
+
{
|
|
13
|
+
"ddd": 0,
|
|
14
|
+
"ind": 1,
|
|
15
|
+
"ty": 4,
|
|
16
|
+
"nm": "Body",
|
|
17
|
+
"ks": {
|
|
18
|
+
"o": { "a": 0, "k": 100 },
|
|
19
|
+
"p": {
|
|
20
|
+
"a": 1,
|
|
21
|
+
"k": [
|
|
22
|
+
{ "i": { "x": 0.833, "y": 0.833 }, "o": { "x": 0.167, "y": 0.167 }, "t": 0, "s": [100, 120] },
|
|
23
|
+
{ "i": { "x": 0.833, "y": 0 }, "o": { "x": 0.167, "y": 1 }, "t": 30, "s": [100, 60] },
|
|
24
|
+
{ "t": 60, "s": [100, 120] }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"a": { "a": 0, "k": [0, 0] },
|
|
28
|
+
"s": { "a": 0, "k": [100, 100] }
|
|
29
|
+
},
|
|
30
|
+
"shapes": [
|
|
31
|
+
{
|
|
32
|
+
"ty": "gr",
|
|
33
|
+
"it": [
|
|
34
|
+
{
|
|
35
|
+
"ty": "el",
|
|
36
|
+
"s": { "a": 0, "k": [150, 150] },
|
|
37
|
+
"p": { "a": 0, "k": [0, 0] }
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"ty": "fl",
|
|
41
|
+
"c": { "a": 0, "k": [0.95, 0.5, 0.35, 1] }
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"v": "5.9.0",
|
|
3
|
+
"fr": 60,
|
|
4
|
+
"ip": 0,
|
|
5
|
+
"op": 90,
|
|
6
|
+
"w": 200,
|
|
7
|
+
"h": 200,
|
|
8
|
+
"nm": "Success",
|
|
9
|
+
"ddd": 0,
|
|
10
|
+
"assets": [],
|
|
11
|
+
"layers": [
|
|
12
|
+
{
|
|
13
|
+
"ddd": 0,
|
|
14
|
+
"ind": 1,
|
|
15
|
+
"ty": 4,
|
|
16
|
+
"nm": "Body",
|
|
17
|
+
"ks": {
|
|
18
|
+
"o": { "a": 0, "k": 100 },
|
|
19
|
+
"r": { "a": 1, "k": [
|
|
20
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 0, "s": [0] },
|
|
21
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 45, "s": [360] },
|
|
22
|
+
{ "t": 90, "s": [360] }
|
|
23
|
+
]},
|
|
24
|
+
"p": { "a": 0, "k": [100, 100] },
|
|
25
|
+
"a": { "a": 0, "k": [0, 0] },
|
|
26
|
+
"s": { "a": 0, "k": [100, 100] }
|
|
27
|
+
},
|
|
28
|
+
"shapes": [
|
|
29
|
+
{
|
|
30
|
+
"ty": "gr",
|
|
31
|
+
"it": [
|
|
32
|
+
{
|
|
33
|
+
"ty": "el",
|
|
34
|
+
"s": { "a": 0, "k": [150, 150] },
|
|
35
|
+
"p": { "a": 0, "k": [0, 0] }
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"ty": "fl",
|
|
39
|
+
"c": { "a": 0, "k": [0.2, 0.8, 0.4, 1] }
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"v": "5.9.0",
|
|
3
|
+
"fr": 60,
|
|
4
|
+
"ip": 0,
|
|
5
|
+
"op": 90,
|
|
6
|
+
"w": 200,
|
|
7
|
+
"h": 200,
|
|
8
|
+
"nm": "Wave",
|
|
9
|
+
"ddd": 0,
|
|
10
|
+
"assets": [],
|
|
11
|
+
"layers": [
|
|
12
|
+
{
|
|
13
|
+
"ddd": 0,
|
|
14
|
+
"ind": 1,
|
|
15
|
+
"ty": 4,
|
|
16
|
+
"nm": "Body",
|
|
17
|
+
"ks": {
|
|
18
|
+
"o": { "a": 0, "k": 100 },
|
|
19
|
+
"r": { "a": 1, "k": [
|
|
20
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 0, "s": [-20] },
|
|
21
|
+
{ "i": { "x": [0.833], "y": [0.833] }, "o": { "x": [0.167], "y": [0.167] }, "t": 30, "s": [20] },
|
|
22
|
+
{ "t": 60, "s": [-20] }
|
|
23
|
+
]},
|
|
24
|
+
"p": { "a": 0, "k": [100, 100] },
|
|
25
|
+
"a": { "a": 0, "k": [0, 0] },
|
|
26
|
+
"s": { "a": 0, "k": [100, 100] }
|
|
27
|
+
},
|
|
28
|
+
"shapes": [
|
|
29
|
+
{
|
|
30
|
+
"ty": "gr",
|
|
31
|
+
"it": [
|
|
32
|
+
{
|
|
33
|
+
"ty": "el",
|
|
34
|
+
"s": { "a": 0, "k": [150, 150] },
|
|
35
|
+
"p": { "a": 0, "k": [0, 0] }
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"ty": "fl",
|
|
39
|
+
"c": { "a": 0, "k": [0.95, 0.5, 0.35, 1] }
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Body -->
|
|
3
|
+
<rect x="25" y="30" width="50" height="45" rx="10" fill="currentColor" id="body"/>
|
|
4
|
+
<!-- Head -->
|
|
5
|
+
<rect x="20" y="20" width="60" height="40" rx="8" fill="currentColor" id="head"/>
|
|
6
|
+
<!-- Antenna -->
|
|
7
|
+
<line x1="50" y1="20" x2="50" y2="10" stroke="#666" stroke-width="3" id="antenna"/>
|
|
8
|
+
<circle cx="50" cy="8" r="4" fill="#FF6B6B" id="antenna-ball"/>
|
|
9
|
+
<!-- Eyes -->
|
|
10
|
+
<circle cx="35" cy="38" r="8" fill="#FFF" id="left-eye-bg"/>
|
|
11
|
+
<circle cx="35" cy="38" r="4" fill="#00F" id="left-eye"/>
|
|
12
|
+
<circle cx="65" cy="38" r="8" fill="#FFF" id="right-eye-bg"/>
|
|
13
|
+
<circle cx="65" cy="38" r="4" fill="#00F" id="right-eye"/>
|
|
14
|
+
<!-- Mouth -->
|
|
15
|
+
<rect x="40" y="48" width="20" height="6" rx="3" fill="#333" id="mouth"/>
|
|
16
|
+
<!-- Buttons -->
|
|
17
|
+
<circle cx="40" cy="65" r="3" fill="#FFD700" id="button-1"/>
|
|
18
|
+
<circle cx="50" cy="65" r="3" fill="#FFD700" id="button-2"/>
|
|
19
|
+
<circle cx="60" cy="65" r="3" fill="#FFD700" id="button-3"/>
|
|
20
|
+
</svg>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Base face -->
|
|
3
|
+
<circle cx="50" cy="50" r="40" fill="currentColor" id="face"/>
|
|
4
|
+
<!-- Ears -->
|
|
5
|
+
<path d="M 20 30 L 15 15 L 35 25 Z" fill="currentColor" id="left-ear"/>
|
|
6
|
+
<path d="M 80 30 L 85 15 L 65 25 Z" fill="currentColor" id="right-ear"/>
|
|
7
|
+
<!-- Eyes -->
|
|
8
|
+
<circle cx="35" cy="45" r="5" fill="#000" id="left-eye"/>
|
|
9
|
+
<circle cx="65" cy="45" r="5" fill="#000" id="right-eye"/>
|
|
10
|
+
<!-- Nose -->
|
|
11
|
+
<path d="M 48 55 L 52 55 L 50 58 Z" fill="#FF9999" id="nose"/>
|
|
12
|
+
<!-- Mouth -->
|
|
13
|
+
<path d="M 45 60 Q 50 65 55 60" stroke="#000" stroke-width="2" fill="none" id="mouth"/>
|
|
14
|
+
<!-- Whiskers -->
|
|
15
|
+
<line x1="20" y1="55" x2="35" y2="58" stroke="#000" stroke-width="1" id="left-whisker-1"/>
|
|
16
|
+
<line x1="20" y1="60" x2="35" y2="60" stroke="#000" stroke-width="1" id="left-whisker-2"/>
|
|
17
|
+
<line x1="65" y1="58" x2="80" y2="55" stroke="#000" stroke-width="1" id="right-whisker-1"/>
|
|
18
|
+
<line x1="65" y1="60" x2="80" y2="60" stroke="#000" stroke-width="1" id="right-whisker-2"/>
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation Controller Implementation
|
|
3
|
+
* Controls Lottie and SVG animations with unified API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
IAnimationController,
|
|
8
|
+
AnimationOptions,
|
|
9
|
+
AnimationEvent,
|
|
10
|
+
} from '../../domain/interfaces/IAnimationController';
|
|
11
|
+
import type { MascotAnimation } from '../../domain/types/MascotTypes';
|
|
12
|
+
|
|
13
|
+
export class AnimationController implements IAnimationController {
|
|
14
|
+
private _currentAnimation: MascotAnimation | null = null;
|
|
15
|
+
private _isPlaying: boolean = false;
|
|
16
|
+
private _isPaused: boolean = false;
|
|
17
|
+
private _progress: number = 0;
|
|
18
|
+
private _speed: number = 1;
|
|
19
|
+
private _eventListeners: Map<AnimationEvent, Set<(data?: unknown) => void>>;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this._eventListeners = new Map();
|
|
23
|
+
this._initializeEventListeners();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
play(
|
|
27
|
+
animation: MascotAnimation,
|
|
28
|
+
options?: AnimationOptions
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
this._currentAnimation = animation;
|
|
31
|
+
this._isPlaying = true;
|
|
32
|
+
this._isPaused = false;
|
|
33
|
+
|
|
34
|
+
if (options?.speed !== undefined) {
|
|
35
|
+
this._speed = options.speed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this._emit('start', { animation: animation.id });
|
|
39
|
+
|
|
40
|
+
// Simulate animation completion
|
|
41
|
+
// In real implementation, this would be controlled by LottieView
|
|
42
|
+
const duration = animation.duration || 2000;
|
|
43
|
+
const adjustedDuration = duration / this._speed;
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
if (this._isPlaying && !this._isPaused) {
|
|
48
|
+
this._isPlaying = false;
|
|
49
|
+
this._progress = 1;
|
|
50
|
+
this._emit('finish', { animation: animation.id });
|
|
51
|
+
options?.onFinish?.();
|
|
52
|
+
}
|
|
53
|
+
resolve();
|
|
54
|
+
}, adjustedDuration);
|
|
55
|
+
|
|
56
|
+
options?.onStart?.();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pause(): void {
|
|
61
|
+
if (!this._isPlaying || this._isPaused) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this._isPaused = true;
|
|
66
|
+
this._emit('pause');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resume(): void {
|
|
70
|
+
if (!this._isPaused) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this._isPaused = false;
|
|
75
|
+
this._emit('resume');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stop(): void {
|
|
79
|
+
this._isPlaying = false;
|
|
80
|
+
this._isPaused = false;
|
|
81
|
+
this._progress = 0;
|
|
82
|
+
this._currentAnimation = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getProgress(): number {
|
|
86
|
+
return this._progress;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setProgress(progress: number): void {
|
|
90
|
+
if (progress < 0 || progress > 1) {
|
|
91
|
+
throw new Error('Progress must be between 0 and 1');
|
|
92
|
+
}
|
|
93
|
+
this._progress = progress;
|
|
94
|
+
this._emit('progress', { progress });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setSpeed(speed: number): void {
|
|
98
|
+
if (speed <= 0) {
|
|
99
|
+
throw new Error('Speed must be greater than 0');
|
|
100
|
+
}
|
|
101
|
+
this._speed = speed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
isPlaying(): boolean {
|
|
105
|
+
return this._isPlaying && !this._isPaused;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
on(event: AnimationEvent, callback: (data?: unknown) => void): () => void {
|
|
109
|
+
if (!this._eventListeners.has(event)) {
|
|
110
|
+
this._eventListeners.set(event, new Set());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this._eventListeners.get(event)!.add(callback);
|
|
114
|
+
|
|
115
|
+
// Return unsubscribe function
|
|
116
|
+
return () => {
|
|
117
|
+
this.off(event, callback);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
off(event: AnimationEvent, callback: (data?: unknown) => void): void {
|
|
122
|
+
const listeners = this._eventListeners.get(event);
|
|
123
|
+
if (listeners) {
|
|
124
|
+
listeners.delete(callback);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Private Methods
|
|
129
|
+
private _initializeEventListeners(): void {
|
|
130
|
+
const events: AnimationEvent[] = ['start', 'finish', 'pause', 'resume', 'progress', 'error'];
|
|
131
|
+
events.forEach((event) => {
|
|
132
|
+
if (!this._eventListeners.has(event)) {
|
|
133
|
+
this._eventListeners.set(event, new Set());
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private _emit(event: AnimationEvent, data?: unknown): void {
|
|
139
|
+
const listeners = this._eventListeners.get(event);
|
|
140
|
+
if (listeners) {
|
|
141
|
+
listeners.forEach((callback) => {
|
|
142
|
+
try {
|
|
143
|
+
callback(data);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`Error in ${event} event listener:`, error);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Getters
|
|
152
|
+
get currentAnimation(): MascotAnimation | null {
|
|
153
|
+
return this._currentAnimation;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get speed(): number {
|
|
157
|
+
return this._speed;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get isPaused(): boolean {
|
|
161
|
+
return this._isPaused;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset Manager Implementation
|
|
3
|
+
* Loads and caches Lottie and SVG assets
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
IAssetManager,
|
|
8
|
+
AssetCache,
|
|
9
|
+
} from '../../domain/interfaces/IAssetManager';
|
|
10
|
+
import type { MascotAnimation } from '../../domain/types/MascotTypes';
|
|
11
|
+
|
|
12
|
+
export class AssetManager implements IAssetManager {
|
|
13
|
+
private readonly _cache: AssetCache;
|
|
14
|
+
private readonly _loadedAssets: Set<string>;
|
|
15
|
+
private readonly _maxCacheSize: number = 50 * 1024 * 1024; // 50MB
|
|
16
|
+
private _currentCacheSize: number = 0;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this._cache = {};
|
|
20
|
+
this._loadedAssets = new Set();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
loadLottieAnimation(
|
|
24
|
+
source: string | object
|
|
25
|
+
): Promise<MascotAnimation> {
|
|
26
|
+
const assetId = this._getAssetId(source);
|
|
27
|
+
|
|
28
|
+
if (this._isAssetLoaded(assetId)) {
|
|
29
|
+
return Promise.resolve(this._cache[assetId].data as MascotAnimation);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Simulate loading - in real implementation, this would actually load the file
|
|
33
|
+
const animation: MascotAnimation = {
|
|
34
|
+
id: assetId,
|
|
35
|
+
name: typeof source === 'string' ? source.split('/').pop() || 'unknown' : 'custom',
|
|
36
|
+
type: 'idle',
|
|
37
|
+
source,
|
|
38
|
+
loop: false,
|
|
39
|
+
autoplay: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this._cacheAsset(assetId, animation);
|
|
43
|
+
return Promise.resolve(animation);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
loadSVGAsset(source: string): Promise<string> {
|
|
47
|
+
if (this._isAssetLoaded(source)) {
|
|
48
|
+
return Promise.resolve(this._cache[source].data as string);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Simulate loading - in real implementation, this would load the SVG file
|
|
52
|
+
const svgContent = this._loadSVGFromFile(source);
|
|
53
|
+
this._cacheAsset(source, svgContent);
|
|
54
|
+
return Promise.resolve(svgContent);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async preloadAnimations(sources: Array<string | object>): Promise<void> {
|
|
58
|
+
const promises = sources.map((source) =>
|
|
59
|
+
this.loadLottieAnimation(source)
|
|
60
|
+
);
|
|
61
|
+
await Promise.all(promises);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clearCache(): void {
|
|
65
|
+
Object.keys(this._cache).forEach((key) => {
|
|
66
|
+
delete this._cache[key];
|
|
67
|
+
});
|
|
68
|
+
this._loadedAssets.clear();
|
|
69
|
+
this._currentCacheSize = 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getAssetUrl(assetId: string): string | null {
|
|
73
|
+
if (this._isAssetLoaded(assetId)) {
|
|
74
|
+
return assetId;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
isAssetLoaded(assetId: string): boolean {
|
|
80
|
+
return this._loadedAssets.has(assetId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getLoadedAssets(): string[] {
|
|
84
|
+
return Array.from(this._loadedAssets);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Private Methods
|
|
88
|
+
private _getAssetId(source: string | object): string {
|
|
89
|
+
if (typeof source === 'string') {
|
|
90
|
+
return source;
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify(source);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private _cacheAsset(assetId: string, data: unknown): void {
|
|
96
|
+
const size = this._estimateSize(data);
|
|
97
|
+
|
|
98
|
+
// Check if cache is full
|
|
99
|
+
if (this._currentCacheSize + size > this._maxCacheSize) {
|
|
100
|
+
this._evictOldestAssets();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this._cache[assetId] = {
|
|
104
|
+
data,
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
size,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
this._loadedAssets.add(assetId);
|
|
110
|
+
this._currentCacheSize += size;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private _isAssetLoaded(assetId: string): boolean {
|
|
114
|
+
return this._loadedAssets.has(assetId) && !!this._cache[assetId];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _evictOldestAssets(): void {
|
|
118
|
+
const sortedAssets = Object.entries(this._cache)
|
|
119
|
+
.sort(([, a], [, b]) => a.timestamp - b.timestamp);
|
|
120
|
+
|
|
121
|
+
let freedSpace = 0;
|
|
122
|
+
const targetSpace = this._maxCacheSize * 0.3; // Evict 30% of cache
|
|
123
|
+
|
|
124
|
+
for (const [assetId, asset] of sortedAssets) {
|
|
125
|
+
if (freedSpace >= targetSpace) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
delete this._cache[assetId];
|
|
130
|
+
this._loadedAssets.delete(assetId);
|
|
131
|
+
freedSpace += asset.size;
|
|
132
|
+
this._currentCacheSize -= asset.size;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private _estimateSize(data: unknown): number {
|
|
137
|
+
// Rough estimation in bytes
|
|
138
|
+
return JSON.stringify(data).length * 2; // 2 bytes per char (UTF-16)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private _loadSVGFromFile(_source: string): string {
|
|
142
|
+
// In real implementation, this would use FileSystem or require()
|
|
143
|
+
// For now, return a placeholder
|
|
144
|
+
return `<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="currentColor"/></svg>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Getters
|
|
148
|
+
get cacheSize(): number {
|
|
149
|
+
return this._currentCacheSize;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get cacheStats(): { size: number; count: number; maxSize: number } {
|
|
153
|
+
return {
|
|
154
|
+
size: this._currentCacheSize,
|
|
155
|
+
count: this._loadedAssets.size,
|
|
156
|
+
maxSize: this._maxCacheSize,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|