跳到主要内容

FKW-凡科快图

凡科快图是一个大型在线图片制作工具平台,提供简便的可视化操作,用户可通过系统模板和拖拉拽等交互,生成高清图片。定位为 Web 端的轻量级 PS,是一款拥有复杂交互逻辑的极简平面设计工具。


目录


一、技术栈总览

类别技术选型说明
框架Vue 2.6核心视图层框架
状态管理Vuex 3.1多模块命名空间 Store
路由Vue Router 3.1部分子项目使用
图形渲染SVG + CanvasSVG 为主,Canvas 辅助(截图/滤镜/图片处理)
图形库Fabric.js 3.6(参考)元素模型设计参考了 Fabric.js 的面向对象思路
富文本Quill.js富文本编辑能力
3D 文字ThreeText(WebGL)3D 立体文字效果
构建工具Webpack 4多入口构建,代码分割
CSS 预处理Sass/SCSS全局主题变量 + 组件级样式
HTTP 请求Axios全局拦截器 + Token 管理
工具库Lodash / jQuery 3.4jQuery 用于 DOM 操作和事件委托
组件库KtuUI(自研,基于 iView)50+ 组件,支持 14+ 语言国际化
代码规范ESLint + Husky + Commitlint自定义提交类型:Add/Upd/Fix/Best/Chore/Docs/Revert
开发服务器Express + http-proxy-middleware智能请求路由代理
QR 码jsQR + artQrcode二维码识别与艺术化生成
压缩JSZip批量下载压缩
缓存IndexedDB客户端大数据量缓存

二、项目架构

2.1 整体目录结构

项目目录结构
ktupc-master/
├── build/ # Webpack 配置(comm/dev/prod/ktuui)
├── config/ # 环境配置(域名/路径/开发者映射)
├── mock/ # API Mock 数据
├── server/ # Express 开发服务器 + 代理
├── src/
│ ├── global/ # 全局共享资源
│ │ ├── components/ # 共享 Vue 组件(IconFont, Login)
│ │ ├── fonts/ # Web 字体
│ │ ├── image/ # 全局图片资源
│ │ ├── js/ # 全局 JS 模块(Quill, Swiper, jQuery 插件等)
│ │ ├── scss/ # 全局 SCSS 变量/mixin
│ │ ├── utils/ # 通用工具(cookie, md5, 浏览器检测, HTTP 拦截器)
│ │ └── vuePlugins/ # Vue 插件(vue-draggable)
│ ├── ktu/ # ★ 核心编辑器模块
│ │ ├── editor/ # PC 端完整编辑器(196 Vue 组件 + 35 基类 + 20 mixin)
│ │ ├── mEditor/ # 小程序/移动端编辑器
│ │ ├── interface/ # 模板导入界面
│ │ └── materialModal/ # 素材选择弹窗
│ ├── ktuhome/ # 首页工具集(8 个子工具)
│ ├── ktucms/ # CMS 管理后台
│ └── KtuUI/ # 自研 UI 组件库(50+ 组件)
├── package.json
├── .eslintrc.js # ESLint 规则(506 行)
└── commitlint.config.js # 提交信息规范

2.2 高层架构图

2.3 编辑器 UI 布局


三、核心模块深度解析

3.1 元素类型系统(OOP 继承体系)

元素工厂Ktu.element.processElement(data) 根据 data.type 分发到正确构造函数(textbox/cimage/background/path-group/group/qrcode 等 15+ 类型)。

3.2 状态管理架构

3.3 渲染管线

3.4 初始化流程


四、计算密集型核心算法详解

4.1 向量数学基础类(Vector)

src/ktu/editor/js/Vector.ts
class Vector {
constructor(x, y) { this.x = x; this.y = y; }

getMagnitude() { return Math.sqrt(this.x ** 2 + this.y ** 2); } // ||v||

dot(vector) { return this.x * vector.x + this.y * vector.y; } // 点积,用于投影和角度

perpendicular() { return new Vector(this.y, -this.x); } // 左手法线 (y, -x)

normal() { return this.perpendicular().normalize(); } // 单位法向量

// 标准 2D 旋转矩阵:
// [cos(θ) -sin(θ)] [x] [x·cosθ - y·sinθ]
// [sin(θ) cos(θ)] [y] = [x·sinθ + y·cosθ]
rotate(angle) {
const rad = angle * Math.PI / 180;
return { x: this.x * Math.cos(rad) - this.y * Math.sin(rad),
y: this.x * Math.sin(rad) + this.y * Math.cos(rad) };
}
}

