@umituz/react-native-mascot 1.0.4 → 1.0.8
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/README.md +60 -0
- package/package.json +2 -1
- package/src/application/services/AnimationStateManager.ts +69 -0
- package/src/application/services/AppearanceManagement.ts +40 -0
- package/src/application/services/MascotService.ts +42 -33
- package/src/application/services/PersonalityManagement.ts +39 -0
- package/src/application/services/StateHistory.ts +55 -0
- package/src/application/services/StateMachine.ts +154 -0
- package/src/application/services/StateTransitions.ts +73 -0
- package/src/application.ts +40 -0
- package/src/assets/index.ts +14 -19
- package/src/core.ts +62 -0
- package/src/domain/entities/Mascot.ts +186 -127
- package/src/domain/types/AnimationStateTypes.ts +148 -0
- package/src/domain/types/MascotTypes.ts +9 -0
- package/src/domain/value-objects/AnimationState.ts +126 -0
- package/src/index.ts +9 -99
- package/src/infrastructure/controllers/AnimationController.ts +26 -122
- package/src/infrastructure/controllers/AnimationPlayer.ts +104 -0
- package/src/infrastructure/controllers/AnimationTimer.ts +62 -0
- package/src/infrastructure/controllers/EventManager.ts +108 -0
- package/src/infrastructure/di/Container.ts +73 -10
- package/src/infrastructure/managers/AssetManager.ts +134 -63
- package/src/infrastructure/managers/MascotBuilder.ts +89 -0
- package/src/infrastructure/managers/MascotFactory.ts +24 -176
- package/src/infrastructure/managers/MascotTemplates.ts +151 -0
- package/src/infrastructure/utils/LRUCache.ts +218 -0
- package/src/infrastructure.ts +24 -0
- package/src/presentation/components/LottieMascot.tsx +85 -0
- package/src/presentation/components/MascotView.tsx +42 -233
- package/src/presentation/components/SVGMascot.tsx +61 -0
- package/src/presentation/contexts/MascotContext.tsx +2 -3
- package/src/presentation/hooks/useMascot.ts +118 -39
- package/src/presentation/hooks/useMascotAnimation.ts +9 -15
- package/src/presentation/hooks/useMascotState.ts +213 -0
- package/src/presentation.ts +37 -0
- package/src/types.d.ts +4 -0
- package/src/application/index.ts +0 -8
- package/src/domain/value-objects/index.ts +0 -9
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation State Types
|
|
3
|
+
* State-based animation system inspired by AIStylistMascot implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Predefined animation states with specific behaviors
|
|
8
|
+
*/
|
|
9
|
+
export type MascotAnimationState =
|
|
10
|
+
| 'idle' // Calm breathing, default state (loop)
|
|
11
|
+
| 'loading' // Active processing, rotation/swirl (loop)
|
|
12
|
+
| 'success' // Confirmation, scale pulse (non-loop)
|
|
13
|
+
| 'error' // Error acknowledgment, shake (non-loop)
|
|
14
|
+
| 'empty' // Empty state, inviting gesture (loop)
|
|
15
|
+
| 'guide'; // Onboarding assistance (loop)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* State configuration for each animation state
|
|
19
|
+
*/
|
|
20
|
+
export interface MascotStateConfig {
|
|
21
|
+
state: MascotAnimationState;
|
|
22
|
+
loop: boolean;
|
|
23
|
+
duration?: number; // in milliseconds
|
|
24
|
+
autoPlay?: boolean;
|
|
25
|
+
speed?: number;
|
|
26
|
+
onComplete?: () => void;
|
|
27
|
+
transitionTo?: MascotAnimationState; // Auto-transition after completion
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* State transition rules
|
|
32
|
+
*/
|
|
33
|
+
export interface StateTransition {
|
|
34
|
+
from: MascotAnimationState;
|
|
35
|
+
to: MascotAnimationState;
|
|
36
|
+
condition?: 'always' | 'on-success' | 'on-error' | 'on-complete';
|
|
37
|
+
delay?: number; // Delay before transition (ms)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* State history for tracking and debugging
|
|
42
|
+
*/
|
|
43
|
+
export interface StateHistoryEntry {
|
|
44
|
+
state: MascotAnimationState;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
duration?: number;
|
|
47
|
+
triggeredBy?: 'user' | 'system' | 'auto-transition';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Size variants for mascot display
|
|
52
|
+
*/
|
|
53
|
+
export type MascotSize = 'small' | 'medium' | 'large' | number;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Size configuration in pixels
|
|
57
|
+
*/
|
|
58
|
+
export interface MascotSizeConfig {
|
|
59
|
+
small: number;
|
|
60
|
+
medium: number;
|
|
61
|
+
large: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Default state configurations
|
|
66
|
+
*/
|
|
67
|
+
export const DEFAULT_STATE_CONFIGS: Record<MascotAnimationState, MascotStateConfig> = {
|
|
68
|
+
idle: {
|
|
69
|
+
state: 'idle',
|
|
70
|
+
loop: true,
|
|
71
|
+
duration: 3000,
|
|
72
|
+
autoPlay: true,
|
|
73
|
+
speed: 1,
|
|
74
|
+
},
|
|
75
|
+
loading: {
|
|
76
|
+
state: 'loading',
|
|
77
|
+
loop: true,
|
|
78
|
+
duration: 2000,
|
|
79
|
+
autoPlay: true,
|
|
80
|
+
speed: 1,
|
|
81
|
+
},
|
|
82
|
+
success: {
|
|
83
|
+
state: 'success',
|
|
84
|
+
loop: false,
|
|
85
|
+
duration: 1000,
|
|
86
|
+
autoPlay: true,
|
|
87
|
+
speed: 1,
|
|
88
|
+
transitionTo: 'idle',
|
|
89
|
+
},
|
|
90
|
+
error: {
|
|
91
|
+
state: 'error',
|
|
92
|
+
loop: false,
|
|
93
|
+
duration: 500,
|
|
94
|
+
autoPlay: true,
|
|
95
|
+
speed: 1,
|
|
96
|
+
transitionTo: 'idle',
|
|
97
|
+
},
|
|
98
|
+
empty: {
|
|
99
|
+
state: 'empty',
|
|
100
|
+
loop: true,
|
|
101
|
+
duration: 3000,
|
|
102
|
+
autoPlay: true,
|
|
103
|
+
speed: 1,
|
|
104
|
+
},
|
|
105
|
+
guide: {
|
|
106
|
+
state: 'guide',
|
|
107
|
+
loop: true,
|
|
108
|
+
duration: 2000,
|
|
109
|
+
autoPlay: true,
|
|
110
|
+
speed: 1,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Default size configurations in pixels
|
|
116
|
+
*/
|
|
117
|
+
export const DEFAULT_SIZE_CONFIG: MascotSizeConfig = {
|
|
118
|
+
small: 40,
|
|
119
|
+
medium: 80,
|
|
120
|
+
large: 120,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* State transition mappings
|
|
125
|
+
*/
|
|
126
|
+
export const STATE_TRANSITIONS: Record<MascotAnimationState, StateTransition[]> = {
|
|
127
|
+
idle: [
|
|
128
|
+
{ from: 'idle', to: 'loading', condition: 'always' },
|
|
129
|
+
{ from: 'idle', to: 'empty', condition: 'always' },
|
|
130
|
+
{ from: 'idle', to: 'guide', condition: 'always' },
|
|
131
|
+
],
|
|
132
|
+
loading: [
|
|
133
|
+
{ from: 'loading', to: 'success', condition: 'on-success' },
|
|
134
|
+
{ from: 'loading', to: 'error', condition: 'on-error' },
|
|
135
|
+
],
|
|
136
|
+
success: [
|
|
137
|
+
{ from: 'success', to: 'idle', condition: 'on-complete', delay: 100 },
|
|
138
|
+
],
|
|
139
|
+
error: [
|
|
140
|
+
{ from: 'error', to: 'idle', condition: 'on-complete', delay: 100 },
|
|
141
|
+
],
|
|
142
|
+
empty: [
|
|
143
|
+
{ from: 'empty', to: 'idle', condition: 'always' },
|
|
144
|
+
],
|
|
145
|
+
guide: [
|
|
146
|
+
{ from: 'guide', to: 'idle', condition: 'on-complete', delay: 200 },
|
|
147
|
+
],
|
|
148
|
+
};
|
|
@@ -73,3 +73,12 @@ export interface MascotInteraction {
|
|
|
73
73
|
handler: () => void | Promise<void>;
|
|
74
74
|
animation?: string;
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
export interface AnimationOptions {
|
|
78
|
+
speed?: number;
|
|
79
|
+
loop?: boolean;
|
|
80
|
+
autoplay?: boolean;
|
|
81
|
+
onStart?: () => void;
|
|
82
|
+
onFinish?: () => void;
|
|
83
|
+
onError?: (error: Error) => void;
|
|
84
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation State Value Object
|
|
3
|
+
* Encapsulates animation state logic and validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
MascotAnimationState,
|
|
8
|
+
MascotStateConfig,
|
|
9
|
+
StateTransition,
|
|
10
|
+
} from '../types/AnimationStateTypes';
|
|
11
|
+
import { DEFAULT_STATE_CONFIGS, STATE_TRANSITIONS } from '../types/AnimationStateTypes';
|
|
12
|
+
|
|
13
|
+
export class AnimationState {
|
|
14
|
+
private constructor(
|
|
15
|
+
public readonly value: MascotAnimationState,
|
|
16
|
+
private readonly _config: MascotStateConfig
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
static create(state: MascotAnimationState, config?: Partial<MascotStateConfig>): AnimationState {
|
|
20
|
+
const defaultConfig = DEFAULT_STATE_CONFIGS[state];
|
|
21
|
+
const finalConfig = { ...defaultConfig, ...config, state };
|
|
22
|
+
return new AnimationState(state, finalConfig);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if state should loop
|
|
27
|
+
*/
|
|
28
|
+
shouldLoop(): boolean {
|
|
29
|
+
return this._config.loop;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get animation duration
|
|
34
|
+
*/
|
|
35
|
+
getDuration(): number {
|
|
36
|
+
return this._config.duration || 3000;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if animation should auto-play
|
|
41
|
+
*/
|
|
42
|
+
shouldAutoPlay(): boolean {
|
|
43
|
+
return this._config.autoPlay ?? true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get animation speed
|
|
48
|
+
*/
|
|
49
|
+
getSpeed(): number {
|
|
50
|
+
return this._config.speed || 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get next state after completion (for auto-transition)
|
|
55
|
+
*/
|
|
56
|
+
getNextState(): MascotAnimationState | null {
|
|
57
|
+
return this._config.transitionTo || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get available transitions from current state
|
|
62
|
+
*/
|
|
63
|
+
getAvailableTransitions(): StateTransition[] {
|
|
64
|
+
return STATE_TRANSITIONS[this.value] || [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if transition to target state is allowed
|
|
69
|
+
*/
|
|
70
|
+
canTransitionTo(targetState: MascotAnimationState): boolean {
|
|
71
|
+
const transitions = this.getAvailableTransitions();
|
|
72
|
+
return transitions.some(t => t.to === targetState);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get transition delay to target state
|
|
77
|
+
*/
|
|
78
|
+
getTransitionDelay(targetState: MascotAnimationState): number {
|
|
79
|
+
const transitions = this.getAvailableTransitions();
|
|
80
|
+
const transition = transitions.find(t => t.to === targetState);
|
|
81
|
+
return transition?.delay || 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get completion callback
|
|
86
|
+
*/
|
|
87
|
+
getOnComplete(): (() => void) | undefined {
|
|
88
|
+
return this._config.onComplete;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if state is a success state
|
|
93
|
+
*/
|
|
94
|
+
isSuccessState(): boolean {
|
|
95
|
+
return this.value === 'success';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if state is an error state
|
|
100
|
+
*/
|
|
101
|
+
isErrorState(): boolean {
|
|
102
|
+
return this.value === 'error';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if state is a loading state
|
|
107
|
+
*/
|
|
108
|
+
isLoadingState(): boolean {
|
|
109
|
+
return this.value === 'loading';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if state is an idle state
|
|
114
|
+
*/
|
|
115
|
+
isIdleState(): boolean {
|
|
116
|
+
return this.value === 'idle';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
equals(other: AnimationState): boolean {
|
|
120
|
+
return this.value === other.value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
toJSON(): MascotStateConfig {
|
|
124
|
+
return { ...this._config };
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,109 +2,18 @@
|
|
|
2
2
|
* @umituz/react-native-mascot
|
|
3
3
|
*
|
|
4
4
|
* Interactive mascot system for React Native apps with DDD architecture
|
|
5
|
+
* Version: 1.0.6
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
// Domain - Value Objects
|
|
13
|
-
export { Mood, EnergyLevel, FriendlinessLevel, PlayfulnessLevel } from './domain/value-objects';
|
|
14
|
-
|
|
15
|
-
// Domain - Types
|
|
16
|
-
export type {
|
|
17
|
-
MascotType,
|
|
18
|
-
MascotStyle,
|
|
19
|
-
MascotAnimationType,
|
|
20
|
-
MascotAppearance,
|
|
21
|
-
MascotAccessory,
|
|
22
|
-
MascotPersonality,
|
|
23
|
-
MascotAnimation,
|
|
24
|
-
MascotConfig,
|
|
25
|
-
MascotState,
|
|
26
|
-
MascotInteraction,
|
|
27
|
-
MascotMood,
|
|
28
|
-
AnimationSpeed,
|
|
29
|
-
} from './domain/types/MascotTypes';
|
|
30
|
-
|
|
31
|
-
// Domain - Interfaces
|
|
32
|
-
export type {
|
|
33
|
-
IAnimationController,
|
|
34
|
-
AnimationEvent,
|
|
35
|
-
AnimationOptions,
|
|
36
|
-
} from './domain/interfaces/IAnimationController';
|
|
37
|
-
|
|
38
|
-
export type {
|
|
39
|
-
IAssetManager,
|
|
40
|
-
AssetCache,
|
|
41
|
-
} from './domain/interfaces/IAssetManager';
|
|
42
|
-
|
|
43
|
-
export type {
|
|
44
|
-
IMascotRepository,
|
|
45
|
-
} from './domain/interfaces/IMascotRepository';
|
|
46
|
-
|
|
47
|
-
// Application - Services
|
|
48
|
-
export { MascotService } from './application/services/MascotService';
|
|
49
|
-
export type { MascotTemplate } from './application/services/MascotService';
|
|
50
|
-
|
|
51
|
-
// Application - Errors
|
|
52
|
-
export {
|
|
53
|
-
MascotError,
|
|
54
|
-
MascotNotInitializedError,
|
|
55
|
-
AnimationNotFoundError,
|
|
56
|
-
InvalidEnergyLevelError,
|
|
57
|
-
InvalidFriendlinessLevelError,
|
|
58
|
-
InvalidPlayfulnessLevelError,
|
|
59
|
-
InvalidMoodTransitionError,
|
|
60
|
-
MascotNotFoundError,
|
|
61
|
-
TemplateNotFoundError,
|
|
62
|
-
} from './application/errors/MascotErrors';
|
|
63
|
-
|
|
64
|
-
// Application - DTOs
|
|
65
|
-
export type {
|
|
66
|
-
MascotDTO,
|
|
67
|
-
AnimationStateDTO,
|
|
68
|
-
MascotInitOptionsDTO,
|
|
69
|
-
AnimationPlaybackOptionsDTO,
|
|
70
|
-
MascotUpdateOptionsDTO,
|
|
71
|
-
} from './application/dto/MascotDTO';
|
|
72
|
-
|
|
73
|
-
// Infrastructure - DI
|
|
74
|
-
export { DIContainer } from './infrastructure/di/Container';
|
|
75
|
-
|
|
76
|
-
// Infrastructure - Repositories
|
|
77
|
-
export { MascotRepository } from './infrastructure/repositories/MascotRepository';
|
|
78
|
-
|
|
79
|
-
// Infrastructure - Controllers
|
|
80
|
-
export { AnimationController } from './infrastructure/controllers/AnimationController';
|
|
81
|
-
|
|
82
|
-
// Infrastructure - Managers
|
|
83
|
-
export { AssetManager } from './infrastructure/managers/AssetManager';
|
|
84
|
-
export { MascotFactory, type MascotTemplate as FactoryMascotTemplate } from './infrastructure/managers/MascotFactory';
|
|
85
|
-
|
|
86
|
-
// Presentation - Components
|
|
87
|
-
export { MascotView } from './presentation/components/MascotView';
|
|
88
|
-
export type { MascotViewProps } from './presentation/components/MascotView';
|
|
89
|
-
|
|
90
|
-
// Presentation - Hooks
|
|
91
|
-
export { useMascot } from './presentation/hooks/useMascot';
|
|
92
|
-
export type {
|
|
93
|
-
UseMascotOptions,
|
|
94
|
-
UseMascotReturn,
|
|
95
|
-
} from './presentation/hooks/useMascot';
|
|
96
|
-
|
|
97
|
-
export { useMascotAnimation } from './presentation/hooks/useMascotAnimation';
|
|
98
|
-
export type {
|
|
99
|
-
UseMascotAnimationOptions,
|
|
100
|
-
UseMascotAnimationReturn,
|
|
101
|
-
} from './presentation/hooks/useMascotAnimation';
|
|
102
|
-
|
|
103
|
-
// Presentation - Contexts
|
|
104
|
-
export { MascotProvider, useMascotContext } from './presentation/contexts/MascotContext';
|
|
105
|
-
export type { MascotProviderProps, MascotContextValue } from './presentation/contexts/MascotContext';
|
|
8
|
+
// Re-export all layers
|
|
9
|
+
export * from './core';
|
|
10
|
+
export * from './application';
|
|
11
|
+
export * from './infrastructure';
|
|
12
|
+
export * from './presentation';
|
|
106
13
|
|
|
107
14
|
// Constants
|
|
15
|
+
import type { MascotMood, AnimationSpeed } from './domain/types/MascotTypes';
|
|
16
|
+
|
|
108
17
|
export const MASCOT_TEMPLATES = [
|
|
109
18
|
'friendly-bot',
|
|
110
19
|
'cute-pet',
|
|
@@ -132,6 +41,7 @@ export const DEFAULT_ANIMATION_SPEEDS: AnimationSpeed[] = [
|
|
|
132
41
|
|
|
133
42
|
// Convenience export - get service instance
|
|
134
43
|
export const getMascotService = () => {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
135
45
|
const { DIContainer } = require('./infrastructure/di/Container');
|
|
136
46
|
return DIContainer.getInstance().getMascotService();
|
|
137
47
|
};
|
|
@@ -1,163 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Animation Controller
|
|
3
|
-
*
|
|
2
|
+
* Animation Controller (Main - 60 lines)
|
|
3
|
+
* Unified animation control interface
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
IAnimationController,
|
|
8
|
-
AnimationOptions,
|
|
9
|
-
AnimationEvent,
|
|
10
|
-
} from '../../domain/interfaces/IAnimationController';
|
|
6
|
+
import type { IAnimationController, AnimationOptions, AnimationEvent } from '../../domain/interfaces/IAnimationController';
|
|
11
7
|
import type { MascotAnimation } from '../../domain/types/MascotTypes';
|
|
8
|
+
import { AnimationPlayer } from './AnimationPlayer';
|
|
9
|
+
import { EventManager } from './EventManager';
|
|
12
10
|
|
|
13
11
|
export class AnimationController implements IAnimationController {
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private _isPaused: boolean = false;
|
|
17
|
-
private _progress: number = 0;
|
|
18
|
-
private _speed: number = 1;
|
|
19
|
-
private _eventListeners: Map<AnimationEvent, Set<(data?: unknown) => void>>;
|
|
12
|
+
private _player: AnimationPlayer;
|
|
13
|
+
private _events: EventManager;
|
|
20
14
|
|
|
21
15
|
constructor() {
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
16
|
+
this._player = new AnimationPlayer();
|
|
17
|
+
this._events = new EventManager();
|
|
24
18
|
}
|
|
25
19
|
|
|
26
|
-
play(
|
|
27
|
-
animation
|
|
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
|
-
});
|
|
20
|
+
play(animation: MascotAnimation, options?: AnimationOptions): Promise<void> {
|
|
21
|
+
return this._player.play(animation, options);
|
|
58
22
|
}
|
|
59
23
|
|
|
60
24
|
pause(): void {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
this._isPaused = true;
|
|
66
|
-
this._emit('pause');
|
|
25
|
+
this._player.pause();
|
|
26
|
+
this._events.emit('pause');
|
|
67
27
|
}
|
|
68
28
|
|
|
69
29
|
resume(): void {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
this._isPaused = false;
|
|
75
|
-
this._emit('resume');
|
|
30
|
+
this._player.resume();
|
|
31
|
+
this._events.emit('resume');
|
|
76
32
|
}
|
|
77
33
|
|
|
78
34
|
stop(): void {
|
|
79
|
-
this.
|
|
80
|
-
this._isPaused = false;
|
|
81
|
-
this._progress = 0;
|
|
82
|
-
this._currentAnimation = null;
|
|
35
|
+
this._player.stop();
|
|
83
36
|
}
|
|
84
37
|
|
|
85
38
|
getProgress(): number {
|
|
86
|
-
return this.
|
|
39
|
+
return this._player.progress;
|
|
87
40
|
}
|
|
88
41
|
|
|
89
42
|
setProgress(progress: number): void {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
this._progress = progress;
|
|
94
|
-
this._emit('progress', { progress });
|
|
43
|
+
this._player.setProgress(progress);
|
|
44
|
+
this._events.emit('progress', { progress });
|
|
95
45
|
}
|
|
96
46
|
|
|
97
47
|
setSpeed(speed: number): void {
|
|
98
|
-
|
|
99
|
-
throw new Error('Speed must be greater than 0');
|
|
100
|
-
}
|
|
101
|
-
this._speed = speed;
|
|
48
|
+
this._player.setSpeed(speed);
|
|
102
49
|
}
|
|
103
50
|
|
|
104
51
|
isPlaying(): boolean {
|
|
105
|
-
return this.
|
|
52
|
+
return this._player.isPlaying;
|
|
106
53
|
}
|
|
107
54
|
|
|
108
55
|
on(event: AnimationEvent, callback: (data?: unknown) => void): () => void {
|
|
109
|
-
|
|
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
|
-
};
|
|
56
|
+
return this._events.on(event, callback);
|
|
119
57
|
}
|
|
120
58
|
|
|
121
59
|
off(event: AnimationEvent, callback: (data?: unknown) => void): void {
|
|
122
|
-
|
|
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;
|
|
60
|
+
this._events.off(event, callback);
|
|
158
61
|
}
|
|
159
62
|
|
|
160
|
-
|
|
161
|
-
|
|
63
|
+
destroy(): void {
|
|
64
|
+
this._player.destroy();
|
|
65
|
+
this._events.clear();
|
|
162
66
|
}
|
|
163
67
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation Player (100 lines)
|
|
3
|
+
* Core playback logic for mascot animations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MascotAnimation } from '../../domain/types/MascotTypes';
|
|
7
|
+
import type { AnimationOptions } from '../../domain/interfaces/IAnimationController';
|
|
8
|
+
import { AnimationTimer } from './AnimationTimer';
|
|
9
|
+
|
|
10
|
+
export class AnimationPlayer {
|
|
11
|
+
private _currentAnimation: MascotAnimation | null = null;
|
|
12
|
+
private _isPlaying: boolean = false;
|
|
13
|
+
private _isPaused: boolean = false;
|
|
14
|
+
private _progress: number = 0;
|
|
15
|
+
private _speed: number = 1;
|
|
16
|
+
private readonly _timer: AnimationTimer;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this._timer = new AnimationTimer();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get currentAnimation(): MascotAnimation | null {
|
|
23
|
+
return this._currentAnimation;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get isPlaying(): boolean {
|
|
27
|
+
return this._isPlaying && !this._isPaused;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get progress(): number {
|
|
31
|
+
return this._progress;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get speed(): number {
|
|
35
|
+
return this._speed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
play(animation: MascotAnimation, options?: AnimationOptions): Promise<void> {
|
|
39
|
+
// Stop existing animation
|
|
40
|
+
this.stop();
|
|
41
|
+
|
|
42
|
+
this._currentAnimation = animation;
|
|
43
|
+
this._isPlaying = true;
|
|
44
|
+
this._isPaused = false;
|
|
45
|
+
|
|
46
|
+
if (options?.speed !== undefined) {
|
|
47
|
+
this._speed = options.speed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const duration = animation.duration || 2000;
|
|
51
|
+
const adjustedDuration = duration / this._speed;
|
|
52
|
+
|
|
53
|
+
// Start timer
|
|
54
|
+
this._timer.start(adjustedDuration, () => {
|
|
55
|
+
this._isPlaying = false;
|
|
56
|
+
this._progress = 1;
|
|
57
|
+
options?.onFinish?.();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
options?.onStart?.();
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pause(): void {
|
|
65
|
+
if (!this._isPlaying || this._isPaused) return;
|
|
66
|
+
|
|
67
|
+
this._isPaused = true;
|
|
68
|
+
this._timer.pause();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resume(): void {
|
|
72
|
+
if (!this._isPaused) return;
|
|
73
|
+
|
|
74
|
+
this._isPaused = false;
|
|
75
|
+
this._timer.resume();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stop(): void {
|
|
79
|
+
this._isPlaying = false;
|
|
80
|
+
this._isPaused = false;
|
|
81
|
+
this._progress = 0;
|
|
82
|
+
this._currentAnimation = null;
|
|
83
|
+
this._timer.stop();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setProgress(progress: number): void {
|
|
87
|
+
if (progress < 0 || progress > 1) {
|
|
88
|
+
throw new Error('Progress must be between 0 and 1');
|
|
89
|
+
}
|
|
90
|
+
this._progress = progress;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setSpeed(speed: number): void {
|
|
94
|
+
if (speed <= 0) {
|
|
95
|
+
throw new Error('Speed must be greater than 0');
|
|
96
|
+
}
|
|
97
|
+
this._speed = speed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
destroy(): void {
|
|
101
|
+
this.stop();
|
|
102
|
+
this._timer.destroy();
|
|
103
|
+
}
|
|
104
|
+
}
|