diff --git a/sundynix-desktop/frontend/src/assets/office/a0.webp b/sundynix-desktop/frontend/src/assets/office/a0.webp
new file mode 100644
index 0000000..2a77297
Binary files /dev/null and b/sundynix-desktop/frontend/src/assets/office/a0.webp differ
diff --git a/sundynix-desktop/frontend/src/assets/office/a1.webp b/sundynix-desktop/frontend/src/assets/office/a1.webp
new file mode 100644
index 0000000..0d1d6d7
Binary files /dev/null and b/sundynix-desktop/frontend/src/assets/office/a1.webp differ
diff --git a/sundynix-desktop/frontend/src/assets/office/a2.webp b/sundynix-desktop/frontend/src/assets/office/a2.webp
new file mode 100644
index 0000000..36eaa84
Binary files /dev/null and b/sundynix-desktop/frontend/src/assets/office/a2.webp differ
diff --git a/sundynix-desktop/frontend/src/assets/office/a3.webp b/sundynix-desktop/frontend/src/assets/office/a3.webp
new file mode 100644
index 0000000..dde8f04
Binary files /dev/null and b/sundynix-desktop/frontend/src/assets/office/a3.webp differ
diff --git a/sundynix-desktop/frontend/src/assets/office/a4.webp b/sundynix-desktop/frontend/src/assets/office/a4.webp
new file mode 100644
index 0000000..2f6db9e
Binary files /dev/null and b/sundynix-desktop/frontend/src/assets/office/a4.webp differ
diff --git a/sundynix-desktop/frontend/src/assets/office/desk.webp b/sundynix-desktop/frontend/src/assets/office/desk.webp
new file mode 100644
index 0000000..ca558b8
Binary files /dev/null and b/sundynix-desktop/frontend/src/assets/office/desk.webp differ
diff --git a/sundynix-desktop/frontend/src/assets/office/lead.webp b/sundynix-desktop/frontend/src/assets/office/lead.webp
new file mode 100644
index 0000000..7f3fe8e
Binary files /dev/null and b/sundynix-desktop/frontend/src/assets/office/lead.webp differ
diff --git a/sundynix-desktop/frontend/src/components/OfficeView.tsx b/sundynix-desktop/frontend/src/components/OfficeView.tsx
index cd7ab8f..5ddc268 100644
--- a/sundynix-desktop/frontend/src/components/OfficeView.tsx
+++ b/sundynix-desktop/frontend/src/components/OfficeView.tsx
@@ -1,6 +1,16 @@
import { useMemo, type CSSProperties } from "react";
import { deriveTeam, teamNow, type RunPhase, type TeamSeat } from "../lib/run";
import type { ExecEvent } from "../lib/api";
+import leadSprite from "../assets/office/lead.webp";
+import deskSprite from "../assets/office/desk.webp";
+import a0 from "../assets/office/a0.webp";
+import a1 from "../assets/office/a1.webp";
+import a2 from "../assets/office/a2.webp";
+import a3 from "../assets/office/a3.webp";
+import a4 from "../assets/office/a4.webp";
+
+// 专家立绘:按工位序号循环取,同一次运行里每个专家稳定对应同一个人。
+const ACTORS = [a0, a1, a2, a3, a4];
// 2D 卡通办公室:把多智能体协调演成一出办公室短剧。
// 专家被派发 → 从门口走进来 → 在工位敲键盘 → 干完起身走到协调者桌前递简报 → 回工位。
@@ -13,7 +23,7 @@ import type { ExecEvent } from "../lib/api";
// 动效=状态编码,不是装饰;并守 prefers-reduced-motion。
const VB_W = 720;
-const VB_H = 460;
+const VB_H = 510;
const FLOOR_Y = 200;
const OUTLINE = "#2f2a26";
@@ -103,6 +113,27 @@ function Hair({ color, style }: { color: string; style: number }) {
);
}
+// 立绘贴图:AI 生成 + 抠绿幕的角色。给了 sprite 就用贴图,没给则回落到手绘。
+// 贴图是扁的:手臂动不了、表情换不了 —— 状态改由显示器辉光 / 头顶气泡 / 铭牌圆点表达(本来就有)。
+// 局部坐标与手绘版对齐:脚在原点、身高 150、宽约 72。
+const SPRITE_H = 168;
+const SPRITE_W = 81;
+
+function ChibiSprite({ sprite, delay }: { sprite: string; delay: number }) {
+ return (
+
+
+
+ );
+}
+
// typing: undefined=不敲;{count,delay}=敲 count 次(count=Infinity 即一直敲)
function Chibi({
shirt,
@@ -112,6 +143,7 @@ function Chibi({
happy,
sad,
delay,
+ sprite,
}: {
shirt: string;
hair: string;
@@ -120,7 +152,9 @@ function Chibi({
happy?: boolean;
sad?: boolean;
delay: number;
+ sprite?: string;
}) {
+ if (sprite) return ;
const armStyle: Vars | undefined = typing
? { animationName: "sdx-type", animationDuration: `${TYPE_MS}ms`, animationTimingFunction: "ease-in-out", animationIterationCount: typing.count === Infinity ? "infinite" : typing.count, animationDelay: `${typing.delay}s` }
: undefined;
@@ -212,32 +246,35 @@ function Paper({ dur, delay }: { dur: number; delay: number }) {
}
// 工位(桌子层):白桌 + 显示器辉光(状态色)+ 桌面铭牌。桌子挡住下半身,人只露上半身。
+// 桌子贴图的解剖(量出来的,单位=贴图内百分比):
+// 显示器 x 56%~94% / y 0~37%;咖啡杯 x 13%~26%;桌面 y 42%~56%;挡板 y 58%~94%。
+// 锚点按「桌面」而不是「底边」:底边贴地的话桌宽 200 时显示器会顶到 y=-136 糊住人脸。
+// 按桌面对齐后桌子下半截自然超出脚线——这是对的,桌子比人更靠近镜头,本就该画得更低。
+const DESK_RATIO = 376 / 260;
+const DESK_SURFACE_FRAC = 0.44; // 桌面在贴图里的高度位置
+const DESK_MON_CX = 0.75; // 显示器中心的横向位置(辉光叠在这儿)
+const DESK_MON_CY = 0.2;
+
function Deskette({ name, sub, tone, glow, w = 200 }: { name: string; sub: string; tone: string; glow?: boolean; w?: number }) {
const hw = w / 2;
+ const dh = w / DESK_RATIO;
+ const top = -46 - DESK_SURFACE_FRAC * dh; // 让桌面落在 y=-46(与原手绘桌一致)
+ const monX = -hw + DESK_MON_CX * w;
+ const monY = top + DESK_MON_CY * dh;
return (
- {/* 显示器(背对我们,屏幕辉光溢出到桌面) */}
-
-
-
-
-
-
- {/* 桌面 + 桌沿 */}
-
-
- {/* 咖啡杯 */}
-
-
-
-
- {/* 铭牌 */}
-
-
-
- {fitText(name, (w - 62) / 17)}
+ {/* 屏幕光溢到桌面:画在贴图「之下」,是光晕不是色斑。
+ 贴图里的显示器背壳朝着我们,把状态色直接糊在背壳上物理讲不通、也像贴纸。 */}
+
+
+ {/* 铭牌 + 副标题:都放在挡板内(实测挡板 = local -27~+23),
+ 掉到挡板外会骑在桌子下边线上、被切成半截。 */}
+
+
+
+ {fitText(name, (w - 62) / 16)}
-
+
{sub}
@@ -299,6 +336,7 @@ function Actor({ cue, reduced, hasLead }: { cue: Cue; reduced: boolean; hasLead:
happy={finished}
sad={seat.status === "error"}
delay={index * 0.23}
+ sprite={ACTORS[index % ACTORS.length]}
/>
{handoff && !reduced && }
{cue.bubble && }
@@ -388,7 +426,7 @@ export function OfficeView({ events, output, phase }: { events: ExecEvent[]; out
// 排位:≤5 人一排(前排),否则拆两排,后排小、前排大 —— 用缩放造纵深。
// 两排时后排不出头顶气泡:气泡很高,会顶到协调者的桌子;状态仍由显示器辉光和铭牌圆点表达。
const rows: TeamSeat[][] = seats.length <= 5 ? [seats] : [seats.slice(0, Math.ceil(seats.length / 2)), seats.slice(Math.ceil(seats.length / 2))];
- const rowCfg = rows.length === 1 ? [{ y: 430, s: 1 }] : [{ y: 322, s: 0.6 }, { y: 438, s: 0.82 }];
+ const rowCfg = rows.length === 1 ? [{ y: 452, s: 1 }] : [{ y: 330, s: 0.58 }, { y: 460, s: 0.8 }];
// 档期:直播时事件到哪演到哪(延迟一律 0);复盘时按时间戳排,整段压进 PLAY_MS。
const rate = streaming ? 0 : Math.min(1, PLAY_MS / span);
@@ -472,6 +510,16 @@ export function OfficeView({ events, output, phase }: { events: ExecEvent[]; out
`}