4.2 坐标系统与元素定位

坐标模型left, top旋转后的左上角坐标(不是 AABB 左上角)。

calculateCoords() —— 旋转矩阵计算四角

src/ktu/editor/js/TheElement.ts
calculateCoords() {
const w = this.getDimensions().w * this.scaleX;
const h = this.getDimensions().h * this.scaleY;
const rad = this.angle * Math.PI / 180;

const tl = { x: this.left, y: this.top }; // 锚点
const tr = { x: tl.x + w * Math.cos(rad), y: tl.y + w * Math.sin(rad) }; // 宽度方向
const br = { x: tr.x - h * Math.sin(rad), y: tr.y + h * Math.cos(rad) }; // 高度方向
const bl = { x: tl.x - h * Math.sin(rad), y: tl.y + h * Math.cos(rad) };
const center = { x: (tl.x + br.x) / 2, y: (tl.y + br.y) / 2 };

return { tl, tr, br, bl, mt, mr, mb, ml, center };
}
              宽度方向 (cosθ, sinθ)
TL ──────────────────────→ TR
│ │
│ 高度方向 (-sinθ, cosθ) │
│ center │
↓ ↓
BL ──────────────────────→ BR

4.3 旋转算法 —— 绕中心点旋转

src/ktu/editor/js/TheElement.ts
setAngle(angle) {
const delta = angle - this.angle;
const rad = Math.PI * delta / 180;
const { center } = this.coords;

// TL 到 center 的向量,绕原点旋转 delta 角
const v = { x: this.left - center.x, y: this.top - center.y };
const rotated = {
x: Math.cos(rad) * v.x - Math.sin(rad) * v.y,
y: Math.sin(rad) * v.x + Math.cos(rad) * v.y,
};
this.left = rotated.x + center.x; // 新 TL = center + 旋转后的向量
this.top = rotated.y + center.y;
this.angle = angle;
}

4.4 SAT 碰撞检测(分离轴定理)

理论

两个凸多边形不相交 ⟺ 存在一条分离轴。对两个 OBB,只需检查 4 条轴(每个矩形 2 条边法线)。

createOBB —— 有向包围盒

src/ktu/editor/js/Interactive.ts
createOBB(obb, object) {
const rad = (obb.angle || 0) * Math.PI / 180;
return {
width: obb.width, height: obb.height,
centerPoint: new Vector(center.x, center.y),
axes: [
new Vector(Math.cos(rad), Math.sin(rad)), // 宽度轴
new Vector(-Math.sin(rad), Math.cos(rad)), // 高度轴
],
};
}

getProjectionRadius —— 投影半径

src/ktu/editor/js/Interactive.ts
// r = (W × |axis · axisW| + H × |axis · axisH|) / 2
getProjectionRadius(obb, axis) {
return (obb.width * Math.abs(axis.dot(obb.axes[0]))
+ obb.height * Math.abs(axis.dot(obb.axes[1]))) / 2;
}

isCollided —— SAT 主流程

src/ktu/editor/js/Interactive.ts
isCollided(object, customBox) {
const obbA = this.createOBB(this.selectedBox);
const obbB = this.createOBB(objectOBB, object);
const nv = obbA.centerPoint.subtract(obbB.centerPoint); // 中心连线

// 遍历 4 条轴:每条轴上 "投影半径之和 > 中心距" 则此轴无法分离
return obbA.axes.concat(obbB.axes).every(axis =>
this.getProjectionRadius(obbA, axis)
+ this.getProjectionRadius(obbB, axis) > Math.abs(nv.dot(axis))
);
}

鼠标命中检测:传入 1×1 像素 OBB 做 SAT 检测。

4.5 旋转元素的缩放(最复杂的交互)

方向正负号判定

src/ktu/editor/js/scale.ts
// 1. 控制点在元素本地坐标系的角度
const controlAngleMap = {
mr: 0, br: includedAngle, mb: 90, bl: 180-includedAngle,
ml: 180, tl: 180+includedAngle, mt: 270, tr: 360-includedAngle,
};

