8.6 KiB
maic-renderer 设计稿(v1:只读画布)
配套计划:
docs/superpowers/plans/2026-05-28-maic-renderer-package.md原始 spec:docs/superpowers/specs/2026-05-28-maic-renderer-package-design.md
1. 目标
把 OpenMAIC 主仓 components/maic-renderer/ 中的只读画布部分抽成独立 workspace 包,让任意 React + Tailwind 4 项目都能"装包 → 传 Slide → 直接渲染"。
v1 范围限定为只读画布(对应主仓 Editor/ScreenCanvas.tsx 子树)。编辑能力(Editor/Canvas/*、Operate/*、ProseMirror live editor)留给 v2。
理由:
- 闭环最小可用,跟
pptxtojson-pro形成「产 Slide → 渲 Slide」对子。 - 编辑器接口面(选中态、命令、撤销、剪贴板、协作)定型后难改,第一版强行定下来大概率返工。
- 只读包本身有独立价值(嵌课件、报告、回放、PDF 导出预览等)。
2. 设计决策
| # | 决策点 | 选择 | 理由 |
|---|---|---|---|
| D1 | 范围 | 分两步:v1 只读 + v2 编辑 | 闭环最小,风险可控 |
| D2 | 数据 API | 纯 Props 主;同时导出可选 Provider | 上手最快,进阶用户也照顾到 |
| D3 | 样式 | 要求消费者使用 Tailwind 4 | 工作量最小,跟主仓工程实践一致 |
| D4 | 特效层 | 包含,4 个全做成可选 props,默认关 | 还原"OpenMAIC 同款画布"必备,但默认零负担 |
| D5 | 业务耦合元素 | 暴露 renderImage / renderVideo 插槽,包内默认渲原生标签 | 包不感知业务,消费者按需注入 |
| D6 | i18n | 包不带文案 | UI 文案下放给消费者 |
| D7 | ProseMirror | v1 不依赖 | 只读用 dangerouslySetInnerHTML 就够,体积大幅缩小 |
| D8 | 包发布 | workspace 包,未来可发 npm;当前名 maic-renderer(同 pptxtojson-pro 风格) |
一致性 |
3. 对外 API
3.1 主入口
import { SlideCanvas, type SlideCanvasProps } from 'maic-renderer';
interface SlideCanvasProps {
/** 单页幻灯片数据(PPTist 风格) */
slide: Slide;
/** 画布缩放,默认 1 */
scale?: number;
/** 可选:覆盖 slide.background */
background?: SlideBackground;
/** 可选:4 个播放特效,默认全关 */
effects?: {
laser?: { elementId: string; color?: string; duration?: number };
spotlight?: { elementId: string };
highlight?: { elementId: string };
zoom?: { elementId: string; scale: number };
};
/** 可选:图片渲染插槽,消费者可注入 placeholder/retry 逻辑 */
renderImage?: (el: PPTImageElement, resolvedSrc: string) => React.ReactNode;
/** 可选:视频渲染插槽 */
renderVideo?: (el: PPTVideoElement) => React.ReactNode;
/** 可选:元素点击 */
onElementClick?: (el: PPTElement, event: React.MouseEvent) => void;
/** 透传外层 */
className?: string;
style?: React.CSSProperties;
}
3.2 Provider 高阶模式(可选)
import { SlideRendererProvider, useSlideContext } from 'maic-renderer';
<SlideRendererProvider slide={slide} scale={0.9}>
<SlideCanvas />
<MyCustomOverlay /> {/* 子组件 useSlideContext() 拿数据 */}
</SlideRendererProvider>
3.3 元素子组件(细粒度复用)
import {
BaseTextElement, BaseShapeElement, BaseImageElement,
BaseLineElement, BaseChartElement, BaseLatexElement,
BaseTableElement, BaseVideoElement, BaseCodeElement,
} from 'maic-renderer/elements';
3.4 类型
import type {
Slide, PPTElement, SlideBackground, SlideTheme,
PPTTextElement, PPTShapeElement, PPTImageElement,
PPTLineElement, PPTChartElement, PPTLatexElement,
PPTTableElement, PPTVideoElement, PPTCodeElement,
ImageElementClip, ImageElementFilters,
Gradient, GradientType, PPTElementOutline, PPTElementShadow,
} from 'maic-renderer/types';
4. 包结构
packages/maic-renderer/
├── package.json
├── README.md
├── DESIGN.md
├── tsconfig.json
├── rollup.config.js
└── src/
├── index.ts # 主入口
├── SlideCanvas.tsx # 替代 ScreenCanvas(props 驱动)
├── SlideElement.tsx # 替代 ScreenElement
├── context.tsx # Provider + useSlideContext
├── effects/
│ ├── index.ts
│ ├── HighlightOverlay.tsx
│ ├── SpotlightOverlay.tsx
│ ├── LaserOverlay.tsx
│ └── ZoomWrapper.tsx
├── elements/
│ ├── index.ts
│ ├── text/BaseTextElement.tsx
│ ├── shape/
│ │ ├── BaseShapeElement.tsx
│ │ ├── GradientDefs.tsx
│ │ └── PatternDefs.tsx
│ ├── image/
│ │ ├── BaseImageElement.tsx
│ │ ├── ImageOutline.tsx
│ │ ├── useClipImage.ts
│ │ └── useFilter.ts
│ ├── line/BaseLineElement.tsx
│ ├── chart/
│ │ ├── BaseChartElement.tsx
│ │ └── Chart.tsx
│ ├── latex/BaseLatexElement.tsx
│ ├── table/BaseTableElement.tsx
│ ├── video/BaseVideoElement.tsx
│ ├── code/BaseCodeElement.tsx
│ └── shared/
│ ├── ElementOutline.tsx
│ ├── useElementFill.ts
│ ├── useElementOutline.ts
│ ├── useElementShadow.ts
│ └── useElementFlip.ts
├── hooks/
│ ├── useSlideBackgroundStyle.ts
│ └── useViewportSize.ts
├── utils/
│ ├── cn.ts
│ ├── geometry.ts
│ └── element.ts
└── types/
├── slides.ts # 从 lib/types/slides 拷贝
├── effects.ts # 新增
└── index.ts
5. 依赖
peerDependencies:
{
"react": ">=18",
"react-dom": ">=18",
"motion": ">=11",
"tailwindcss": ">=4"
}
dependencies(运行时必备):
katex— latex 渲染tinycolor2— 颜色工具lucide-react— 图标clsx、tailwind-merge—cnhelper
chart 库:复用主仓现用方案,T10 实现阶段确认。开发阶段先 inline 进 dependencies,若体积过大改 peerDep。
ProseMirror 系列:v1 完全不依赖。
6. 构建
跟 pptxtojson-pro 完全一致:
pnpm build # rollup -c && tsc --emitDeclarationOnly
产物:
dist/
├── index.{js,cjs,d.ts}
├── elements/index.{js,cjs,d.ts}
└── types/index.{js,cjs,d.ts}
7. 关键解耦点
7.1 ScreenCanvas → SlideCanvas
| 原 | 新 |
|---|---|
useCanvasStore.use.canvasScale() |
props.scale ?? 1 |
useSceneSelector(c => c.canvas.elements) |
props.slide.elements |
useSceneSelector(c => c.canvas.background) |
props.background ?? props.slide.background |
useCanvasStore.use.laserElementId/Options() |
props.effects?.laser |
useCanvasStore.use.zoomTarget() |
props.effects?.zoom |
findElementGeometry(...) |
拷贝进 utils/geometry.ts |
7.2 ScreenElement → SlideElement
theme.fontColor / theme.fontName 改成从 props.slide.theme 读取;useSceneSelector 移除。
7.3 BaseImageElement
包内退化成纯 <img> 版本(含 clip/filter/outline/shadow),所有 placeholder / retry / i18n 逻辑剥离。消费者通过 renderImage 注入业务版本。
7.4 BaseVideoElement
同上,纯 <video>。
7.5 其余 Base*Element
只有类型导入路径要改(@/lib/types/slides → ../../types/slides),无业务耦合。
8. 验收标准
pnpm --filter maic-renderer build通过,产出干净 dist- 主仓
/maic-renderer-demo路由用包渲染一份手写 Slide,目视与原ScreenCanvas一致 - v1 不替换主仓既有调用
9. v2 预告
Editor/Canvas/*抽到包内editing/- ProseMirror live editor
- 选中/拖拽/resize/旋转/裁剪/对齐线/标尺
- 撤销/重做/剪贴板
- API:
<SlideEditor slide editable onChange={...} />
10. 风险
| 风险 | 等级 | 缓解 |
|---|---|---|
| Chart 库依赖不明,包体积可能膨胀 | 中 | T10 阶段确认;若 >200KB gz 改 peerDep |
| Tailwind 4 强依赖筛掉很多消费者 | 中 | README 显著说明;v3 再考虑编译 CSS |
主仓 Slide 类型未来演进,包内拷贝版会漂移 |
低 | 后续可通过 pptxtojson-pro 已有的 openmaic/types/slides.ts 三方共享 |
| BaseImageElement 砍业务后主仓现有页面会丢功能 | 无影响 | 本次任务不替换主仓调用,主仓继续用自己的 BaseImageElement |