1. 从“artcraft”这个名字说起:它到底想解决什么问题
第一次看到“artcraft”这个标题,我脑子里蹦出来的第一反应是“艺术”加“手艺”,一个听起来既像创作工具又像手工平台的词。后来跟几个做独立开发的朋友聊,发现大家对这个词的理解出奇地一致:它指向的是一类把创意表达和工程实现缝合在一起的项目——不是纯粹的艺术创作软件,也不是纯粹的代码工具库,而是那种让“想做点好看东西的人”能快速上手的中间层。
这类项目通常出现在几个典型场景里:独立开发者想给自己的小工具做一个有质感的界面,设计师想把自己的视觉稿变成可交互的原型,或者内容创作者想批量生成风格统一的配图。它的核心价值不在于功能有多强大,而在于降低从想法到成品的摩擦。你不需要先学三个月图形学,也不需要啃完某个框架的全部文档,就能做出一个看起来不像是“程序员审美”的东西。
我之所以对这个方向感兴趣,是因为过去几年我见过太多项目死在“最后一公里”上——后端逻辑写得漂漂亮亮,前端界面一塌糊涂,用户点进来三秒钟就关掉了。artcraft 这类项目瞄准的正是这个痛点:它不要求你成为全栈专家,但能让你在有限的时间里做出一个拿得出手的成品。
这篇文章我会从设计思路、核心细节、实操过程、常见问题四个维度,把这类项目的完整面貌拆开来讲。无论你是刚入门的开发者,还是想扩展技能边界的设计师,都能从中找到可以直接抄作业的部分。
2. 内容整体设计与思路拆解
2.1 为什么是“art”加“craft”而不是别的组合
拆开来看,“art”代表的是审美层面的追求——配色、排版、动效、视觉层次;“craft”代表的是工程层面的落地——代码结构、性能优化、可维护性。这两个词放在一起,其实暗示了一个很重要的设计哲学:审美和工程不是对立的,而是可以互相成就的。
我在实际项目中试过两种极端路线。一种是“先功能后美化”,先把所有逻辑跑通,最后再套一层皮。结果往往是功能跑通了,但改样式的时候发现代码结构根本不支持灵活调整,改一个颜色要动五个文件。另一种是“先设计后实现”,用设计工具把每个状态都画出来,再照着实现。结果往往是设计稿很漂亮,但实现时发现某些效果在目标平台上根本跑不起来,或者性能开销大到不可接受。
artcraft 这类项目通常走的是第三条路:把视觉决策和工程决策放在同一个抽象层里考虑。具体来说,它会提供一套预设的“风格系统”——比如一组配色方案、一套间距规则、几种字体组合——你在写代码的时候直接调用这些预设,而不是每次从零开始调参数。这样做的好处是,你的代码天然就带着一致的视觉语言,不需要后期再去“统一风格”。
2.2 核心架构的选型逻辑
从技术选型上看,这类项目通常会面临几个关键决策点。第一个是渲染方式:是用 Canvas 还是 SVG,是用 DOM 还是 WebGL。这个选择直接决定了项目的适用范围和性能天花板。
我个人的经验是,如果你的目标场景是“生成静态图片”或“做简单的交互动效”,Canvas 是更稳妥的选择。它的 API 相对简单,性能可控,而且导出图片很方便。但如果你需要做复杂的矢量图形编辑,或者需要每个元素都能独立响应事件,SVG 会更合适。WebGL 则是另一个量级的东西,适合做粒子效果、3D 变换这类高视觉冲击力的场景,但学习曲线陡峭,调试成本高。
第二个决策点是状态管理。artcraft 类项目通常会有大量的“当前选中元素”“当前应用样式”“历史操作记录”这类状态。如果状态管理没做好,很容易出现“改了 A 元素结果 B 元素也跟着变了”的诡异 bug。我见过比较优雅的方案是用不可变数据结构来管理画布状态,每次操作生成一个新的状态快照,这样撤销重做和状态对比都很方便。
第三个决策点是导出格式。是导出 PNG、SVG 还是 JSON 配置?这个选择会影响整个项目的架构。如果导出 PNG,那渲染逻辑可以比较自由;如果导出 SVG,那所有图形元素都必须是矢量描述;如果导出 JSON 配置,那意味着你的项目本质上是一个“配置生成器”,渲染可以交给别的工具去做。
2.3 目标用户画像与场景匹配
这类项目的目标用户通常有三类。第一类是独立开发者,他们需要快速给自己的产品做一个像样的界面,但不想在视觉上花太多时间。第二类是内容创作者,他们需要批量生成风格统一的封面图、插图或社交媒体配图。第三类是教育场景下的学习者,他们想通过一个具体的项目来理解“创意如何变成代码”这个过程。
针对不同用户,项目的设计侧重点应该有所不同。对独立开发者来说,API 的简洁性最重要——最好三行代码就能出一个效果。对内容创作者来说,模板的丰富度和批量处理能力最关键。对学习者来说,代码的可读性和注释的详细程度才是核心。
我在实际使用中发现,很多项目失败的原因不是功能不够强,而是没有明确自己主要服务哪类用户。试图讨好所有人的结果往往是所有人都觉得不好用。artcraft 这类项目如果让我来设计,我会优先服务第一类用户,因为他们的需求最明确,反馈最直接,而且他们通常愿意为“省时间”这件事付费或贡献代码。
3. 核心细节解析与实操要点
3.1 风格系统的抽象层次
风格系统是这类项目的灵魂。一个好的风格系统应该包含哪些层次?我总结下来至少有四层:色彩层、排版层、形状层、动效层。
色彩层不只是定义几个颜色值,而是要定义颜色之间的关系。比如主色、辅色、强调色、背景色、文字色之间的对比度和和谐度。我见过比较聪明的做法是用 HSL 色彩空间来定义颜色,因为这样调整明度和饱和度比 RGB 直观得多。你可以定义一个基准色,然后通过调整 L 值来生成一系列深浅变体,保证整个配色方案的一致性。
排版层要处理的是字号阶梯、行高规则、字重搭配。一个常见的错误是直接使用浏览器默认的 h1 到 h6 字号,结果在不同屏幕上显示效果差异巨大。更好的做法是定义一个模块化比例,比如 1.25 或 1.333,然后基于基准字号计算出整个阶梯。这样无论基准字号怎么变,整个排版系统的比例关系都是稳定的。
形状层包括圆角半径、边框宽度、阴影参数这些。这些参数看起来琐碎,但它们是决定“视觉质感”的关键。圆角从 4px 改成 8px,整个界面的气质就会从“锐利”变成“柔和”。阴影的模糊半径和偏移量稍微调一下,元素的“悬浮感”就会完全不同。
动效层是最容易被忽视的一层。很多人觉得动效就是加个 transition,但实际上动效的缓动曲线、持续时间、触发时机都需要精心设计。一个 200ms 的 ease-out 和一个 300ms 的 ease-in-out,给人的感觉是完全不同的。
3.2 渲染管线的关键节点
从代码到最终画面,中间要经过好几个关键节点。理解这些节点,才能知道在什么地方做优化、在什么地方做取舍。
第一个节点是场景图构建。你把所有要绘制的元素组织成一个树状结构,每个节点包含位置、尺寸、样式、子节点等信息。这个结构的设计直接影响后续的遍历效率和更新成本。我建议用扁平化加分组的方式,而不是纯粹的深层嵌套,因为深层嵌套在更新时容易触发不必要的重绘。
第二个节点是布局计算。如果元素的位置是相对的(比如“在父元素内居中”),就需要先计算出绝对坐标才能绘制。这个计算过程如果太复杂,会成为性能瓶颈。一个实用的技巧是缓存布局结果,只有当影响布局的属性发生变化时才重新计算。
第三个节点是绘制指令生成。把场景图转换成具体的绘制调用,比如“在 (x, y) 位置画一个半径为 r 的圆,填充色为 c”。这个阶段要注意绘制顺序,因为后绘制的元素会覆盖先绘制的元素。层级管理没做好,就会出现“该在上面的被压在下面”的问题。
第四个节点是实际渲染。如果是 Canvas,就是调用 ctx 的各种方法;如果是 SVG,就是生成对应的 DOM 节点;如果是 WebGL,就是提交顶点数据和着色器程序。这个阶段的性能优化空间最大,但也最依赖具体平台的特性。
3.3 参数计算的实际案例
举个具体的例子来说明参数计算的过程。假设我们要实现一个“卡片阴影”效果,需要确定阴影的偏移量、模糊半径和颜色透明度。
首先确定光源方向。通常假设光源在左上方,所以阴影向右下方偏移。偏移量取卡片宽度的 2% 到 4% 比较自然。如果卡片宽度是 300px,偏移量就是 6px 到 12px。
然后确定模糊半径。模糊半径和偏移量有一个经验比例:模糊半径大约是偏移量的 2 到 3 倍。所以偏移 8px 的话,模糊半径取 16px 到 24px。
最后确定颜色透明度。阴影颜色通常是黑色或深灰色,透明度在 0.1 到 0.25 之间。透明度太低看不出效果,太高会显得脏。如果背景是深色,阴影颜色应该改成白色或浅色,透明度也要相应调整。
把这些参数写成代码大概是这样的:
const shadow = { offsetX: cardWidth * 0.03, offsetY: cardWidth * 0.03, blur: cardWidth * 0.08, color: 'rgba(0, 0, 0, 0.15)' };这个计算过程看起来简单,但如果你直接写死数值,换一个卡片尺寸就会显得不协调。用相对比例来计算,才能保证不同尺寸下的视觉一致性。
注意:阴影参数没有绝对的标准值,需要根据具体的设计风格微调。扁平化风格通常用更小的偏移和更大的模糊,拟物化风格则相反。
3.4 样式与逻辑的分离策略
一个常见的架构问题是:样式逻辑和业务逻辑混在一起,导致改样式的时候要翻遍整个代码库。解决这个问题的核心思路是把样式定义抽离成独立的配置对象,业务代码只负责引用配置,不直接写具体的样式值。
比如不要这样写:
ctx.fillStyle = '#3498db'; ctx.fillRect(10, 10, 100, 50);而是这样写:
const theme = { primary: '#3498db', spacing: { sm: 8, md: 16, lg: 24 } }; ctx.fillStyle = theme.primary; ctx.fillRect(theme.spacing.sm, theme.spacing.sm, 100, 50);这样做的好处是,当你想换一套配色时,只需要改 theme 对象,所有引用它的地方都会自动更新。而且 theme 对象可以导出成 JSON,方便在不同项目之间共享。
更进一步的做法是支持多套主题切换。定义多个 theme 对象,运行时根据用户选择或系统设置动态切换。这在需要支持深色模式的场景下特别有用。
4. 实操过程与核心环节实现
4.1 环境搭建与依赖选择
假设我们要从零开始搭建一个 artcraft 类的项目,第一步是确定技术栈。我的建议是:Vite + TypeScript + Canvas API。Vite 提供极快的开发服务器和构建速度,TypeScript 提供类型安全(这在处理复杂的样式配置时特别有价值),Canvas API 提供足够的渲染能力且没有额外的学习成本。
初始化项目的命令:
npm create vite@latest artcraft-demo -- --template vanilla-ts cd artcraft-demo npm install npm run dev如果你需要更高级的图形能力,可以考虑加入roughjs来做手绘风格,或者d3-scale来做数据驱动的尺寸映射。但我的建议是先不要引入太多依赖,把核心的渲染和状态管理跑通之后,再根据实际需求逐步添加。
目录结构建议这样组织:
src/ core/ renderer.ts // 渲染引擎 scene.ts // 场景图管理 style.ts // 样式系统 elements/ rect.ts // 矩形元素 circle.ts // 圆形元素 text.ts // 文本元素 themes/ default.ts // 默认主题 dark.ts // 深色主题 utils/ color.ts // 颜色工具 layout.ts // 布局计算 main.ts // 入口文件这个结构的核心思想是按职责分层,而不是按功能模块分。core 层不依赖 elements 层,elements 层不依赖 themes 层,依赖关系是单向的。这样当你需要替换某个层的时候,不会牵连到其他层。
4.2 渲染引擎的最小实现
渲染引擎的核心是一个循环:清空画布 → 遍历场景图 → 绘制每个元素。听起来简单,但要做好需要处理不少细节。
class Renderer { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private scene: Scene; constructor(canvas: HTMLCanvasElement, scene: Scene) { this.canvas = canvas; this.ctx = canvas.getContext('2d')!; this.scene = scene; this.resize(); } private resize() { const dpr = window.devicePixelRatio || 1; const rect = this.canvas.getBoundingClientRect(); this.canvas.width = rect.width * dpr; this.canvas.height = rect.height * dpr; this.ctx.scale(dpr, dpr); } render() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.scene.getVisibleElements().forEach(element => { element.draw(this.ctx); }); } }这里有几个关键点值得展开。第一是devicePixelRatio 的处理。如果不处理,在高分屏上绘制出来的图形会模糊。处理方式是把 canvas 的实际像素尺寸设成 CSS 尺寸乘以 dpr,然后用ctx.scale(dpr, dpr)把坐标系缩放到 CSS 尺寸。这样你在写绘制代码时用的就是逻辑像素,不用关心实际像素。
第二是清空画布的方式。clearRect是最常用的,但如果你的背景不是透明的,用fillRect填充背景色会更高效。另外,如果场景内容变化不大,可以考虑局部重绘而不是全量重绘,但这需要更复杂的状态追踪机制。
第三是绘制顺序。getVisibleElements()返回的数组顺序就是绘制顺序,后面的覆盖前面的。所以场景图在构建时就要确定好层级关系,通常用 z-index 或类似机制来排序。
4.3 样式系统的落地实现
样式系统的核心是一个主题对象加上一套样式解析函数。主题对象定义所有可配置的视觉参数,解析函数负责把抽象参数转换成具体的绘制指令。
interface Theme { colors: { primary: string; secondary: string; background: string; text: string; }; spacing: { xs: number; sm: number; md: number; lg: number; xl: number; }; typography: { baseSize: number; scaleRatio: number; lineHeight: number; }; shapes: { borderRadius: number; borderWidth: number; shadowBlur: number; }; } const defaultTheme: Theme = { colors: { primary: '#2c3e50', secondary: '#e74c3c', background: '#ecf0f1', text: '#34495e' }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 }, typography: { baseSize: 16, scaleRatio: 1.25, lineHeight: 1.6 }, shapes: { borderRadius: 8, borderWidth: 1, shadowBlur: 12 } };基于这个主题,可以写一个辅助函数来计算字号阶梯:
function getFontSize(level: number, theme: Theme): number { return theme.typography.baseSize * Math.pow(theme.typography.scaleRatio, level); }level 为 0 时是基准字号,1 是放大一级,-1 是缩小一级。这样无论基准字号怎么变,整个阶梯的比例关系都是稳定的。
4.4 一个完整的绘制案例
假设我们要绘制一个“信息卡片”,包含标题、正文和底部标签。完整的实现流程如下。
第一步,定义卡片的数据结构:
interface CardData { title: string; body: string; tags: string[]; width: number; }第二步,计算卡片的布局。标题占一行,正文占若干行,标签在底部横向排列。高度需要根据内容动态计算:
function calculateCardHeight(data: CardData, theme: Theme): number { const padding = theme.spacing.lg; const titleHeight = getFontSize(2, theme) * theme.typography.lineHeight; const bodyLines = Math.ceil(data.body.length / 30); const bodyHeight = bodyLines * getFontSize(0, theme) * theme.typography.lineHeight; const tagHeight = getFontSize(-1, theme) * 2; return padding * 2 + titleHeight + theme.spacing.md + bodyHeight + theme.spacing.md + tagHeight; }第三步,绘制卡片背景和阴影:
function drawCardBackground(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, theme: Theme) { ctx.save(); ctx.shadowColor = 'rgba(0, 0, 0, 0.1)'; ctx.shadowBlur = theme.shapes.shadowBlur; ctx.shadowOffsetY = theme.shapes.shadowBlur / 3; ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.roundRect(x, y, w, h, theme.shapes.borderRadius); ctx.fill(); ctx.restore(); }第四步,绘制文字内容。这里要注意文字换行的处理,Canvas 本身不提供自动换行,需要手动计算每行的字符数并逐行绘制。
function drawWrappedText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number) { const chars = text.split(''); let line = ''; let currentY = y; for (const char of chars) { const testLine = line + char; if (ctx.measureText(testLine).width > maxWidth && line !== '') { ctx.fillText(line, x, currentY); line = char; currentY += lineHeight; } else { line = testLine; } } ctx.fillText(line, x, currentY); }这个换行算法是按字符逐个测量的,对中文没问题,但对英文来说会把单词切断。更完善的做法是按单词分割,但那样代码会复杂不少。实际项目中可以根据内容语言来选择策略。
实操心得:
ctx.roundRect在较新的浏览器中才支持,如果需要兼容旧环境,可以用arcTo手动绘制圆角矩形。我一般会写一个 polyfill 函数,根据环境自动选择实现。
4.5 导出与持久化
绘制完成后,通常需要把结果导出成图片或配置文件。导出 PNG 最简单:
function exportPNG(canvas: HTMLCanvasElement, filename: string) { const link = document.createElement('a'); link.download = filename; link.href = canvas.toDataURL('image/png'); link.click(); }如果需要更高质量的输出,可以用toBlob配合canvas.toDataURL('image/png', 1.0)来指定质量参数。对于需要打印的场景,可以把 dpr 临时调高再重绘一次,导出更高分辨率的图片。
导出 JSON 配置则需要把场景图序列化:
function serializeScene(scene: Scene): string { return JSON.stringify({ elements: scene.getElements().map(el => el.toJSON()), theme: scene.getTheme() }, null, 2); }反序列化就是反过来,根据 JSON 重建场景图。这个能力对于做“模板保存与加载”功能特别有用。
5. 常见问题与排查技巧实录
5.1 渲染模糊与锯齿问题
这是 Canvas 项目最常见的问题。表现是图形边缘模糊,或者文字看起来发虚。根本原因通常是canvas 的实际像素尺寸和 CSS 尺寸不匹配。
排查步骤:首先检查 canvas 的 width/height 属性是否设置了,然后检查 CSS 的 width/height 是否和属性值一致(考虑 dpr)。如果 dpr 是 2,canvas 的 width 应该是 CSS 宽度的 2 倍。
另一个可能的原因是绘制坐标不是整数。Canvas 在绘制非整数坐标的图形时会做抗锯齿处理,导致边缘模糊。解决办法是在绘制前对坐标取整:
ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));但要注意,取整会导致元素位置有 1px 以内的偏差,对于需要精确对齐的场景可能不合适。这时候可以考虑用ctx.translate(0.5, 0.5)来做半像素偏移,让线条落在像素中心。
5.2 性能瓶颈的定位与优化
当场景中元素数量增多时,渲染性能会明显下降。定位瓶颈的方法是分段计时:
const t0 = performance.now(); // 场景图遍历 const t1 = performance.now(); // 布局计算 const t2 = performance.now(); // 实际绘制 const t3 = performance.now(); console.log(`遍历: ${t1-t0}ms, 布局: ${t2-t1}ms, 绘制: ${t3-t2}ms`);如果瓶颈在遍历阶段,说明场景图结构太深或元素太多,可以考虑空间索引(比如四叉树)来快速筛选可见元素。如果瓶颈在布局阶段,说明布局计算太复杂,可以考虑缓存布局结果。如果瓶颈在绘制阶段,说明绘制调用太多,可以考虑合并绘制或使用离屏 Canvas 缓存静态内容。
我实测下来最有效的优化是离屏缓存:把不经常变化的背景元素绘制到一个离屏 Canvas 上,每帧只需要绘制变化的部分。这个技巧在制作“背景固定、前景变化”的场景时效果特别明显。
5.3 事件交互的坐标转换
Canvas 本身不提供元素级别的事件系统,所有点击事件都是绑定在 canvas 元素上的。要把鼠标坐标转换成场景中的元素坐标,需要做坐标变换。
function getMousePos(canvas: HTMLCanvasElement, event: MouseEvent) { const rect = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; return { x: (event.clientX - rect.left) * dpr, y: (event.clientY - rect.top) * dpr }; }拿到坐标后,需要遍历场景图,判断哪个元素包含了这个点。判断逻辑取决于元素的形状:矩形用边界检测,圆形用距离检测,不规则形状可能需要用路径检测。
注意:如果场景图有变换(比如缩放或旋转),坐标转换会更复杂,需要应用逆变换矩阵。我建议在项目初期就引入一个简单的变换系统,不然后期加变换功能会很痛苦。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 图形模糊 | dpr 未处理 | 检查 canvas 实际尺寸 | 按 dpr 缩放 canvas |
| 边缘锯齿 | 坐标非整数 | 打印绘制坐标 | 坐标取整或半像素偏移 |
| 性能下降 | 元素过多 | 分段计时 | 离屏缓存或空间索引 |
| 点击无响应 | 坐标转换错误 | 打印转换后坐标 | 检查 dpr 和 rect 偏移 |
| 颜色不一致 | 色彩空间问题 | 对比设计稿和输出 | 统一使用 sRGB |
| 文字换行异常 | 测量方式不当 | 检查 measureText 结果 | 按字符或单词分割 |
| 导出图片空白 | 绘制未完成 | 检查导出时机 | 在 render 回调后导出 |
| 主题切换无效 | 缓存未清除 | 检查主题引用 | 切换后强制重绘 |
5.5 几个容易踩的坑
第一个坑是在循环中创建对象。比如每帧都新建一个Path2D或Gradient对象,会导致频繁的垃圾回收,影响帧率。解决办法是把这些对象缓存起来,只在参数变化时重建。
第二个坑是忘记 restore。ctx.save()和ctx.restore()必须成对出现,否则状态会泄漏到后续的绘制中。我建议在绘制每个元素时都包一层 save/restore,虽然有一点性能开销,但能避免很多诡异的问题。
第三个坑是异步加载图片后忘记重绘。如果场景中包含图片元素,图片加载是异步的,加载完成后需要触发一次重绘,否则图片不会显示。解决办法是在图片的 onload 回调中调用 render 方法。
第四个坑是字体未加载完成就绘制文字。Web 字体是异步加载的,如果字体还没加载完就绘制,会使用默认字体,导致布局和预期不符。解决办法是用document.fonts.ready等待字体加载完成后再首次渲染。
6. 扩展方向与个人实践体会
6.1 从静态绘制到交互编辑
基础版本跑通之后,下一步自然是加入交互编辑能力。这需要引入命中检测、拖拽处理、属性面板三个模块。命中检测负责判断鼠标点中了哪个元素,拖拽处理负责更新元素位置,属性面板负责展示和修改选中元素的样式。
实现命中检测时,我建议用包围盒加精确检测的两级策略。先用包围盒快速筛选,如果包围盒都不包含点击点,就不用做精确检测了。这样在元素数量多的时候能显著提升响应速度。
拖拽处理的关键是记录拖拽起始状态,这样在拖拽过程中可以基于起始状态计算新位置,而不是基于上一帧的位置累加。后者容易产生累积误差,而且撤销操作会很麻烦。
6.2 模板系统与批量生成
如果目标用户是内容创作者,模板系统是必不可少的。模板本质上是一组预设的场景图配置,用户选择一个模板后,只需要替换其中的文字和图片内容,就能生成新的作品。
批量生成则是在模板系统之上加一层数据驱动:准备一个数据数组,遍历数组,每次用当前数据填充模板并导出图片。这个能力在做社交媒体配图、电商主图这类场景时特别有用。
我在实际项目中实现批量生成时,遇到的最大问题是导出速度。每张图都要重新渲染和编码,如果数量多的话会卡住主线程。解决办法是用requestIdleCallback或 Web Worker 来分片处理,避免阻塞用户界面。
6.3 个人体会
折腾这类项目最大的收获,是让我重新理解了“设计”和“工程”的关系。以前我总觉得这是两个独立的领域,设计师出稿,工程师实现,中间靠沟通来衔接。但做 artcraft 类项目的过程让我意识到,好的工程实现本身就是在做设计决策——你选择什么样的抽象层次,定义什么样的参数体系,提供什么样的默认值,这些都在影响最终用户能做出什么样的东西。
另一个体会是,约束比自由更重要。一个提供无限可能的工具,往往让人无从下手。而一个提供有限但精心设计的选项的工具,反而能让人快速做出像样的东西。这也是为什么主题系统、预设模板这类“约束机制”在 artcraft 类项目中如此关键。
最后分享一个小技巧:如果你也在做类似的项目,建议在早期就建立一个视觉回归测试机制。每次修改渲染逻辑后,自动生成一组标准场景的截图,和基准截图对比。这样能快速发现“改 A 影响了 B”的问题,比手动检查高效得多。