// 2. 屏幕空间实际角度 = 元素旋转角 + 控制点本地角度
const totalAngle = target.angle + controlAngleMap[currentControl];

// 3. 根据象限确定正负号
const plusPosition = cornerList[Math.floor(totalAngle / 90)];
const xSign = plusPosition.includes('r') ? 1 : -1;
const ySign = plusPosition.includes('b') ? 1 : -1;

非等比缩放(中间控制点)

src/ktu/editor/js/scale.ts
// ml 缩放:宽度方向 = (cos(θ), sin(θ)),TL 沿此方向反向移动
if (currentControl === 'ml') {
offset.scaleX = offsetX / target.width;
offset.left = -offsetX * Math.cos(rad);
offset.top = -offsetX * Math.sin(rad);
}
// mt 缩放:高度方向 = (-sin(θ), cos(θ))
if (currentControl === 'mt') {
offset.scaleY = offsetY / target.height;
offset.left = offsetY * Math.sin(rad);
offset.top = -offsetY * Math.cos(rad);
}

等比缩放(角控制点)

src/ktu/editor/js/scale.ts
// 按宽高比分配
offsetY = (offsetX + offsetY) / (1 + WHRatio);
offsetX = offsetY * WHRatio;

// tl:沿对角线方向偏移
if (currentControl === 'tl') {
const sideLength = Math.sqrt(offsetX ** 2 + offsetY ** 2);
offset.left = -sideLength * Math.sin(atan(WHRatio) - rad) * sign;
offset.top = -sideLength * Math.cos(atan(WHRatio) - rad) * sign;
}

4.6 SnapLine 吸附对齐

角度吸附:0°/45°/90°/135°/180°/225°/270°/315°,±5° 容差。

4.7 Group 嵌套坐标变换

4.8 图片裁剪坐标系

裁剪维护 imageShapeBox(可见裁剪区)和 imageBox(完整图片区)。变换模式:反旋转 → 本地操作 → 重旋转

旋转裁剪边界约束使用直线方程:y = (x1·y2 - x2·y1 - (y2-y1)·x) / (x1 - x2) 计算旋转边界交点。

4.9 坐标空间总结

空间定义用途
文档空间元素 left/top/scale/angle所有计算基准
屏幕空间文档 × scale + editorClientRectSAT 碰撞检测
视口空间文档 × scale + viewOffsetCSS transform
Group 本地相对 Group 原点getPositionToEditor() 转换
裁剪本地imageShapeBox + imageBox反旋转→操作→重旋转

五、SVG 滤镜与图片特效系统

5.1 SVG 滤镜架构(filters.src.js)

架构设计

编辑器实现了一套完整的 基于 SVG Filter Primitives 的图像滤镜链,支持 8 种滤镜效果的自由组合和强度调节。多个滤镜以串联链的方式组合在同一个 <filter> 标签内,SVG 会按顺序依次处理每个 filter primitive,上一个的输出作为下一个的输入。

滤镜类继承体系

滤镜链组合机制

src/ktu/editor/js/filters.src.ts
class Filters {
constructor(filters) {
this.type = filters.type || 'origin';
this.intensity = filters.intensity || 0;
this.allFilters = ['vignette', 'brightness', 'contrast', 'saturation',
'tint', 'blur', 'sharpen', 'xProcess'];
// 按名称实例化每个子滤镜
this.allFilters.forEach(name => {
if (filters[name]) {
this[name] = new ClassName(filters[name]);
}
});
}

// 生成完整的 SVG defs + filter 标签
getDefs(options) {
const defsStr = this.allFilters.reduce((s, name) =>
this[name] ? s + this[name].getDefs(options) : s, '');
const filtersStr = this.allFilters.reduce((s, name) =>
this[name] ? s + this[name].getFilter(options) : s, '');
return filtersStr ? `
<defs>
${defsStr}
<filter id="filter_${options.filterId}">
${filtersStr}
</filter>
</defs>` : '';
}
}

关键设计:多个滤镜以串联链的方式组合在同一个 <filter> 标签内,SVG 会按顺序依次处理每个 filter primitive,上一个的输出作为下一个的输入。

