Add free full-body 3D live camera avatar examples

这个提交包含在:
cryptocommuniums-afk
2026-03-15 22:32:09 +08:00
父节点 fe5e539a47
当前提交 e3fe9a8e7b
修改 9 个文件,包含 326 行新增28 行删除

查看文件

@@ -19,9 +19,11 @@ import {
createEmptyStabilizedActionMeta,
createStableActionState,
drawLiveCameraOverlay,
getAvatarPreset,
resolveAvatarKeyFromPrompt,
stabilizeActionStream,
type AvatarKey,
type AvatarPreset,
type AvatarRenderState,
type FrameActionSample,
type LiveActionType,
@@ -32,6 +34,7 @@ import {
Camera,
CameraOff,
CheckCircle2,
ExternalLink,
FlipHorizontal,
Maximize2,
Minus,
@@ -1114,7 +1117,10 @@ export default function LiveCamera() {
const heroAction = ACTION_META[currentAction];
const rawActionMeta = ACTION_META[rawAction];
const pendingActionMeta = stabilityMeta.pendingAction ? ACTION_META[stabilityMeta.pendingAction] : null;
const resolvedAvatarLabel = AVATAR_PRESETS.find((preset) => preset.key === resolvedAvatarKey)?.label || "猩猩";
const resolvedAvatarPreset = getAvatarPreset(resolvedAvatarKey);
const resolvedAvatarLabel = resolvedAvatarPreset?.label || "猩猩";
const animalAvatarPresets = AVATAR_PRESETS.filter((preset) => preset.category === "animal");
const fullBodyAvatarPresets = AVATAR_PRESETS.filter((preset) => preset.category === "full-body-3d");
const previewTitle = analyzing
? stabilityMeta.pending && pendingActionMeta
? `${pendingActionMeta.label} 切换确认中`
@@ -1204,6 +1210,78 @@ export default function LiveCamera() {
</div>
);
const renderAvatarShowcaseCard = (preset: AvatarPreset) => {
const active = resolvedAvatarKey === preset.key;
return (
<div
key={preset.key}
className={`overflow-hidden rounded-[22px] border transition ${
active
? "border-primary/50 bg-primary/5 shadow-lg shadow-primary/10"
: "border-border/60 bg-background hover:border-primary/30 hover:bg-muted/40"
}`}
>
<button
type="button"
onClick={() => {
setAvatarKey(preset.key);
setAvatarEnabled(true);
}}
className="group block w-full text-left"
>
<div className="relative aspect-[4/5] overflow-hidden bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.9),_rgba(226,232,240,0.18)_38%,_rgba(15,23,42,0.92))]">
<img
src={`/avatars/opensource3d/${
preset.key === "beachKing"
? "beach-king"
: preset.key === "sportTv"
? "sport-tv"
: preset.key === "juanita3d"
? "juanita"
: "jenny"
}.webp`}
alt={preset.label}
className="h-full w-full object-contain p-3 transition duration-300 group-hover:scale-[1.03]"
loading="lazy"
/>
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-20 bg-gradient-to-t from-slate-950 via-slate-950/55 to-transparent" />
<div className="absolute left-3 top-3">
<Badge className="border-white/10 bg-black/60 text-white hover:bg-black/60">3D </Badge>
</div>
{active ? (
<div className="absolute right-3 top-3">
<Badge className="border-primary/20 bg-primary text-primary-foreground hover:bg-primary">使</Badge>
</div>
) : null}
</div>
<div className="space-y-3 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-semibold">{preset.label}</div>
<div className="mt-1 text-xs text-muted-foreground">{preset.collection} · {preset.license}</div>
</div>
</div>
<p className="text-xs leading-5 text-muted-foreground">{preset.description}</p>
</div>
</button>
<div className="flex items-center justify-between gap-3 border-t border-border/60 px-4 py-3">
<div className="text-[11px] uppercase tracking-[0.16em] text-muted-foreground">VRM </div>
{preset.modelUrl ? (
<a
href={preset.modelUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-xs font-medium text-primary"
>
<ExternalLink className="h-3.5 w-3.5" />
</a>
) : null}
</div>
</div>
);
};
return (
<div className="space-y-4 mobile-safe-bottom">
<Dialog open={showSetupGuide} onOpenChange={setShowSetupGuide}>
@@ -1320,7 +1398,7 @@ export default function LiveCamera() {
<div>
<h1 className="text-3xl font-semibold tracking-tight"></h1>
<p className="mt-2 max-w-2xl text-sm leading-6 text-white/70">
24
24 10 4 3D Avatar
</p>
</div>
</div>
@@ -1455,7 +1533,7 @@ export default function LiveCamera() {
<div>
<div className="text-sm font-medium"></div>
<div className="mt-1 text-xs text-muted-foreground">
10
使 10 4 3D Avatar
</div>
</div>
<Switch
@@ -1467,7 +1545,7 @@ export default function LiveCamera() {
</div>
<div className="text-xs text-muted-foreground">
{resolvedAvatarLabel}
{avatarPrompt.trim() ? ` · 输入 ${avatarPrompt.trim()}` : " · 可输入猩猩、狐狸、熊猫、兔子等别名自动映射"}
{avatarPrompt.trim() ? ` · 输入 ${avatarPrompt.trim()}` : " · 可输入猩猩、狐狸、熊猫、兔子,或 BeachKing、Juanita 等别名自动映射"}
</div>
</div>
<div>
@@ -1477,9 +1555,12 @@ export default function LiveCamera() {
<SelectValue />
</SelectTrigger>
<SelectContent>
{AVATAR_PRESETS.map((preset) => (
{animalAvatarPresets.map((preset) => (
<SelectItem key={preset.key} value={preset.key}>{preset.label}</SelectItem>
))}
{fullBodyAvatarPresets.map((preset) => (
<SelectItem key={preset.key} value={preset.key}>{preset.label} · 3D</SelectItem>
))}
</SelectContent>
</Select>
</div>
@@ -1488,11 +1569,25 @@ export default function LiveCamera() {
<Input
value={avatarPrompt}
onChange={(event) => setAvatarPrompt(event.target.value)}
placeholder="例如 狐狸 / panda coach / dog mascot"
placeholder="例如 狐狸 / panda coach / BeachKing / Juanita"
className="h-12 rounded-2xl border-border/60"
/>
</div>
</div>
<div className="mt-4 rounded-[24px] border border-border/60 bg-background/80 p-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-sm font-medium"> 3D </div>
<div className="mt-1 text-xs text-muted-foreground">
4 Open Source Avatars CC0 VRM/three-vrm沿
</div>
</div>
<Badge variant="secondary" className="rounded-full px-3 py-1 text-xs">CC0 · Open Source Avatars</Badge>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{fullBodyAvatarPresets.map(renderAvatarShowcaseCard)}
</div>
</div>
</div>
</CardContent>
</Card>