Unsere Rive-Animations-Pipeline: Arabische Figuren lebendig gemacht
Amal nutzt Rive (ehemals Flare) für alle Figurenanimationen – einschließlich lippen-synchronisiertem Sprechen, Avatar-Anpassung, Rückmeldungsreaktionen und Spielfiguren. Wir entschieden uns für Rive statt Lottie oder Sprite-Sheets, weil es Laufzeit-Zustandsmaschinen, programmatische Manipulation und GPU-beschleunigtes Rendering bei 60fps in einer einzigen kompakten Datei pro Charakter unterstützt.
Die Animationsressourcenbibliothek
Kernfiguren
lip-sync-amal-01.riv
- Haupt-Amal-Figur (Ganzkörper- und nur-Gesicht-Varianten)
- Mehrere Artboards pro Mundposition (für Phonem-Mapping)
- Zustände: leerlauf, spricht, Fehler, Feier, Schlafen
- Dateigröße: 1,2 MB (vs. 50+ MB für Sprite-Sheets)
avatar.riv
- Anpassbarer Benutzer-Avatar (3 Artboards)
- Ganzkörper: Kopf, Rumpf, Gliedmaßen mit Kleidung
- Nur-Kopf: für Dashboard und Eltern-App
- Schmetterlings-Begleiter: Belohnungsanimation
- Komponentenbasiert: Kopfform, Haare, Augen, Kleidung, Accessoires, Farben
- Dateigröße: 2,4 MB
coin-01.riv & coins-01.riv
- Belohnungsanimationen (Münzen, die schweben und gesammelt werden)
- Einzelne Münze: 150 KB
- Mehrere Münzen: 300 KB
cute-monster-final.riv
- Rückmeldecharakter mit mehreren emotionalen Zuständen
- Zustände: glücklich (richtige Antwort), verwirrt (falsch), nachdenklich (Verarbeitung), Feier (Streak)
- Dateigröße: 1,8 MB
Android-spezifische Optimierung
- Benutzerdefinierter NDK-Build (Rive NDK-r28) für 16KB-Seitenausrichtungs-Konformität
- Reduziert Binärgröße um 8% im Vergleich zum Standard-Build
- Sorgt für Kompatibilität mit aggressivem Speichermanagement in Android 12+
Lippen-Synchronisations-Pipeline (Technische Detailtiefe)
Schritt 1: TTS-Audioerzeugung + Sprachauszeichnungsextraktion
# src/services/tts_client.py
from google.cloud import texttospeech
def generate_speech_with_marks(text: str, language: str = 'ar-SA'):
client = texttospeech.TextToSpeechClient()
synthesis_input = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(
language_code=language,
name='ar-SA-Neural2-A' # WaveNet-Stimme
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
effects_profile_id=['small-bluetooth-speaker-class-device'] # Lautsprecher für Kinder
)
# Anfordern von Sprachauszeichnungen (Phonem-Timing)
request = texttospeech.SynthesizeSpeechRequest(
input=synthesis_input,
voice=voice,
audio_config=audio_config,
enable_text_to_speech_as_cloud_service=True
)
response = client.synthesize_speech(request=request)
# Antwort beinhaltet:
# - audio_content: MP3-Bytes
# - Zeitpunkte: [{Zeichen, Byte-Pos, Zeit_ms}, ...]
return {
'audio': response.audio_content,
'speech_marks': response.timepoints # Phonem-Level-Zeitstempel
}
Schritt 2: Zuordnung von Phonemen zu Rive-Mundzuständen
lip_sync_avatar.json ordnet arabische Phoneme Mundpositionen zu:
{
"phoneme_map": {
"ا": { "rive_state": "mouth_a_open", "duration_ms": 200 },
"ب": { "rive_state": "mouth_lips_closed", "duration_ms": 150 },
"ع": { "rive_state": "mouth_pharyngeal", "duration_ms": 250 },
"ق": { "rive_state": "mouth_uvular", "duration_ms": 180 },
...
},
"mouth_positions": [
{ "id": "mouth_a_open", "blend_values": { "jaw_open": 0.8, "lips_rounded": 0.2 } },
{ "id": "mouth_lips_closed", "blend_values": { "jaw_open": 0.1, "lips_rounded": 0.9 } },
...
]
}
Schritt 3: LipSync-Controller orchestriert Wiedergabe
// lib/src/modules/animations/controllers/lip_sync_controller.dart
class LipSyncController extends GetxController {
late Rive riveCharacter;
late AudioPlayer audioPlayer;
late LipSyncMapper mapper;
void playWithLipSync(String text, String audioPath) {
// Schritt 1: Rive-Charakter laden
riveCharacter.loadRiveFile('lip-sync-amal-01.riv');
// Schritt 2: Sprachauszeichnungen aus TTS-Ausgabe laden
mapper = LipSyncMapper.fromJson(loadJsonAsset('lip_sync_avatar.json'));
// Schritt 3: Audio abspielen und Mundanimation steuern
audioPlayer.play(AudioSource.file(audioPath));
// Schritt 4: Bei jedem Audio-Frame Mundposition aktualisieren
audioPlayer.onPositionChanged.listen((Duration position) {
String phoneme = mapper.phonemeAtTime(position.inMilliseconds);
String riveState = mapper.riveStateForPhoneme(phoneme);
riveCharacter.setStateInput('mouth_state', riveState);
});
}
}
Schritt 4: RiveCharacterController verwaltet Lebenszyklus
// Verwalten des vollständigen Animationszustands (nicht nur Mund)
class RiveCharacterController extends GetxController {
States: leerlauf → vorbereiten → sprechen → leerlauf → Fehler/Feier
void startExercise() {
// Charakterübergänge: leerlauf → vorbereiten (bereit zum Zuhören)
character.setStateInput('state_machine', 'prepare');
}
void childSpeaks(String recognizedText, double accuracy) {
character.setStateInput('state_machine', 'speaking');
lipSyncController.playFeedback(recognizedText);
}
void onFeedbackComplete(bool wasCorrect) {
if (wasCorrect) {
character.setStateInput('state_machine', 'celebrate');
playRewardAnimation();
} else {
character.setStateInput('state_machine', 'error');
playEncouragingPhrase();
}
}
}
Avatar-Anpassungssystem
Komponentenbasierte Architektur
Kinder passen ihren Avatar aus Teilen an:
{
"avatar_customization": {
"head_shapes": [
{ "id": "round", "rive_element": "head_round" },
{ "id": "oval", "rive_element": "head_oval" },
{ "id": "square", "rive_element": "head_square" }
],
"hair_styles": [
{ "id": "ponytail", "rive_element": "hair_ponytail" },
{ "id": "braids", "rive_element": "hair_braids" },
{ "id": "straight", "rive_element": "hair_straight" }
],
"colors": {
"skin_tone": ["light", "medium", "dark"],
"hair_color": ["black", "brown", "blonde", "red"],
"shirt_color": ["blue", "pink", "green", "yellow", "purple"],
"accent_color": ["red", "orange", "green", "blue"]
}
}
}
Rive-Benannte-Elemente-Zuordnung (avatar_customization_rive_names.dart)
const avatarRiveNames = {
'head_round': 'Head_Round',
'head_oval': 'Head_Oval',
'hair_ponytail': 'Hair_Ponytail',
'shirt_blue': 'Shirt_Blue',
'shirt_pink': 'Shirt_Pink',
// ... 50+ Element-Zuordnungen
};
Wenn ein Kind "runder Kopf + blaues Hemd" auswählt, tut die App:
- Rive-Element
Head_Roundaktivieren - Rive-Element
Shirt_Blueaktivieren - Alle anderen Kopfformen und Hemdfarben deaktivieren
- Der personalisierte Avatar des Kindes erscheint nun in der gesamten App
Warum Rive über Alternativen
| Feature | Rive | Lottie | Sprite-Sheets | Video |
|---|---|---|---|---|
| Zustandsmaschinen | ✓ | ✗ | ✗ | ✗ |
| Laufzeitkontrolle | ✓ (vollständig) | Teilweise | Manuell | ✗ (passiv) |
| Dateigröße | 1-2 MB | 2-3 MB | 50+ MB | 100+ MB |
| Leistung | 60fps GPU | 30fps CPU | 60fps GPU | Variabel |
| Interaktivität | ✓ Vollständig | ✓ Teilweise | ✓ Vollständig | ✗ Keine |
| Lernkurve | Mittel | Einfach | Einfach | Einfach |
| Wartung | Eine .riv-Datei | Eine JSON | Hunderte von Bildern | Ein Video |
Rive gewinnt, weil wir programmatische Kontrolle, Zustandsmaschinen und Kompaktheit für eine mobile App benötigen.
Leistungsoptimierung
- Vorladen von Charakteren:
.riv-Dateien während des App-Starts laden, nicht pro Übung - GPU-Rendering: Rive nutzt automatisch GPU, wenn verfügbar, CPU-Fallback auf alten Geräten
- Speicherpooling: Wiederverwendung von Rive-Controllern über Bildschirme hinweg, um Garbage Collection-Pausen zu vermeiden
- Komprimierung: Rive-Dateien sind bereits komprimiert; keine zusätzliche Optimierung erforderlich
Ergebnis: 60fps-Animationen auf Snapdragon 662+ (2019-Mittelklasse-Handys).
FAQ
F: Kann ich Animationen von Adobe Animate zu Rive exportieren? A: Nicht direkt. Wir verwenden Rives nativen Editor (rive.app). Animatoren gestalten in Rive, nicht in Animate oder After Effects. Der Workflow ist: Charakter in Rive gestalten → als .riv exportieren → in Flutter-App integrieren.
F: Wie handhabt ihr unterschiedliche Körpertypen oder Behinderungen? A: Die Avatar-Anpassung umfasst Körperoptionen (schlank, athletisch, rund) und Accessoires (Brillen, Hörgeräte, Mobilitätshilfen). Dies sorgt dafür, dass alle Kinder Repräsentation sehen.
F: Was, wenn ein Kind seinen Avatar nicht mag? A: Es kann jederzeit angepasst werden. Die App zwingt kein bestimmtes Aussehen auf – Kinder haben volle kreative Kontrolle.