各滤镜的 SVG 实现详解

1. 亮度(Brightness)—— feColorMatrix
<feColorMatrix type="matrix" values="
1 0 0 0 {value}
0 1 0 0 {value}
0 0 1 0 {value}
0 0 0 1 0
"/>

原理:SVG 颜色矩阵是 5×4 矩阵,对 RGBA 做线性变换。最后一列是偏移量,给 R/G/B 通道加相同的偏移值实现亮度调节。

| R' |   | 1 0 0 0 v | | R |     R' = R + v
| G' | = | 0 1 0 0 v | | G | G' = G + v
| B' | | 0 0 1 0 v | | B | B' = B + v
| A' | | 0 0 0 1 0 | | A | A' = A
| 1 |
2. 对比度(Contrast)—— feColorMatrix
src/ktu/editor/js/filters/Contrast.ts
const slope = 1.015 * (value + 1.0) / (1.0 * (1.015 - value));
const intercept = -0.5 * slope + 0.5;
<feColorMatrix type="matrix" values="
{slope} 0 0 0 {intercept}
0 {slope} 0 0 {intercept}
0 0 {slope} 0 {intercept}
0 0 0 1 0
"/>

原理:对比度调节的数学本质是以 0.5(中灰)为中心的线性缩放。slope 是缩放系数(>1 增加对比度,<1 降低),intercept = 0.5 - 0.5 * slope 确保 0.5 灰度值不变。

输出 = slope × 输入 + intercept
= slope × (输入 - 0.5) + 0.5 // 以中灰为中心缩放
3. 饱和度(Saturation)—— feColorMatrix saturate
<feColorMatrix type="saturate" values="{value + 1}"/>

原理:SVG 内置的 saturate 类型,value=0 为灰度,value=1 为原始颜色,>1 为过饱和。内部使用加权 RGB 混合矩阵(基于人眼亮度感知系数 0.299R + 0.587G + 0.114B)。

4. 色调/染色(Tint)—— feColorMatrix + 颜色梯度
src/ktu/editor/js/filters/Tint.ts
class Tint extends Filter {
constructor(data) {
this.tintList = ['#ee82ee','#ff0000','#ffa500','#ffff00',
'#008000','#0000ff','#4b0082','#ee82ee'];
this.allTints = this.getTints(); // 生成 200 个过渡色
}

// 通过 RGB 线性插值生成色环渐变
gradient(startColor, endColor, step) {
const rStep = (end.r - start.r) / step;
// ... G, B 同理
for (let i = 0; i < step; i++) {
result.push(rgbToHex(start.r + rStep * i, ...));
}
}

getFilter() {
const tint = this.getColorByNum(this.value); // [-100, 100] → 色环颜色
const rgb = hexToRgb(tint).map(c => c / 255);
const offset = this.alpha; // 0.14 默认透明度
return `<feColorMatrix type="matrix" values="
0.86 0 0 0 ${rgb.r * offset}
0 0.86 0 0 ${rgb.g * offset}
0 0 0.86 0 ${rgb.b * offset}
0 0 0 1 0"/>`;
}
}

