zyc 72e7df09cd Initial commit: AR avatar prototype
包含三个子项目:
- avatar-h5-renderer: Live2D Cubism 4 H5 渲染器 (Vite + TS)
- avatar_flutter_app: Flutter 容器 App (打包 H5 进 WebView)
- gif-export: puppeteer 导出 32 个动作的透明 GIF (供 ESP32 圆屏播放)

模型资源: Haru, Natori (含贴图、moc3、motions, expressions)
设计文档: AI驱动虚拟形象渲染方案_v5.1.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:14:10 +08:00

98 lines
3.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
/// Flutter → H5 桥接层。
///
/// 把 [window.avatar] 暴露的 JS API 包装成 Dart 方法。
/// 内部只通过 [evaluateJavascript] 调用,未来切到 Cubism Native 时
/// 实现这一层的另一个版本即可UI 完全无需改动。
class AvatarBridge {
final InAppWebViewController _ctrl;
AvatarBridge(this._ctrl);
/// 等待 H5 端的 window.avatar 注册完成且模型加载到 expression/motion 可用
Future<void> waitReady({Duration timeout = const Duration(seconds: 15)}) async {
final deadline = DateTime.now().add(timeout);
while (DateTime.now().isBefore(deadline)) {
final result = await _ctrl.evaluateJavascript(source: '''
(function() {
if (!window.avatar) return false;
const exps = window.avatar.listExpressions();
const mots = window.avatar.listMotions();
return exps.length > 0 || Object.keys(mots).length > 0;
})()
''');
if (result == true) return;
await Future.delayed(const Duration(milliseconds: 200));
}
if (kDebugMode) {
debugPrint('[AvatarBridge] waitReady timed out');
}
}
Future<void> setState(String state) => _eval('avatar.setState("$state")');
Future<void> setExpression(String name) =>
_eval('avatar.setExpression("$name")');
Future<void> playMotion(String group, int index) =>
_eval('avatar.playMotion("$group", $index)');
Future<void> setMouthOpen(double value) =>
_eval('avatar.setMouthOpen($value)');
Future<void> playAction(String name) =>
_eval('avatar.playAction("$name")');
Future<void> startDance() => _eval('avatar.startDance()');
Future<void> stopDance() => _eval('avatar.stopDance()');
Future<void> playMockConversation() => _eval('avatar.playMockConversation()');
/// 拉一次资产清单(表情名、动作分组、语义动作列表)
Future<AvatarManifest> fetchManifest() async {
final raw = await _ctrl.evaluateJavascript(source: '''
JSON.stringify({
expressions: avatar.listExpressions(),
motions: avatar.listMotions(),
actions: avatar.listActions(),
})
''');
if (raw is! String) {
return const AvatarManifest(expressions: [], motions: {}, actions: []);
}
final json = jsonDecode(raw) as Map<String, dynamic>;
final expressions =
(json['expressions'] as List).map((e) => e as String).toList();
final motions = (json['motions'] as Map<String, dynamic>).map(
(k, v) => MapEntry(k, v as int),
);
final actions =
(json['actions'] as List).map((e) => e as String).toList();
return AvatarManifest(
expressions: expressions,
motions: motions,
actions: actions,
);
}
Future<dynamic> _eval(String source) => _ctrl.evaluateJavascript(source: source);
}
/// 当前角色资产清单(表情名 + motion 分组 + 语义动作)
class AvatarManifest {
final List<String> expressions;
final Map<String, int> motions;
final List<String> actions;
const AvatarManifest({
required this.expressions,
required this.motions,
required this.actions,
});
}