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 `} + {/* 屏幕光的柔性衰减:硬边椭圆看着像色斑,径向渐变才像光 */} + + {[RUN, OK, ERR].map((c) => ( + + + + + + ))} + {/* 白板:协调者的思考/综合过程 */} @@ -524,6 +572,7 @@ export function OfficeView({ events, output, phase }: { events: ExecEvent[]; out happy={lead.status === "done"} sad={lead.status === "error"} delay={0} + sprite={leadSprite} />