原理

  1. 预生成一个 200 色的色环:从紫→红→橙→黄→绿→蓝→靛→紫,通过 RGB 线性插值平滑过渡
  2. UI 滑块值 [-100, 100] 映射到色环中的一个颜色
  3. 将原图 RGB 通道缩放到 0.86(略微降低原色强度),再叠加色调偏移量(rgb × alpha
5. 模糊(Blur)—— feGaussianBlur
<feGaussianBlur stdDeviation="{value}"/>

直接使用 SVG 原生高斯模糊,stdDeviation 控制模糊半径。

6. 锐化(Sharpen)—— feConvolveMatrix
<feConvolveMatrix order="3,3" kernelMatrix="
0 {value} 0
{value} {-4v+1} {value}
0 {value} 0
"/>

原理:使用 3×3 卷积核做拉普拉斯锐化。标准拉普拉斯核为:

 0  -1   0       0    v   0
-1 5 -1 → v -4v+1 v (v 为负值时为锐化强度)
0 -1 0 0 v 0

中心值 = -4v + 1,确保核的权重之和为 1(亮度不变),周围为负值提取边缘细节。

7. 交叉处理(XProcess)—— feComponentTransfer + 贝塞尔曲线
src/ktu/editor/js/filters/XProcess.ts
class XProcess extends Filter {
getTable(xProcess) {
// 根据 xProcess 正负值,计算 R/G/B 三通道的控制点
// 正值:暖色调(红橙增强,蓝色衰减)
// 负值:冷色调(蓝色增强,红色衰减)
return [redControlPoints, greenControlPoints, blueControlPoints];
}

// 三次贝塞尔曲线插值,生成 LUT 查找表
getRange(p0, p1, p2, p3) {
// B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
function getXY(t, arr) {
const offset = 1 - t;
return arr[0]*offset**3 + 3*arr[1]*offset**2*t + 3*arr[2]*offset*t**2 + arr[3]*t**3;
}
// 以 0.05 步长采样,生成 tableValues
}

getFilter() {
return `
<feComponentTransfer>
<feFuncR type="table" tableValues="${redLUT}"/>
<feFuncG type="table" tableValues="${greenLUT}"/>
<feFuncB type="table" tableValues="${blueLUT}"/>
</feComponentTransfer>`;
}
}

原理

  1. feComponentTransfer 是 SVG 的通道传输函数,可以独立调整 R/G/B 每个通道的色调曲线
  2. 通过 4 个控制点定义三次贝塞尔曲线,模拟胶片交叉冲洗效果
  3. 将贝塞尔曲线以 0.05 步长采样,生成 tableValues(LUT 查找表)
  4. 正值 → 暖色调(十字冲洗效果),负值 → 冷色调
8. 暗角(Vignette)—— radialGradient + feImage + feComposite
src/ktu/editor/js/filters/Vignette.ts
class Vignette extends Filter {
getDefs(options) {
const startOffset = 90 - this.value; // 暗角起始位置
return `
<radialGradient id="stops_${id}" r="75%">
<stop offset="${startOffset}%" stop-color="rgb(0,0,0)" stop-opacity="0"/>
<stop offset="100%" stop-color="rgb(0,0,0)" stop-opacity="1"/>
</radialGradient>
<rect id="rect_${id}" fill="url(#stops_${id})" width="..." height="..."/>`;
}
getFilter(options) {
return `
<feImage xlink:href="#rect_${id}" result="vignette"/>
<feComposite in="vignette" in2="SourceGraphic"/>`;
}
}

原理

  1. 创建径向渐变(radialGradient):从中心透明渐变到边缘黑色
  2. 用这个渐变填充一个矩形,生成暗角遮罩
  3. feImage 将遮罩引入滤镜管线
  4. feComposite 将遮罩与原图合成(默认 over 模式,黑色半透明叠加在原图上)

滤镜链示意

5.2 Canvas 图片特效系统(imageEffects.src.js)

除了 SVG 滤镜,项目还实现了基于 Canvas 像素级操作的高级图片样式系统,用于实现 SVG 无法完成的效果。

轮廓检测 —— Marching Squares 算法

getContour() 实现了经典的 Marching Squares 轮廓追踪算法,用于提取 PNG 图片的透明边界:

src/ktu/editor/js/imageEffects.src.ts
getContour(imageData, width, start) {
// 判断像素是否非透明
const defineNonTransparent = (x, y) => (imageData[(y * width + x) * 4 + 3] > 0);

// 查找表:16 种 2×2 网格状态对应的移动方向
const contourDx = [1, 0, 1, 1, -1, 0, -1, 1, 0, 0, 0, 0, -1, 0, -1, undefined];
const contourDy = [0, -1, 0, 0, 0, -1, 0, 0, 1, -1, 1, 1, 0, -1, 0, undefined];

do {
// 取当前点周围 2×2 网格的 4 个像素状态,组合为 4-bit 索引
i = 0;
if (defineNonTransparent(x-1, y-1)) i += 1; // 左上
if (defineNonTransparent(x, y-1)) i += 2; // 右上
if (defineNonTransparent(x-1, y )) i += 4; // 左下
if (defineNonTransparent(x, y )) i += 8; // 右下

// 鞍点(对角相同)的特殊处理:使用上一步方向决定
if (i === 6) { dx = pdy === -1 ? -1 : 1; dy = 0; }
else if (i === 9) { dx = 0; dy = pdx === 1 ? -1 : 1; }
else { dx = contourDx[i]; dy = contourDy[i]; }

// 方向变化时记录拐点
if (dx !== pdx && dy !== pdy) {
c.push([x, y]);
}
x += dx; y += dy;
} while (s[0] != x || s[1] != y); // 回到起点闭合
return c;
}

Marching Squares 原理

2×2 网格有 16 种可能状态(每个像素透明/不透明):

·─· ·─■ ■─· ■─■ ·─· ·─■ ■─· ■─■
·─· ·─· ·─· ·─· ·─■ ·─■ ·─■ ·─■
0 1 2 3 4 5 6 7

·─· ·─■ ■─· ■─■ ·─· ·─■ ■─· ■─■
■─· ■─· ■─· ■─· ■─■ ■─■ ■─■ ■─■
8 9 10 11 12 13 14 15

每种状态对应一个移动方向(dx, dy),沿透明/不透明的边界行走。

用途

  • PNG 图片的描边(stroke):沿检测到的轮廓绘制描边线条
  • 多轮廓提取:getAllContours() 循环调用,每次提取一个轮廓后用 clip + clearRect 擦除,直到无剩余不透明像素
  • JPG 图片优化:无透明通道时直接返回四角坐标

Canvas 高级样式渲染

initStyleCanvas() 基于 Canvas 2D 上下文实现了多种图片特效:

src/ktu/editor/js/imageEffects.src.ts
function drawStyleCanvas() {
const cover = this.style.covers[0];

switch (this.style.id) {
case 0: // PNG 描边(支持纯色 + 线性渐变描边)
// 使用 Marching Squares 轮廓 → ctx.stroke()
// 支持 createLinearGradient 渐变描边
break;
case 1: // 阴影效果
// ctx.shadowColor / shadowBlur / shadowOffsetX/Y
break;
case 2: // 图案填充
// createPattern() 平铺/拉伸
break;
// ... 更多高级样式
}
}

关键技术点

  • 描边支持渐变:解析 JSON 格式的渐变配置,用 createLinearGradient 创建渐变笔刷
  • 防抖机制debounceFunc(callback, 100) 避免连续调整时频繁重绘
  • DOM 缓存this.imageDom 缓存已加载的图片 DOM,this.patternDom 缓存 pattern
  • 多轮廓描边:对含多个不连通区域的 PNG,依次描边每个轮廓
  • 坐标变换getStyleBoundingRect() 在旋转空间中计算含样式的真实包围盒(反旋转→加偏移→重旋转)

SVG 滤镜 vs Canvas 特效对比

维度SVG 滤镜(filters.src.js)Canvas 特效(imageEffects.src.js)
渲染方式声明式 SVG Filter Primitives命令式 Canvas 2D API
适用场景亮度/对比度/饱和度/模糊等全局调色描边/阴影/图案填充等像素级特效
性能GPU 加速(浏览器原生实现)CPU 渲染,支持防抖优化
组合方式串联在 <filter> 标签内自动链式处理手动分步绘制
输出SVG 内联,直接渲染Canvas → toDataURL → 替换 DOM
跨端后端 Node.js 可同构仅浏览器端

六、技术亮点与难点

6.1 亮点

1. Canvas → SVG 统一渲染架构重构

核心架构决策

初始版本编辑用 Canvas + 生成用 SVG → 统一采用 SVG。减少 40% 开发维护成本。SVG 可利用 Vue 响应式驱动更新,天然支持 DOM 事件和 CSS 交互,是图形编辑器类项目的最佳选择。

2. 面向对象的元素类型系统

15+ 种元素类型继承体系,工厂模式创建,统一序列化/反序列化接口,扩展性好。

3. 完整的 2D 图形引擎

自研 SAT 碰撞检测 + OBB 包围盒 + 向量运算库 + 5 层坐标空间互转 + 旋转缩放方向判定。

4. 双引擎滤镜系统

SVG Filter Primitives(8 种滤镜链式组合)+ Canvas 像素级特效(Marching Squares 轮廓检测 + 描边/阴影/图案填充)。前者 GPU 加速高效调色,后者精确像素操作。

5. SnapLine 智能吸附

6 参考线 × N 元素全量检测,4px 自适应缩放阈值,画布边缘 + 元素间双重吸附。

6. SVG 前后端同构

性能优化成果

Node.js 复用前端 toSvg() 逻辑,前端只传精简 JSON。降低 70% 保存时间,彻底解决 10M+ 大型 SVG 保存失败问题。

7. 完善的工程化演进

Python + YUI → Gulp + Babel + Sass → Webpack 4 模块化 + ESLint + Commitlint + 自研 KtuUI(50+ 组件,14+ 语言)。

8. 首屏性能优化

懒加载 + WebP + 代码分割 + HTTP 缓存 + IndexedDB。首屏 -40%,CDN 流量 -40%。

6.2 难点

1. 旋转元素的缩放方向判定

最复杂的交互逻辑

旋转后拖拽控制点的屏幕偏移方向不直接对应增大/缩小。需通过 controlAngleMap + totalAngle + 象限映射确定正负号,角控制点和中间控制点的锚点调整逻辑完全不同。这是整个编辑器中最容易出 Bug 的模块。

2. 多层坐标变换

Group 嵌套 + 翻转 + 旋转 + 缩放的 5 步变换链极易出错。

3. SVG 滤镜的数学模型

每种滤镜对应不同的 SVG Filter Primitive 和参数计算:对比度的 slope/intercept 公式、XProcess 的贝塞尔曲线 LUT 生成、锐化的拉普拉斯卷积核。

4. Marching Squares 轮廓追踪

16 种网格状态的方向查找表设计,鞍点特殊处理,多轮廓迭代提取。

5. SVG 字体渲染

自动换行手动计算、字间距逐字符定位、下划线手动绘制、倾斜用 transform 模拟。

6. Canvas → SVG 迁移

默认间距、变换基点、裁切方式、翻转 API 全不同,需逐一确保视觉一致。


七、改进建议

7.1 架构层面

建议改进方向优先级
TypeScript核心模块渐进式迁移,特别是坐标运算和滤镜参数
Vue 3 + PiniaComposition API 更适合组织复杂逻辑
Vite替代 Webpack 4,开发体验大幅提升
全局对象解耦provide/inject 替代 Ktu + defineProperty
XState 状态机管理编辑模式互斥切换,替代标志位

7.2 性能优化

建议说明
Web WorkerSAT 碰撞、SnapLine 遍历、滤镜计算移至 Worker
四叉树空间索引加速碰撞检测和吸附查询 O(n) → O(log n)
OffscreenCanvasWorker 内图片处理和缩略图生成
CSS Houdini用 Paint Worklet 替代部分 Canvas 特效渲染
WebGPU图像滤镜用 GPU Compute Shader 加速

7.3 代码质量

安全隐患

filters.src.js 中使用 eval() 动态实例化滤镜类,存在安全风险且不利于 Tree Shaking,应改为映射表方式。

建议说明
单元测试补充坐标运算 / SAT / 撤销重做 / 滤镜参数的测试
gl-matrix替代自研 Vector,支持完整矩阵运算
滤镜 eval 移除动态实例化改为映射表
Plugin 架构元素类型/工具/滤镜通过插件注册

八、面试高频问题与深度解答

Q1: 为什么从 Canvas 切换到 SVG?

维度CanvasSVG
渲染位图/像素级矢量/DOM 节点
事件需自行命中检测原生 DOM 事件
缩放放大模糊无损缩放
编程命令式声明式
交互光标/选区都要自己写天然 CSS/DOM

原因:Canvas 交互开发成本极高;双套代码维护翻倍;SVG 可利用 Vue 响应式驱动更新。 保留 Canvas:缩略图、像素级滤镜、图片导出。


Q2: SAT 碰撞检测原理?

两个 OBB 不碰撞 ⟺ 存在分离轴。4 条轴(每个矩形 2 条边法线),对每条轴:投影半径之和 > 中心距 → 无法分离。全部无法分离 → 碰撞。投影半径 r = (W×|axis·axisW| + H×|axis·axisH|) / 2。鼠标命中:1×1 OBB 做 SAT。


Q3: 旋转后的缩放怎么处理?

  1. 方向判定totalAngle = 元素角度 + 控制点本地角度,根据象限确定 X/Y 正负号
  2. 非等比(中间控制点):只改一个维度,锚点沿对应旋转轴反向移动
  3. 等比(角控制点):按 W:H 比分配偏移,TL 沿对角线方向偏移(极坐标转笛卡尔)
  4. 对角固定acrossMap 映射对角控制点作为锚点

Q4: SVG 滤镜系统是怎么实现的?

8 种滤镜串联在一个 <filter> 标签内,每种对应一个 SVG Filter Primitive:

  • 亮度:feColorMatrix 偏移 R/G/B 通道
  • 对比度:feColorMatrix 以 0.5 为中心的线性缩放(slope/intercept 公式)
  • 饱和度:feColorMatrix saturate 类型
  • 色调:预生成 200 色色环 → feColorMatrix 叠加色彩偏移
  • 模糊:feGaussianBlur
  • 锐化:feConvolveMatrix 3×3 拉普拉斯核
  • 交叉处理:feComponentTransfer,三次贝塞尔曲线插值生成 R/G/B 通道 LUT
  • 暗角:radialGradient → feImage + feComposite 合成

Q5: 图片描边怎么做的?PNG 透明图片怎么描边?

使用 Marching Squares 轮廓追踪算法:

  1. 取图片 imageData,遍历 2×2 像素网格
  2. 4 个像素的透明状态组合为 4-bit 索引(16 种状态)
  3. 查找表确定移动方向,沿透明/不透明边界行走
  4. 方向变化时记录拐点,回到起点闭合
  5. 轮廓描边后 clip + clearRect 擦除已处理区域
  6. 循环直到无剩余不透明像素(支持多个不连通区域)
  7. 最后用 Canvas stroke() 沿轮廓线绘制描边(支持渐变色描边)

Q6: 撤销/重做怎么实现的?

命令模式 + 快照栈:操作前 saveState() 深拷贝 → 操作后 modifiedState() 深拷贝 → 压入 {before, after, actionType} 历史栈(max 100)。25+ 种 HistoryAction 类型,双向反转映射。新操作清空 pointer 之后的分支。


Q7: 大型 SVG 保存失败怎么解决的?

前端只传精简 JSON → 后端 Node.js 同构 toSvg() 逻辑构建完整 SVG → 生成高清图。降低 70% 保存时间,彻底解决 10M+ 失败问题。


Q8: SnapLine 吸附算法?

6 参考点(左/中/右 × 上/中/下)与所有元素的 6 参考点做 3×3 距离比较,阈值 4px/scale(自适应缩放),同时检测画布边缘。角度吸附到 45° 倍数(±5° 容差)。


Q9: 工程化演进历程?


Q10: 如果重新设计会怎么做?

  • Vue 3 + TypeScript + Pinia + Vite
  • XState 状态机管理编辑模式
  • 四叉树加速碰撞/吸附
  • Web Worker 执行计算密集操作
  • WebGPU 加速滤镜
  • CRDT 支持多人协作
  • Plugin 架构提升扩展性
  • gl-matrix 替代 Vector
  • Vitest + Playwright 测试体系

附加高频追问

问题考察点
feColorMatrix 的 5×4 矩阵每个参数含义?SVG 滤镜原理
贝塞尔曲线的参数方程?如何用于 LUT 生成?数学基础
卷积核的权重之和为什么要等于 1?图像处理原理
Marching Squares 的 16 种状态怎么设计查找表?算法设计
2D 旋转矩阵的推导?线性代数
点积的几何意义?项目中哪些地方用到?SAT 投影 + 角度计算 + 旋转偏移
四叉树如何优化碰撞检测?空间索引
Vue 2 defineProperty 的局限?响应式原理
requestAnimationFrame vs setTimeout浏览器渲染机制
Web Worker 通信开销?Transferable Objects?多线程性能
CRDT vs OT?实时协作怎么做?协同编辑
Canvas toDataURL 有什么限制?跨域/内存/格式

本文档基于对 src/ktu/ 完整源码的深度扫描生成,涵盖 editor(196 个 Vue 组件 + 35 个基类文件 + 20 个 mixin + 14 个全局模块)、mEditor、interface、materialModal 四个核心子模块。所有算法分析(SAT/坐标变换/旋转缩放/吸附/SVG 滤镜/Marching Squares)均源自实际源码。