跳到主要内容

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)均源自实际源码。