Article

工作流 Paper.js

更新于:2026-07-10

第1章:Paper.js 简介与环境搭建

1.1 什么是 Paper.js

概念名称说明注意事项
Paper.js一个基于 HTML5 Canvas 的矢量图形脚本框架,用于创建交互式图形和可视化内容。不是直接操作 DOM,而是通过 JavaScript 在 Canvas 上绘制矢量图形。
矢量图形图形以数学方式定义(如点、路径、曲线),可无限缩放而不失真。适合绘制图标、图表、动画等需要高清晰度的图形。
脚本化绘图所有图形通过 JavaScript 创建和控制,支持动态生成和实时交互。与传统使用图像文件(如 PNG、SVG 文件)的方式不同,更灵活但需编程基础。
客户端运行运行在浏览器中,依赖 Canvas API,不依赖服务器端处理。需确保目标浏览器支持 HTML5 Canvas。

1.2 Paper.js 的核心特性

特性名称说明注意事项
分层结构(Layers)支持多个图层管理,便于组织复杂图形。每个项目默认有一个主图层,可创建多个 Layer 对象进行分层绘制。
路径对象(Paths)提供丰富的路径创建与操作 API,支持直线、曲线、贝塞尔等。Path 是最常用的图形类型,可用于绘制任意形状。
布尔运算支持路径之间的合并、相交、差集等布尔操作。使用 unite()、intersect()、subtract() 等方法实现复杂形状生成。
事件驱动交互内置鼠标、键盘事件支持,便于实现用户交互。事件绑定需在 setup 后注册,且依赖 activeView。
动画支持通过 onFrame 事件实现帧级动画控制。动画逻辑写在 onFrame 回调中,由 requestAnimationFrame 驱动。
SVG 导入/导出可将图形导出为 SVG 字符串,也可从 SVG 导入内容。exportSVG() 可生成标准 SVG,适合保存或传输;importSVG() 支持大部分 SVG 元素。
模块化设计核心功能按模块组织,支持按需引入(在构建版本中)。使用 npm 安装时可通过 ES6 import 引入特定模块。

1.3 开发环境准备与项目引入方式

引入方式语法示例用途说明注意事项
CDN 引入<script src="https://unpkg.com/paper-js@0.12.17/dist/paper-full.min.js"></script>快速在网页中使用 Paper.js,无需本地安装。推荐用于学习和原型开发;生产环境建议使用本地依赖管理。
NPM 安装npm install paper用于现代前端项目(如 Webpack、Vite)。安装后需通过 import 或 require 引入:import * as paper from 'paper';
本地文件引入<script src="js/paper-full.min.js"></script>将 Paper.js 文件下载到本地项目中使用。可避免网络依赖,提升加载速度。
模块化引入(ES6)import * as paper from 'paper';在支持 ES6 模块的环境中使用。需配置打包工具(如 Webpack)以正确解析 paper 模块。
Node.js 环境npm install paper-canvas; const paper = require('paper-canvas');在服务端生成图形或进行图像处理。需使用 paper-canvas 等兼容库,不支持 DOM 或 Canvas 交互事件。

1.4 第一个 Paper.js 程序:Hello Paper

方法/属性语法用途说明代码示例注意事项
paper.setup()paper.setup(canvas)初始化 Paper.js 项目,绑定指定 canvasconst canvas = document.getElementById('myCanvas'); paper.setup(canvas);必须在绘制任何图形前调用;canvas 必须存在于 DOM 中。
new Path.Circle()new Path.Circle(center, radius)创建一个圆形路径const circle = new Path.Circle(new Point(80, 80), 50);构造函数参数为中心点(Point)和半径(Number)。
fillColorpath.fillColor = color设置路径的填充颜色circle.fillColor = 'red';可接受字符串颜色(如 ‘red’)、十六进制(‘#ff0000’)或 Color 对象。
view.draw()paper.view.draw()手动触发视图重绘(可选)paper.view.draw();通常不需要手动调用,Paper.js 会自动在操作后重绘;可用于强制刷新。

完整示例代码:

const canvas = document.getElementById('myCanvas');
paper.setup(canvas);
const center = new Point(100, 100);
const circle = new Path.Circle(center, 80);
circle.fillColor = 'blue';
paper.view.draw();

提示:确保 HTML 中有一个 id 为 ‘myCanvas’ 的 <canvas> 元素,并设置宽高。


第2章:核心概念与项目结构

2.1 项目(Project)与作用域(Scope)

概念名称说明注意事项
Project表示一个完整的图形项目,包含图层(Layer)、路径、图像等所有可视元素。每个 Paper.js 实例至少有一个 Project,可通过 paper.project 访问。
Scope作用域,封装了 Project、View、Tool 等核心对象的运行环境。paper 是默认的全局作用域,所有 Paper.js 对象都挂载在作用域下。
多项目支持支持在同一页面创建多个独立的 Project,分别绑定到不同 Canvas。不同 Project 之间数据隔离,需通过作用域切换或实例化新 paper.Context 实现。
paper.project当前活动项目的引用,用于访问和操作当前项目的图形内容。所有新创建的图形对象(如 Path)自动添加到 activeProject 中。
作用域隔离可通过 new paper.PaperScope() 创建独立作用域,避免命名冲突。高级用法,适用于嵌入式组件或多实例应用。

2.2 视图(View)与坐标系统

概念名称说明注意事项
View表示画布的可视化视图,负责渲染 Project 中的内容。每个 Project 关联一个 View,通过 paper.view 访问。
Canvas 坐标系原点 (0,0) 位于左上角,x 向右增加,y 向下增加。与数学坐标系不同,注意 Y 轴方向。
Point表示二维坐标点,格式为 { x, y }可通过 new Point(x, y) 创建,支持向量运算(加、减、缩放等)。
Size表示宽高尺寸,格式为 { width, height }通常用于获取视图大小:view.size.width, view.size.height
view.size当前视图的尺寸(Size 对象),反映 Canvas 的宽高。可直接读取或设置以调整画布渲染区域(不改变 DOM 尺寸)。
像素密度适配View 自动处理高 DPI 屏幕(如 Retina)的像素比问题。无需手动处理 devicePixelRatio,Paper.js 内部已优化。

2.3 活动状态管理:activeProject、activeView、activeLayer

属性名称语法用途说明代码示例注意事项
paper.projectpaper.project获取当前活动项目const proj = paper.project;所有图形操作默认作用于当前 activeProject。
paper.viewpaper.view获取当前活动视图const v = paper.view;用于访问视图尺寸、绑定事件、触发重绘等。
project.activeLayerproject.activeLayer获取或设置当前活动图层const layer = paper.project.activeLayer;新创建的项目项(如 Path)自动添加到 activeLayer。
project.layersproject.layers获取项目中所有图层的数组const layers = paper.project.layers;支持通过索引访问图层:layers[0]
layer.activate()layer.activate()将指定图层设置为当前活动图层const myLayer = new Layer(); myLayer.activate();调用后,后续创建的对象将自动加入该图层。

2.4 入口函数:paper.setup() 与 paper.view.onFrame()

方法/属性语法用途说明代码示例注意事项
paper.setup(canvas)paper.setup(canvasElement)初始化 Paper.js 项目,绑定 Canvas 元素paper.setup(document.getElementById('canvas'));必须在任何绘图操作前调用;一个 Canvas 只能 setup 一次。
paper.view.onFramepaper.view.onFrame = function(event)注册每帧执行的回调函数,用于实现动画paper.view.onFrame = function(event) { circle.rotate(2); };event.delta 表示上一帧到当前帧的时间间隔(秒),可用于平滑动画。
event.countevent.count表示当前是第几帧(从 1 开始)console.log('Frame:', event.count);适用于需要计数的动画逻辑。
event.timeevent.time表示从页面加载到当前帧的总时间(秒)const t = event.time;可用于基于时间的动画(如正弦波运动)。
event.deltaevent.delta表示当前帧与上一帧之间的时间差(秒)const speed = 100 * event.delta; // 每秒移动 100 单位推荐使用 delta 实现帧率无关的动画。
移除 onFrame 事件paper.view.onFrame = null停止帧更新循环paper.view.onFrame = null;用于暂停动画或释放资源。

第3章:路径(Path)基础

3.1 创建基本路径:Path 对象

方法/属性语法用途说明代码示例注意事项
new Path()new Path()创建一个空的路径对象const path = new Path();需手动添加线段或点才能显示。
path.add(point)path.add(point)向路径中添加一个点或线段path.add(new Point(50, 50));可连续调用添加多个点,形成折线。
path.moveTo()path.moveTo(point)将绘图起点移动到指定点(不画线)path.moveTo(new Point(10, 10));用于开始新子路径或跳转位置。
path.lineTo()path.lineTo(point)从当前点画直线到指定点path.lineTo(new Point(100, 100));必须先有起点(如通过 moveTo 或 add)。
path.closedpath.closed = true/false设置路径是否闭合(首尾连接)path.closed = true;闭合路径会自动从最后一个点连回第一个点。
path.strokeColorpath.strokeColor = color设置路径描边颜色path.strokeColor = 'black';接受字符串、十六进制、Color 对象等。
path.fillColorpath.fillColor = color设置路径填充颜色path.fillColor = '#ffcc00';仅闭合路径支持填充。

3.2 直线路径:Path.Line

构造函数语法用途说明代码示例注意事项
Path.Linenew Path.Line(from, to)创建两点之间的直线路径const line = new Path.Line(new Point(20, 20), new Point(100, 100));自动设置 strokeColor 可见,但默认无填充。
fromline.from获取直线起点const start = line.from;返回 Point 对象。
toline.to获取直线终点const end = line.to;返回 Point 对象。
strokeColorline.strokeColor = color设置直线颜色line.strokeColor = 'blue';必须设置描边才可见。
strokeWidthline.strokeWidth = width设置线宽line.strokeWidth = 3;默认为 1,可设为任意正数。

3.3 折线与多边形:Path.RegularPolygon、Path.Rectangle

构造函数语法用途说明代码示例注意事项
Path.RegularPolygonnew Path.RegularPolygon(center, sides, radius)创建正多边形const hexagon = new Path.RegularPolygon(new Point(100, 100), 6, 50);sides 为边数(≥3),radius 为外接圆半径。
centerpolygon.center获取多边形中心点const center = hexagon.center;只读属性。
sidespolygon.sides获取边数const n = hexagon.sides;只读属性。
radiuspolygon.radius获取外接圆半径const r = hexagon.radius;只读属性。
Path.Rectanglenew Path.Rectangle(rect)new Path.Rectangle(point, size)创建矩形路径const rectPath = new Path.Rectangle(new Point(50, 50), new Size(100, 80));第一种用 Rectangle 对象,第二种用起点和尺寸。
rectanglerectPath.rectangle获取路径对应的矩形区域const rect = rectPath.rectangle;返回 Rectangle 对象。
roundRadiusrectPath.roundRadius = radiusnew Size(h, v)设置圆角半径rectPath.roundRadius = 10; // 或 new Size(15, 8)可设为单值(等圆角)或 Size(水平/垂直不同)。

3.4 圆形与椭圆:Path.Circle、Path.Ellipse

构造函数语法用途说明代码示例注意事项
Path.Circlenew Path.Circle(center, radius)创建圆形路径const circle = new Path.Circle(new Point(100, 100), 60);center 为圆心,radius 为半径。
centercircle.center获取圆心const center = circle.center;返回 Point 对象。
radiuscircle.radius获取半径const r = circle.radius;返回数值。
Path.Ellipsenew Path.Ellipse(rect, rotation)创建椭圆路径const ellipse = new Path.Ellipse(new Rectangle(50, 50, 120, 80));rect 定义外接矩形,rotation 可选(弧度)。
ellipse.rectellipse.rect获取椭圆外接矩形const rect = ellipse.rect;返回 Rectangle 对象。
ellipse.rotationellipse.rotation = angle设置椭圆旋转角度(弧度)ellipse.rotation = Math.PI / 4;仅当创建时未指定 rotation 才可修改。

3.5 弧线与曲线:Path.Arc、Path.BezierCurve

构造函数语法用途说明代码示例注意事项
Path.Arcnew Path.Arc(from, through, to)创建通过三点的弧线(起、经、终)const arc = new Path.Arc([20, 100], [100, 20], [180, 100]);自动计算贝塞尔控制点,形成平滑弧线。
fromarc.from获取弧线起点const start = arc.from;返回 Point 对象。
througharc.through获取弧线经过的中间点const mid = arc.through;返回 Point 对象。
toarc.to获取弧线终点const end = arc.to;返回 Point 对象。
Path.BezierCurve.evaluate()Path.BezierCurve.evaluate(segment1, segment2, t)计算贝塞尔曲线上某参数 t 处的点const pt = Path.BezierCurve.evaluate(seg1, seg2, 0.5);t ∈ [0,1],seg1 和 seg2 为两个 Segment(含控制点)。
evaluate()Path.BezierCurve.evaluate(segment1, segment2, t)同上,用于插值计算const tangent = Path.BezierCurve.getTangent(seg1, seg2, 0.5);可获取切线、法线、长度等。
getTangent()Path.BezierCurve.getTangent(segment1, segment2, t)获取 t 处的切线向量const tangent = Path.BezierCurve.getTangent(seg1, seg2, 0.5);返回 Point 对象表示方向。
getNormal()Path.BezierCurve.getNormal(segment1, segment2, t)获取 t 处的法线向量const normal = Path.BezierCurve.getNormal(seg1, seg2, 0.5);垂直于切线,用于动画方向控制。

3.6 自由绘制路径:Path 与鼠标的交互

方法/事件语法用途说明代码示例注意事项
tool.onMouseDowntool.onMouseDown = function(event) { }鼠标按下时触发,开始新路径tool.onMouseDown = function(e) { path = new Path(); path.add(e.point); };通常在此创建新 Path。
tool.onMouseDragtool.onMouseDrag = function(event) { }鼠标拖拽时持续触发tool.onMouseDrag = function(e) { path.add(e.point); };每次拖动产生事件,添加点到路径。
tool.onMouseUptool.onMouseUp = function(event) { }鼠标释放时触发tool.onMouseUp = function(e) { console.log('Drawn!'); };可用于结束绘制、分析路径等。
event.pointevent.point获取事件发生时的坐标点const p = event.point;基于视图坐标系。
event.deltaevent.delta获取本次拖拽移动的偏移量const move = event.delta;用于判断拖动方向或速度。
path.smooth()path.smooth()平滑路径,使其更流畅path.smooth();基于曲率自动调整控制点,适合手绘路径优化。
path.simplify()path.simplify([tolerance])简化路径点数,减少冗余点path.simplify(2.5);tolerance 越大,简化程度越高;可提升性能。

第4章:路径操作与属性

4.1 路径的几何属性:position、bounds、strokeWidth 等

属性名称语法用途说明代码示例注意事项
positionpath.position获取或设置路径的中心位置(基于包围盒)path.position = new Point(200, 150);移动整个路径,不影响其内部点的相对位置。
boundspath.bounds获取路径的边界矩形(Rectangle 对象)const rect = path.bounds; console.log(rect.width, rect.height);包含旋转、缩放后的实际显示范围;只读。
sizepath.bounds.size获取路径包围盒的尺寸const size = path.bounds.size;返回 Size 对象,可用于缩放参考。
areapath.area获取闭合路径的填充面积const area = path.area;仅对闭合路径有效;非闭合路径返回 0。
lengthpath.length获取路径总长度(所有线段之和)const len = path.length;可用于动画进度控制(如沿线移动)。
strokeWidthpath.strokeWidth = width设置描边宽度path.strokeWidth = 4;默认为 1;可为任意正数,支持小数(如 0.5)。
selectedpath.selected = true/false设置路径是否处于选中状态(可视化高亮)path.selected = true;便于调试或交互操作;样式由视图决定。

4.2 描边与填充:strokeColor、fillColor、strokeCap、strokeJoin

属性名称语法用途说明代码示例注意事项
strokeColorpath.strokeColor = color设置描边颜色path.strokeColor = 'red';path.strokeColor = '#00ff00';支持字符串、十六进制、RGB/HSB 对象。
fillColorpath.fillColor = color设置填充颜色path.fillColor = new Color(0.5, 0.8, 1); // RGB 值 0~1 范围仅闭合路径有效;可使用渐变(Gradient)。
strokeCappath.strokeCap = style设置线端样式path.strokeCap = 'round'; // 'butt', 'round', 'square'影响开放路径的起点和终点外观。
strokeJoinpath.strokeJoin = style设置线段连接处样式path.strokeJoin = 'bevel'; // 'miter', 'bevel', 'round'仅对折线或路径拐角处有效。
dashArraypath.dashArray = [on, off]设置虚线模式path.dashArray = [10, 5]; // 每 10 单位实线,5 单位空白数组长度可为偶数多个值,实现复杂虚线;需配合 dashOffset 使用。
dashOffsetpath.dashOffset = offset设置虚线起始偏移量path.dashOffset = 3;控制虚线的相位,可用于动画滚动效果。

4.3 路径点操作:segments、add()、insert()、remove()

方法/属性语法用途说明代码示例注意事项
segmentspath.segments获取路径的所有线段(Segment 数组)const segs = path.segments;每个 Segment 包含 point、handleIn、handleOut。
add(point)path.add(point)在路径末尾添加一个点path.add(new Point(100, 100));自动连接到最后一个点。
add(segment)path.add(new Segment(point))添加一个 Segment 对象path.add(new Segment(new Point(150, 150)));可包含控制点信息。
insert(index, point)path.insert(index, point)在指定索引处插入点path.insert(1, new Point(80, 80));索引从 0 开始;超出范围会抛出错误。
removeSegment(index)path.removeSegment(index)移除指定索引的线段path.removeSegment(0);移除后路径结构改变;若只剩一个点,路径仍存在。
removeSegments(from, to)path.removeSegments(from, to)移除指定范围内的所有线段path.removeSegments(1, 3); // 移除第1到第2个(左闭右开)to 参数可选,默认到末尾。
firstSegmentpath.firstSegment获取第一个线段const first = path.firstSegment;返回 Segment 对象。
lastSegmentpath.lastSegment获取最后一个线段const last = path.lastSegment;返回 Segment 对象。
clear()path.clear()清空所有线段,变为空路径path.clear();路径对象仍存在,但无任何点。

4.4 路径布尔运算:unite()、subtract()、intersect()、exclude()

方法名称语法用途说明代码示例注意事项
unite(path)path1.unite(path2)并集:合并两个路径的覆盖区域const union = circle.unite(rectangle);返回新路径,原路径不变;结果为闭合路径。
subtract(path)path1.subtract(path2)差集:从 path1 中减去 path2 的区域const diff = circle.subtract(rectangle);path1 保留不被 path2 覆盖的部分。
intersect(path)path1.intersect(path2)交集:仅保留两个路径重叠的区域const inter = circle.intersect(rectangle);结果可能为多个子路径。
exclude(path)path1.exclude(path2)异或:保留仅在一个路径中的区域const xor = circle.exclude(rectangle);去除重叠部分,保留非共享区域。
divide(path)path1.divide(path2)分割:path1 被 path2 分割成多个部分const divided = circle.divide(rectangle);返回 path1 被切割后的结果,常用于复杂形状分解。
克隆参与运算path.clone()布尔运算会修改路径结构,建议克隆后再操作const result = path1.clone().unite(path2.clone());避免意外修改原始图形。

4.5 路径简化与平滑:simplify()、smooth()

方法名称语法用途说明代码示例注意事项
simplify(tolerance)path.simplify([tolerance])简化路径,减少线段数量path.simplify();path.simplify(5.0);tolerance 越大,简化程度越高;默认值约为 2.5;保持形状大致不变。
smooth()path.smooth()平滑路径,使其更流畅美观path.smooth();自动调整控制点,适合手绘路径优化。
smooth([options])path.smooth({ type: 'catmull-rom' })指定平滑算法path.smooth({ type: 'catmull-rom' }); // 或 'geometric', 'continuous'不同算法效果不同:‘catmull-rom’ 更圆滑,‘geometric’ 保留更多原始特征。
曲率控制smooth({ factor: 0.4 })调整平滑强度path.smooth({ factor: 0.6 });factor ∈ [0,1],值越大曲线越平滑。
角度阈值smooth({ threshold: 60 })设置角度变化阈值(度)path.smooth({ threshold: 45 });大于阈值的角点保留尖锐,小于则平滑。

第5章:点、尺寸与矩阵

5.1 Point 类:二维坐标表示

属性/方法语法用途说明代码示例注意事项
new Point(x, y)new Point(x, y)new Point([x, y])创建一个二维坐标点const p1 = new Point(100, 200); const p2 = new Point([50, 75]);支持数字数组或直接传入 x, y。
point.x / point.ypoint.x, point.y获取或设置点的 X 和 Y 坐标p1.x = 150; console.log(p1.y);可直接修改,实时影响关联图形。
add()point.add(otherPoint)向当前点加上另一个点const sum = p1.add(p2); // (150+50, 200+75)返回新 Point,不修改原对象。
subtract()point.subtract(otherPoint)当前点减去另一个点const diff = p1.subtract(p2);常用于计算向量或偏移量。
multiply()point.multiply(scalar)点坐标乘以标量(缩放)const scaled = p1.multiply(2); // (300, 400)可用于缩放位置。
divide()point.divide(scalar)点坐标除以标量const half = p1.divide(2);避免除零。
dot()point.dot(otherPoint)计算两个点的点积(内积)const dotProduct = p1.dot(p2);结果为数值:x1*x2 + y1*y2。
lengthpoint.length获取点到原点的距离(向量长度)const dist = p1.length;即 √(x² + y²)。
anglepoint.angle获取点相对于 X 轴的角度(度)const angle = p1.angle;范围:-180° 到 180°。
normalize()point.normalize([length])将点转换为单位向量(可指定长度)const unit = p1.normalize(); // 长度为 1 的方向向量常用于方向计算。
rotate()point.rotate(angle, center)绕某点旋转指定角度const rotated = p1.rotate(45, new Point(0,0));angle 为角度(非弧度),center 可选,默认绕原点。

5.2 Size 类:宽高表示

属性/方法语法用途说明代码示例注意事项
new Size(width, height)new Size(w, h)new Size([w, h])创建一个尺寸对象const size1 = new Size(100, 80); const size2 = new Size([50, 30]);常用于矩形、图像、字体等的尺寸定义。
size.width / heightsize.width, size.height获取或设置宽度和高度size1.width = 120; console.log(size1.height);可动态调整。
add()size.add(otherSize)尺寸相加const total = size1.add(size2);返回新 Size 对象。
subtract()size.subtract(otherSize)尺寸相减const diff = size1.subtract(size2);用于计算尺寸差。
multiply()size.multiply(scalar)尺寸乘以标量(缩放)const doubled = size1.multiply(2);常用于响应式布局或缩放。
divide()size.divide(scalar)尺寸除以标量const half = size1.divide(2);避免除零。
equals()size.equals(otherSize)判断两个尺寸是否相等if (size1.equals(size2)) { ... }精确比较 width 和 height。
isZero()size.isZero()判断尺寸是否为零if (size.isZero()) { ... }即 width === 0 && height === 0。

5.3 Rectangle 类:矩形区域定义

属性/方法语法用途说明代码示例注意事项
new Rectangle(point, size)new Rectangle(point, size)通过左上角点和尺寸创建矩形const rect = new Rectangle(new Point(10, 10), new Size(100, 80));最常用构造方式。
new Rectangle(from, to)new Rectangle(from, to)通过两个对角点创建矩形const rect = new Rectangle([10,10], [110,90]);自动计算左上和右下角。
rect.pointrect.point获取矩形左上角坐标const topLeft = rect.point;返回 Point 对象。
rect.sizerect.size获取矩形尺寸const dim = rect.size;返回 Size 对象。
rect.centerrect.center获取矩形中心点const center = rect.center;只读属性,基于 point 和 size 计算得出。
rect.topLeft / topRight / bottomLeft / bottomRightrect.topRight获取四个角点const tr = rect.topRight;返回 Point 对象,便于定位。
rect.width / heightrect.width, rect.height快速访问宽度和高度rect.width = 150;等价于修改 size.width。
contains(point)rect.contains(point)判断点是否在矩形内部(含边界)if (rect.contains(mousePos)) { ... }常用于碰撞检测或交互判断。
intersect(rect)rect.intersect(otherRect)计算两个矩形的交集区域const overlap = rect1.intersect(rect2);若无重叠,返回 null 或空矩形。
union(rect)rect.union(otherRect)计算两个矩形的并集(最小包围矩形)const bounds = rect1.union(rect2);包含两个矩形的最小矩形。
expand(delta)rect.expand(delta)扩展矩形边界(支持负值收缩)const padded = rect.expand(10); // 四周各扩10单位delta 可为数字或 Size。

5.4 Matrix 类:变换矩阵与几何变换

属性/方法语法用途说明代码示例注意事项
new Matrix()new Matrix()创建单位矩阵(无变换)const matrix = new Matrix();初始状态:[[1,0,0],[0,1,0],[0,0,1]]
translate(dx, dy)matrix.translate(dx, dy)平移变换matrix.translate(50, 30);移动坐标系原点。
scale(sx, sy, center)matrix.scale(sx, [sy], center)缩放变换(可指定中心点)matrix.scale(2, 1.5);matrix.scale(2, null, new Point(100,100));sy 可选,默认与 sx 相同;center 控制缩放中心。
rotate(angle, center)matrix.rotate(angle, center)旋转变换matrix.rotate(45, new Point(50,50));angle 为角度(非弧度);绕指定点旋转。
skew(sx, sy, center)matrix.skew(sx, sy, center)错切(倾斜)变换matrix.skew(10, 0);sx, sy 为倾斜角度。
append(matrix)matrix.append(otherMatrix)将另一个矩阵变换追加到当前矩阵const combined = mtx1.append(mtx2);变换顺序重要:先执行的矩阵在右(数学上右乘)。
prepend(matrix)matrix.prepend(otherMatrix)将另一个矩阵前置(先执行)matrix.prepend(translation);与 append 相反,先应用该变换。
applyTo(item)matrix.applyTo(item)将矩阵变换应用到项目(路径、组等)matrix.applyTo(path);直接修改项目的位置和形态。
inverted()matrix.inverted()获取逆矩阵(撤销变换)const inverse = matrix.inverted();若矩阵不可逆(如缩放为0),返回 null。
isIdentitymatrix.isIdentity判断是否为单位矩阵(无任何变换)if (matrix.isIdentity) { ... }用于状态检测。
valuesmatrix.values获取矩阵的 6 个核心值 [a,b,c,d,tx,ty]const [a,b,c,d,tx,ty] = matrix.values;对应仿射变换矩阵:[a c tx; b d ty; 0 0 1]

提示:Matrix 是实现复杂动画、自定义变换和坐标系统转换的核心工具,常与 Item.transform(matrix) 配合使用。


第6章:颜色与渐变

6.1 Color 类与颜色表示方式(RGB、HSB、灰度)

表示方式语法与构造方式用途说明代码示例注意事项
RGB(红绿蓝)new Color(r, g, b) / new Color(r, g, b, alpha)使用红、绿、蓝三原色定义颜色,支持透明度const red = new Color(1, 0, 0); // 红色 const semiBlue = new Color(0, 0, 1, 0.5); // 半透明蓝色分量范围:0 ~ 1(浮点),不是 0~255!支持 alpha 通道(透明度)。
十六进制字符串new Color('#hex') / Color('#ff0000')使用 CSS 风格的十六进制颜色const green = new Color('#00ff00'); const gray = new Color('#ccc'); // 3位简写支持 #rgb 和 #rrggbb,大小写不敏感,内部自动转为 RGB。
颜色名称new Color('colorName')使用预定义的颜色名称const orange = new Color('orange'); const purple = new Color('purple');支持标准 CSS 颜色名(如 red, blue, aqua 等),可读性强,适合原型设计。
HSB(色相、饱和度、亮度)Color.hsb(hue, saturation, brightness) / Color.hsb(h, s, b, alpha)基于人眼感知的颜色模型,便于调色const vibrant = Color.hsb(120, 1, 1); // 鲜艳绿色 const pastel = Color.hsb(240, 0.3, 0.9); // 淡蓝色hue: 0360(度),saturation, brightness: 01,更直观地调整色彩”感觉”。
灰度(Grayscale)new Color(gray) / new Color(gray, alpha)使用单一灰度值定义黑白灰颜色const black = new Color(0); const midGray = new Color(0.5); const white = new Color(1);gray 值:0(黑)→ 1(白),可加 alpha 控制透明度,省内存,适合单色设计。

颜色转换与访问:

所有颜色内部统一存储,可相互转换:

const color = new Color('#ff9900');
console.log(color.red, color.green, color.blue); // 1, 0.6, 0
console.log(color.hue, color.saturation, color.brightness); // 36, 1, 1

属性访问:color.red, color.hue, color.gray, color.alpha 等。

6.2 Gradient 与 GradientStop

概念语法与结构用途说明代码示例注意事项
Gradient(渐变)new Gradient(stops, radial)定义一个渐变,包含多个颜色停靠点const gradient = new Gradient([new GradientStop('red', 0), new GradientStop('yellow', 0.5), new GradientStop('blue', 1)], false); // 线性stops: GradientStop 数组,radial: true 为径向渐变,false 为线性。
GradientStop(渐变停靠点)new GradientStop(color, offset)定义渐变中某一位置的颜色const stop1 = new GradientStop('white', 0); const stop2 = new GradientStop(new Color(0,0,0), 1);color: 可为 Color 对象或字符串,offset: 0~1,表示位置比例。
线性渐变(Linear)new Gradient(stops, false)颜色沿直线方向过渡const linearGrad = new Gradient([['red', 0], ['blue', 1]]); // 简写数组形式默认方向:从左到右(水平),可通过 GradientColor 的 origin 和 destination 控制方向。
径向渐变(Radial)new Gradient(stops, true)颜色从中心向外辐射状过渡const radialGrad = new Gradient([['yellow', 0], ['red', 1]], true);视觉效果:中心色 → 外围色,常用于光晕、按钮立体感。

提示:Gradient 对象本身不包含位置信息,需与 GradientColor 结合使用来定义实际填充区域。

6.3 应用渐变填充:fillColor 与 strokeColor 使用 Gradient

属性/概念语法用途说明代码示例注意事项
GradientColornew GradientColor(gradient, origin, destination)new GradientColor(gradient, center, radius)将 Gradient 与空间位置结合,用于实际填充const linColor = new GradientColor(linearGrad, new Point(0, 0), new Point(100, 100)); const radColor = new GradientColor(radialGrad, new Point(50, 50), 50);线性:origin → destination 定义渐变方向;径向:center 和 radius 定义圆形区域;必须指定空间参数才能渲染。
fillColor 使用渐变item.fillColor = gradientColor;用渐变填充路径或形状内部const circle = new Path.Circle([50,50], 40); circle.fillColor = radColor;仅闭合路径有效,支持复杂形状的自然渐变映射。
strokeColor 使用渐变item.strokeColor = gradientColor;用渐变作为描边颜色const line = new Path.Line([0,0], [100,100]); line.strokeWidth = 10; line.strokeColor = linColor;需设置足够 strokeWidth 才能看清渐变效果。
渐变方向控制调整 origin/destination 或 center/radius自定义渐变的方向和范围const vertColor = new GradientColor(gradient, new Point(50, 0), new Point(50, 100));利用 Point 精确控制,可实现对角、水平、垂直等任意方向。
动画渐变动态修改 GradientStop 或 GradientColor 参数实现渐变颜色或位置的动画gradient.stops[1].color = 'green'; gradColor.origin.x += 1;可结合 requestAnimationFrame 实现流畅动画,适合加载动画、交互反馈等场景。

最佳实践:

  • 渐变设计建议使用 2~4 个 GradientStop,过多会降低性能且不易控制。
  • 径向渐变常用于模拟光照、按钮凸起效果。
  • 线性渐变适合背景、条形图、现代 UI 风格。

总结: Color 提供灵活的颜色定义方式,Gradient + GradientColor 构成了强大的渐变系统。通过将 GradientColor 赋值给 fillColor 或 strokeColor,可以创建出丰富、生动的视觉效果,是现代图形设计中不可或缺的工具。


第7章:图层与分组

在复杂图形应用中,合理组织和管理图形元素至关重要。Layer(图层)和 Group(组)是实现这一目标的核心结构。它们都继承自 Item 类,支持变换、隐藏、锁定等操作,但用途和行为有所不同。

7.1 Layer 类:图层管理

图层(Layer)是项目(Project)中的顶层容器,用于逻辑或视觉上的分层管理,类似于 Photoshop 中的图层。

属性/方法语法用途说明代码示例注意事项
new Layer()new Layer()创建一个新图层,并自动添加到当前项目const layer = new Layer();每个 Project 至少有一个默认图层。
layer.namelayer.name = '背景';获取或设置图层名称layer.name = 'Foreground'; console.log(layer.name);便于识别和脚本操作,不影响渲染。
layer.visiblelayer.visible = false;控制图层是否可见(隐藏/显示)layer.visible = !layer.visible; // 切换可见性设置 false 后,图层内所有子项不可见,不影响图层结构。
layer.lockedlayer.locked = true;锁定图层,防止编辑layer.locked = true; // 无法选择或修改内容常用于保护背景或参考线,锁定后仍可编程修改。
layer.opacitylayer.opacity = 0.5;设置图层整体透明度layer.opacity = 0.8; // 20% 透明0 = 完全透明,1 = 完全不透明,影响所有子项。
project.layersproject.layers获取项目中所有图层的数组const allLayers = project.layers;按创建顺序排列,可用于遍历或重新排序。
project.activeLayerproject.activeLayer = layer;设置当前激活图层(新对象将添加到此层)project.activeLayer = layer; const path = new Path.Circle(...);关键属性:控制新图形的归属,默认为最后一个图层。
layer.remove()layer.remove();移除图层及其所有子项if (layer.children.length === 0) { layer.remove(); }慎用,会删除所有内容,无法移除最后一个图层。

图层使用场景:

  • 分层设计:背景、前景、UI 控件分别放在不同图层。
  • 批量操作:通过图层统一控制可见性、透明度或导出。
  • 性能优化:将静态内容放在独立图层,减少重绘区域。

7.2 Group 类:对象分组与变换

组(Group)用于将多个图形项(Item)组合在一起,形成一个逻辑单元,便于统一操作。

属性/方法语法用途说明代码示例注意事项
new Group(items)new Group([item1, item2])new Group()创建一个组,并可选地添加初始项const group = new Group([circle, rect]);可创建空组,后续添加。
group.childrengroup.children获取组内所有子项的数组const items = group.children;顺序为添加顺序,可读取但不建议直接修改。
group.applyMatrixgroup.applyMatrix = false;是否将变换”压平”到子项group.rotate(45); group.applyMatrix = true;true:变换应用到子项,组自身无变换;false(默认):变换作用于组容器。
group.clippedgroup.clipped = true;设置组是否启用剪切模式const group = new Group([clipPath, content]); group.clipped = true;第一个子项作为剪切路径,常用于实现遮罩效果。
group.pivotgroup.pivot = new Point(x, y);设置组的变换支点(旋转、缩放中心)group.pivot = group.bounds.center; group.rotate(30);默认为 bounds.topLeft,影响 rotate(), scale() 等操作。
group.bounds / strokeBoundsgroup.bounds / group.strokeBounds获取组的包围盒const bbox = group.bounds; console.log(bbox.center);用于布局、碰撞检测,自动包含所有子项。

组的变换特性:

  • 对 Group 应用 translate()rotate()scale() 等变换时,所有子项相对位置不变,整体移动。
  • 变换以 pivot 为支点进行。
  • 适合制作图标、按钮、可复用组件等。

7.3 图层与组的层级操作:addChild()、insertChild()、removeChild()

Layer 和 Group 都是容器(ContainerItem),支持统一的层级管理方法。

方法语法用途说明代码示例注意事项
addChild(item)container.addChild(item);将项添加为最后一个子项(最上层)layer.addChild(circle);子项自动从原父容器移除(若存在)。
insertChild(index, item)container.insertChild(index, item);在指定索引处插入子项layer.insertChild(0, background);index 从 0 开始,超出范围会抛出错误。
insertAbove(item, itemToInsert)itemToInsert.insertAbove(item);将项插入到另一个项的上方circle.insertAbove(rect);不依赖容器,需在同一父容器。
insertBelow(item, itemToInsert)itemToInsert.insertBelow(item);将项插入到另一个项的下方text.insertBelow(icon);常用于调整绘制顺序。
removeChild(index)container.removeChild(index);移除指定索引的子项group.removeChild(0);移除后对象仍存在,但不再渲染。
removeChild(item)container.removeChild(item);移除指定的子项对象layer.removeChild(path);直接传入对象引用。
removeChildren(from, to)container.removeChildren(0, 2);移除指定范围内的所有子项group.removeChildren(); group.removeChildren(1, 3);to 可选,默认到末尾。
clear()container.clear();清空容器内所有子项layer.clear();高效批量删除,不删除容器本身。

层级顺序与渲染:

  • 子项的数组顺序决定绘制顺序:索引小的先绘制(在底层),索引大的后绘制(在上层)。
  • 使用 addChild() 添加的项总是在最上层。
  • 使用 insertChild(0, item) 可将其置于最底层。

总结:

  • Layer 用于宏观分层,控制可见性、活动状态和项目结构。
  • Group 用于微观组织,实现对象组合、统一变换和剪切效果。
  • 两者都支持 addChild()insertChild()removeChild() 等方法,便于动态构建和管理复杂场景。
  • 合理使用图层与分组,能极大提升代码可维护性和运行效率。

第8章:用户交互与事件处理

在图形应用中,用户交互是实现动态、响应式体验的核心。本章介绍如何通过事件系统捕获鼠标、键盘输入,并利用帧更新机制实现动画与定时控制。

8.1 鼠标事件:onMouseDown、onMouseDrag、onMouseMove、onMouseUp

事件触发时机事件对象属性代码示例注意事项
onMouseDown按下鼠标按钮时触发(通常为左键)event.point:按下位置
event.event:原生 DOM 事件
event.modifiers:修饰键状态
view.onMouseDown = function(event) { console.log('按下:', event.point); };每次按下仅触发一次。
onMouseDrag按住鼠标并移动时触发event.point:当前光标位置
event.delta:移动偏移量
event.downPoint:按下时位置
view.onMouseDrag = function(event) { path.add(event.point); };delta 适合拖拽速度控制。
onMouseMove鼠标移动时触发(无论是否按下)event.point:当前位置
event.modifiers:修饰键状态
view.onMouseMove = function(event) { toolTip.position = event.point; };触发频率高,避免复杂计算。
onMouseUp释放鼠标按钮时触发event.point:释放位置
event.downPoint:按下位置
event.delta:总位移
view.onMouseUp = function(event) { console.log('拖拽距离:', event.delta.length); };标志一次拖拽操作结束。

鼠标事件使用技巧:

  • 结合 event.modifiers.shift 判断是否按住 Shift 键,实现正交绘制。
  • 使用 event.delta 实现惯性滚动或速度感应动画。
  • onMouseDrag 依赖 onMouseDown 触发,必须先定义 onMouseDown

8.2 键盘事件:onKeyDown、onKeyUp

事件触发时机事件对象属性代码示例注意事项
onKeyDown按下按键时触发(支持自动重复)event.key:按键名称
event.character:输入字符
event.event:原生 KeyboardEvent
view.onKeyDown = function(event) { if (event.key === 'space') { player.jump(); } };event.key 是物理键,character 是字符。
onKeyUp释放按键时触发event.key:释放的按键view.onKeyUp = function(event) { if (event.key === 'shift') { tool.setPrecision(false); } };通常不重复触发,用于结束状态。

常用 event.key 值:

  • 字母:‘a’, ‘b’, …, ‘z’
  • 数字:‘0’ ~ ‘9’
  • 功能键:‘enter’, ‘tab’, ‘space’, ‘escape’, ‘backspace’, ‘delete’
  • 方向键:‘up’, ‘down’, ‘left’, ‘right’
  • 修饰键:‘shift’, ‘ctrl’, ‘alt’, ‘meta’(Mac Command 键)

注意事项:

  • 键盘事件需要视图(view)获得焦点才能触发。
  • 避免使用可能被浏览器拦截的快捷键(如 Ctrl+T, Ctrl+W)。
  • 对于文本输入,建议使用 <input> 元素结合图形界面。

8.3 帧更新事件:onFrame

onFrame 是实现平滑动画的核心机制,它在每次浏览器重绘前执行,频率通常为 60 FPS。

属性/方法语法用途说明代码示例注意事项
view.onFrameview.onFrame = function(event) { ... }定义每帧执行的逻辑view.onFrame = function(event) { rotor.rotate(1); };自动启动,无需手动调用。
event.countevent.count当前是第几帧(从1开始)if (event.count % 60 === 0) { ... }适合定时逻辑(如每秒更新)。
event.deltaevent.delta上一帧到当前帧的时间间隔(秒)x += speed * event.delta;关键属性:实现帧率无关动画。

onFrame 使用场景: 连续动画、游戏主循环、数据可视化动态更新、粒子系统。

性能提示: 避免在 onFrame 中执行昂贵操作;使用 event.delta 实现帧率无关动画。

8.4 定时器与动画控制

方法语法用途说明代码示例注意事项
使用 onFrame 模拟定时器累计 event.delta实现固定间隔的定时任务timer += event.delta; if (timer >= interval) { ... }精度受帧率影响。
一次性延迟执行setTimeout延迟执行某段代码setTimeout(() => showNotification(), 3000);不依赖 view。
周期性任务onFrame + 计时器变量更流畅的周期性任务while (elapsed >= period) { blink(); elapsed -= period; }避免 setInterval 的累积误差。
暂停/恢复动画view.onFrame = null / 重新赋值动态控制动画开关view.onFrame = null; // 停止设为 null 可暂停。
requestAnimationFrame手动使用底层控制requestAnimationFrame(animate);view.onFrame 已封装此机制。

动画控制最佳实践: 使用 event.delta 确保帧率无关;避免 setInterval;合理暂停节省资源;状态管理便于控制。

总结:

  • 鼠标事件:实现点击、拖拽、悬停等交互。
  • 键盘事件:处理快捷键和输入控制。
  • onFrame:实现流畅、高性能的动画循环,event.delta 是关键。
  • 定时控制:优先使用 onFrame 结合时间累计,避免 setInterval

第9章:文本与图像

在图形应用中,文本和图像是构成视觉内容的关键元素。

9.1 文本项:PointText 与 TextItem

类型语法与构造方式用途说明代码示例注意事项
PointTextnew PointText(point)基于一个锚点的单行文本const text = new PointText(new Point(50, 50)); text.content = 'Hello World';最简单文本类型,默认左对齐,不支持自动换行。
TextItem(基类)new TextItem()所有文本类型的基类text.content = 'Some text'; text.fontSize = 14;PointText 继承自 TextItem。

PointText 是最常用的文本类,适用于大多数静态或动态文本显示场景。

9.2 字体、字号与文本样式

属性语法用途说明代码示例注意事项
contenttext.content = '新文本';设置或获取文本内容text.content = 'Welcome to Paper.js';支持 Unicode 字符(包括中文)。
fontSizetext.fontSize = 24;设置字体大小(单位:点 pt)text.fontSize = 18;默认值通常为 10 或 12pt。
fonttext.font = 'Arial';设置字体族或完整 CSS 风格字体text.font = 'bold italic 20pt "Times New Roman"';必须确保字体已加载。
fontWeighttext.fontWeight = 'bold';设置字重text.fontWeight = 'normal';可选值:normal, bold, 100~900 等。
fontStyletext.fontStyle = 'italic';设置字体风格text.fontStyle = 'italic';用于斜体文本。
fillColortext.fillColor = 'red';设置文本填充颜色text.fillColor = '#333';支持所有 Color 格式。
strokeColor / strokeWidthtext.strokeColor = 'black'; text.strokeWidth = 0.5;设置描边颜色和宽度text.strokeColor = 'white'; text.strokeWidth = 1;实现”描边文字”效果。
justificationtext.justification = 'center';设置文本对齐方式text.justification = 'right';’left’, ‘center’, ‘right’。
position / pointtext.position = new Point(x, y);设置文本位置text.position = view.center;position 是中心点,point 是基线起点。

字体加载提示:

  • 使用 Web 字体时,需通过 <link>@font-face 加载,并确保字体就绪后再渲染。
  • 可使用 document.fonts.load('16px FontName') 确保字体可用。

9.3 图像导入:Raster 类

方法语法用途说明代码示例注意事项
new Raster(source)new Raster('image.png');从 URL 或 DOM 元素创建光栅图像const raster = new Raster('logo.png');支持 .png, .jpg, .gif, .svg,异步加载。
raster.onLoadraster.onLoad = function() { ... }图像加载完成后的回调img.onLoad = function() { console.log(this.size); };必须使用:确保尺寸可用后再操作。
raster.size / width / heightconsole.log(raster.size);获取图像原始尺寸img.onLoad = function() { console.log(this.width, this.height); };单位为像素,只读。

图像来源支持:

  • 字符串 URL:'path/to/image.jpg'
  • DOM 图像元素:<img id="myImg">new Raster('myImg')
  • Data URL:new Raster('data:image/png;base64,...')

9.4 图像操作:位置、缩放、蒙版与像素访问

操作语法用途说明代码示例注意事项
位置设置raster.position = new Point(x, y);将图像中心定位到指定点raster.position = view.center;position 是图像中心。
缩放raster.scale(sx, [sy]); raster.setSize(w, h);调整图像大小raster.scale(2); raster.setSize(100, 80);scale() 基于当前大小。
旋转raster.rotate(45);旋转图像raster.rotate(30);绕中心点旋转。
透明度raster.opacity = 0.5;设置图像整体透明度raster.opacity = 0.7;0 = 完全透明,1 = 完全不透明。
作为蒙版group.clipped = true;使用图像作为剪切蒙版const group = new Group([mask, content]); group.clipped = true;第一个子项为 raster。
像素访问raster.getImageData(rectangle)获取指定区域的像素数据const data = img.getImageData(); console.log(data.data); // RGBA 数组受同源策略限制。
颜色替换/滤镜结合 getImageData 和 setImageData实现简单图像处理for (let i = 0; i < pixels.length; i += 4) { pixels[i] = 255 - pixels[i]; }可实现灰度、反色、模糊等效果。

性能提示: 频繁像素操作可能导致性能问题;大图像考虑降采样或分块处理。

总结:

  • 文本:使用 PointText 创建文本,通过 font、fontSize、fillColor 等属性控制样式。
  • 图像:使用 Raster 导入,注意 onLoad 回调确保加载完成。
  • 操作:支持位置、缩放、旋转、透明度等基本变换。
  • 高级功能:可将图像用作蒙版,或通过 getImageData() 进行像素级操作(受 CORS 限制)。

第10章:高级路径与贝塞尔曲线

贝塞尔曲线是矢量图形的核心,它通过控制点定义平滑的曲线路径。本章深入探讨 Segment、Curve 的结构,控制点的操作,以及复杂路径绘制与动画。

10.1 Segment 与 Curve 详解

在 Paper.js 中,Path(路径)由多个 Segment(线段)组成,而每两个相邻的 Segment 定义一条 Curve(曲线)。

概念语法与结构用途说明代码示例注意事项
Segment(线段)new Segment(point, handleIn, handleOut)路径中的一个点及其两个控制手柄const segment = new Segment(new Point(50, 50), new Point(-10, -20), new Point(10, -20));point:锚点;handleIn:入控制点;handleOut:出控制点;控制点相对于锚点是偏移量。
Segment.pointsegment.point获取或设置锚点位置segment.point = new Point(100, 100);改变锚点会移动整个线段。
Segment.handleInsegment.handleIn入控制点segment.handleIn = new Point(-15, 0);null 或 (0,0) 时为尖角或直线。
Segment.handleOutsegment.handleOut出控制点segment.handleOut = new Point(15, 0);影响下一段曲线。
Curve(曲线)path.getCurve(index)segment1.curve由两个 Segment 定义的一段贝塞尔曲线const curve = path.getCurve(0);一条 Curve 是一个独立的三次贝塞尔曲线段。

路径结构图示:

Path: [Segment0] ---- Curve0 ----> [Segment1] ---- Curve1 ----> [Segment2]
        |               |               |               |
     point,         handleOut     handleIn,        handleOut
    handleIn                       point,
  • 一个 Path 至少有一个 Segment。
  • n 个 Segment 构成 n-1 条 Curve(如果是开放路径)。

10.2 控制点操作:handleIn、handleOut

操作语法用途说明代码示例注意事项
对称控制点segment.smooth()自动调整控制点,使曲线平滑过渡path.smooth();Paper.js 自动计算最佳控制点。
断开控制点segment.handleIn = ...; segment.handleOut = ...;创建尖角或非对称曲线segment.handleIn = new Point(-10, -10); segment.handleOut = new Point(10, 10);若 handleIn ≠ -handleOut,则为尖角。
统一控制点segment.handleIn = -segment.handleOut;手动保持控制点对称segment.handleOut = new Point(20, 0); segment.handleIn = new Point(-20, 0);等价于 segment.smooth()。
归零控制点segment.handleIn = null;移除控制点,转为直线segment.handleIn = null;null 等价于 (0,0)。
获取控制点属性segment.handleOut.length / .angle查询控制点的向量属性console.log(segment.handleOut.length, segment.handleOut.angle);用于动态调整或动画。

控制点操作技巧:

  • 使用 path.selected = true; 可视化控制点(调试用)。
  • path.smooth({ type: 'catmull-rom' }) 可使用不同平滑算法。

10.3 贝塞尔曲线绘制与编辑

方法语法用途说明代码示例注意事项
手动创建贝塞尔路径path.add(segment)逐步添加带控制点的线段path.add(new Segment([20, 100], null, [30, -30])); path.add(new Segment([100, 20], [-30, 30], [30, -30])); path.add(new Segment([180, 100], [-30, 30], null));首段 handleIn 通常为 null,末段 handleOut 通常为 null。
cubicCurveTo()path.cubicCurveTo(handle1, handle2, to)添加三次贝塞尔曲线段path.cubicCurveTo([50, 70], [70, 70], [100, 100]);类似 SVG 的 C 指令。
编辑现有路径path.segments[index].handleOut = ...动态修改路径形状view.onFrame = function() { segment.handleOut.y = 20 * Math.sin(Date.now() / 500); };适合交互式编辑器。
闭合路径path.closed = true;将开放路径闭合path.closed = true;闭合后首尾也形成 Curve。

贝塞尔曲线设计建议:

  • 控制点长度通常为相邻锚点距离的 1/3 左右,可获得自然曲线。
  • 使用对称控制点(handleIn = -handleOut)保持平滑。
  • 利用 path.firstSegmentpath.lastSegment 快速访问首尾。

10.4 路径动画:逐段绘制动画实现

模拟”手绘”效果,逐步显示路径。

步骤 1:创建路径(预先定义完整路径)

const path = new Path();
path.add([20, 100]);
path.cubicCurveTo([50, 70], [70, 70], [100, 100]);
path.cubicCurveTo([130, 130], [150, 130], [180, 100]);
path.strokeColor = 'black';
path.strokeWidth = 2;
path.dashArray = [path.length, path.length]; // 初始隐藏

使用 dashArray 技巧:[可见长度, 间隔长度]

步骤 2:动画逻辑(在 onFrame 中更新 dashArray)

let progress = 0;
const drawSpeed = path.length / 60; // 60帧画完
view.onFrame = function(event) {
  if (progress < path.length) {
    progress += drawSpeed * event.delta;
    path.dashArray = [progress, path.length - progress];
  } else {
    view.onFrame = null; // 动画结束
  }
};

progress:当前已绘制长度;dashArray[0] 增加,dashArray[1] 减少;使用 event.delta 实现帧率无关动画。

步骤 3:可选——添加箭头或光标

const cursor = new Path.Circle({
  center: path.firstSegment.point,
  radius: 3,
  fillColor: 'red'
});
view.onFrame = function(event) {
  if (progress < path.length) {
    progress += drawSpeed * event.delta;
    path.dashArray = [progress, path.length - progress];
    const location = path.getLocationAt(progress);
    cursor.position = location.point;
  }
};

其他路径动画技术:

  • 虚线动画:固定 dashArray,通过 path.dashOffset 实现流动效果。
  • 分段动画:遍历 path.curves,逐条绘制 Curve。
  • 变形动画:插值两个路径的 segments,实现形状过渡(morphing)。

总结:

  • Segment 是路径的基本单元,包含锚点和两个控制手柄。
  • Curve 由两个 Segment 定义,代表一段三次贝塞尔曲线。
  • 控制点 (handleIn/handleOut) 决定曲线的形状,可通过 smooth() 自动优化。
  • 绘制贝塞尔可使用 cubicCurveTo() 或直接操作 Segment。
  • 路径动画常用 dashArray + onFrame 实现逐段绘制效果。

掌握贝塞尔曲线的底层结构,是创建复杂、流畅矢量动画的基础。


第11章:变换与动画

在图形应用中,变换(Transformation)和动画(Animation)是赋予视觉元素动态行为的核心技术。本章深入讲解对象的平移、旋转、缩放操作,变换矩阵的应用,以及如何实现流畅的动画效果。

11.1 平移、旋转、缩放:translate()、rotate()、scale()

这些方法用于对 Item 及其子类(如 Path、Group、Raster 等)进行基本几何变换。

方法语法用途说明代码示例注意事项
translate(delta)item.translate(new Point(dx, dy)) / item.translate(dx, dy)沿 x 和 y 方向移动对象const circle = new Path.Circle([50, 50], 20); circle.translate(100, 50); // 移动到 [150, 100]delta 是偏移量,可为 Point 或两个数字;不改变对象的 position 属性。
rotate(angle, [center])item.rotate(angle) / item.rotate(angle, center)绕指定点旋转对象square.rotate(45); // 绕自身中心旋转45度 logo.rotate(30, view.center); // 绕视图中心旋转angle 单位为度(非弧度);center 默认为对象的 bounds.center。
scale(scale, [center])item.scale(sx, [sy]) / item.scale(scale, center)按比例缩放对象icon.scale(2); // 放大2倍 text.scale(1.5, 0.8); // x放大1.5倍,y缩小20% group.scale(0.5, pivotPoint);若只传一个参数,则 sx = sy;center 默认为中心点;负值可实现镜像翻转(如 scale(-1, 1) 水平翻转)。

变换的叠加性:

  • 多次变换会累积作用于对象。
  • 变换顺序很重要:translate → rotate 与 rotate → translate 效果不同。
  • 所有变换都存储在对象的 matrix 属性中。

11.2 变换矩阵应用:applyMatrix

变换矩阵(Matrix)是所有几何变换的底层表示。applyMatrix 控制是否将当前矩阵”压平”到路径数据上。

概念语法用途说明代码示例注意事项
item.matrixconsole.log(item.matrix);获取对象的当前变换矩阵rect.rotate(30); console.log(rect.matrix);包含平移、旋转、缩放、剪切等信息;6个值:a, b, c, d, tx, ty(仿射变换)。
item.applyMatrixitem.applyMatrix = true;是否将变换应用到路径数据并重置矩阵path.rotate(45); path.scale(2); path.applyMatrix = true;true:变换永久作用于路径点坐标,matrix 变为恒等矩阵;false(默认):变换仅作用于渲染。
何时使用 applyMatrix = true固化变换,优化性能或导出for (let item of group.children) { item.rotate(randomAngle()); } group.applyMatrix = true;适合静态内容;固化后无法再通过 rotate() 等方法撤销变换。
手动设置矩阵item.matrix = new Matrix();直接赋值变换矩阵const matrix = new Matrix(); matrix.rotate(45, view.center); matrix.translate(100, 0); item.matrix = matrix;高级用法,精确控制变换序列;可组合多个变换。

applyMatrix 的典型场景:

  • 性能优化:对于不再变化的复杂组,固化变换可减少渲染计算。
  • 导出准备:确保 SVG 或其他格式输出的是实际坐标。
  • 碰撞检测:获取变换后的精确几何数据。

11.3 基于 onFrame 的动画实现

view.onFrame 是实现流畅动画的核心机制,它在每次屏幕刷新前执行。

步骤方法代码示例说明
1. 定义动画对象创建需要动画的图形const ball = new Path.Circle({ center: [50, 50], radius: 10, fillColor: 'blue' }); let speed = 200; // 像素/秒初始化位置、速度等状态变量。
2. 设置 onFrame在每帧更新对象状态view.onFrame = function(event) { ball.position.x += speed * event.delta; if (ball.bounds.right > view.bounds.width) { speed = -speed; } };使用 event.delta 实现帧率无关动画。
3. 动画类型扩展实现旋转、缩放、颜色等动画hand.rotate(6 * event.delta); // 每秒6度 clock.scale(1 + 0.1 * Math.sin(Date.now() / 200)); element.fillColor.hue += 1 * event.delta;同样基于 event.delta;可组合多种变换。
4. 停止动画条件满足时清除 onFrameif (ball.position.x > view.bounds.width + 100) { view.onFrame = null; }设为 null 可暂停全局帧循环。

动画最佳实践:

  • 始终使用 event.delta,避免 setTimeout 或固定步长。
  • 将动画状态(位置、角度、透明度等)存储在变量中,便于控制。
  • 复杂动画可封装为独立函数或类。

11.4 缓动函数与动画库集成思路

缓动函数(Easing Functions)用于创建更自然的动画效果(如渐入渐出、弹跳等),而非线性运动。

概念说明常见缓动函数代码示例
什么是缓动函数输入时间比例 t(01),输出进度 f(t)(01),控制动画速度曲线linear(t) = teaseInQuad(t) = t²easeOutQuad(t) = t*(2-t)、easeInOutCubic、elastic、bounce 等function easeOutQuad(t) { return t * (2 - t); }
实现缓动动画结合总时长和缓动函数见下方代码示例
动画库集成思路引入第三方库(如 GSAP、anime.js)管理复杂动画见下方代码示例

缓动动画实现:

let startTime = Date.now();
const duration = 2000; // 2秒
const startPos = ball.position.clone();
const endPos = view.center;

view.onFrame = function(event) {
  const elapsed = Date.now() - startTime;
  const t = Math.min(elapsed / duration, 1); // 归一化时间
  const easedT = easeOutQuad(t); // 应用缓动
  ball.position = startPos + (endPos - startPos) * easedT;
  if (t >= 1) view.onFrame = null; // 结束
};

t:归一化时间(0→1);easedT:缓动后的时间(决定运动快慢)

GSAP 集成示例:

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
// 使用 GSAP 动画 Paper.js 对象
gsap.to(ball, {
  x: '+=100',
  rotation: 360,
  duration: 1,
  ease: 'elastic.out(1, 0.3)'
});

优势:丰富的缓动函数、时间轴、链式调用;注意:GSAP 直接修改 item.matrix。

推荐缓动库: GSAP (GreenSock) 功能强大、anime.js 轻量级、tween.js。

自定义缓动工具函数:

function animate(from, to, duration, easing, onUpdate, onComplete) {
  let start = Date.now();
  view.onFrame = function(event) {
    let t = Math.min((Date.now() - start) / duration, 1);
    let eased = easing(t);
    let value = from + (to - from) * eased;
    onUpdate(value);
    if (t >= 1) {
      if (onComplete) onComplete();
      view.onFrame = null;
    }
  };
}

// 使用
animate(0, 360, 1000, easeOutQuad,
  angle => ball.rotate(angle),
  () => console.log('完成')
);

总结:

  • 基本变换:translate()rotate()scale() 是构建动态效果的基础。
  • 变换矩阵:matrix 存储所有变换,applyMatrix = true 可固化变换提升性能。
  • 动画核心:view.onFrame + event.delta 实现流畅、帧率无关的动画。
  • 高级动画:引入缓动函数使运动更自然,可集成 GSAP 等专业动画库管理复杂序列。

掌握这些技术,你将能够创建出专业级的交互式矢量动画。


第12章:工具与自定义绘制

在交互式图形应用中,用户通常需要通过不同的”工具”来创建或编辑内容,例如画笔、矩形、选择工具等。Paper.js 提供了 Tool 类,允许开发者创建自定义的交互式绘图工具。

12.1 Tool 类与工具注册

Tool 是 Paper.js 中处理用户输入的核心类,它封装了鼠标和键盘事件的监听与响应机制。

概念语法与说明代码示例注意事项
创建新工具const tool = new Tool();const drawTool = new Tool();每个 Tool 实例独立管理其事件回调。
注册工具事件为 Tool 实例绑定事件处理器drawTool.onMouseDown = function(event) { console.log('按下:', event.point); };支持 onMouseDown, onMouseDrag, onMouseMove, onMouseUp, onKeyDown, onKeyUp。
激活工具创建即自动激活(当前文档只能有一个激活工具)const selectTool = new Tool(); // 现在 selectTool 是当前工具同一时间只有一个 Tool 处于激活状态,新创建的 Tool 会自动取代之前的工具。
访问当前工具paper.toolview.toolconsole.log(paper.tool); // 获取当前激活的工具可用于动态切换或查询当前工具状态。

工具生命周期:

  • 创建 Tool 实例。
  • 绑定事件处理器(如 onMouseDown)。
  • 自动激活,开始监听事件。
  • 用户交互触发事件,执行对应逻辑。
  • 创建新 Tool 实例时,旧工具被替换。

12.2 自定义绘图工具:自由笔、矩形工具、橡皮擦等

1. 自由笔工具(Freehand Drawing Tool)

模拟手绘线条,通过 onMouseDown 开始路径,onMouseDrag 添加点。

const freehandTool = new Tool();
let path;

freehandTool.onMouseDown = function(event) {
  // 开始新路径
  path = new Path();
  path.add(event.point);
  path.strokeColor = 'black';
  path.strokeWidth = 2;
};

freehandTool.onMouseDrag = function(event) {
  // 拖拽时添加点
  path.add(event.point);
};

freehandTool.onMouseUp = function(event) {
  // 可选:平滑路径
  path.smooth();
  console.log('绘制完成');
};

技巧:使用 path.add(event.point) 而非 cubicCurveTo 可简化实现,适合快速草图。

2. 矩形绘制工具(Rectangle Tool)

点击并拖拽绘制矩形,类似设计软件中的矩形工具。

const rectTool = new Tool();
let rectangle, startPoint;

rectTool.onMouseDown = function(event) {
  startPoint = event.point;
  rectangle = new Path.Rectangle({
    point: startPoint,
    size: [0, 0],
    strokeColor: 'blue',
    fillColor: null
  });
};

rectTool.onMouseDrag = function(event) {
  // 计算对角矩形
  const rect = new Rectangle(startPoint, event.point);
  rectangle.rectangle = rect; // 更新矩形形状
};

rectTool.onMouseUp = function(event) {
  if (rectangle.area < 1) {
    rectangle.remove(); // 太小则删除
  } else {
    rectangle.fillColor = '#e0e0ff'; // 添加填充
  }
};

关键:使用 Path.Rectangle 和 rectangle 属性可方便地更新矩形几何。

3. 橡皮擦工具(Eraser Tool)

通过碰撞检测删除小路径或擦除部分路径。

const eraserTool = new Tool();

// 方法1:删除小对象
eraserTool.onMouseDown = function(event) {
  const hits = project.hitTestAll(event.point, {
    tolerance: 10,
    type: 'path'
  });
  for (let hit of hits) {
    if (hit.item.bounds.area < 200) { // 小对象删除
      hit.item.remove();
    }
  }
};

限制:Paper.js 不直接支持像素级擦除。常见策略:删除整个小对象、将大路径分割(divide())删除部分段落、使用蒙版(clipMask)模拟擦除效果。

12.3 工具事件的生命周期

Tool 的事件具有明确的生命周期,按用户操作顺序触发:

事件触发时机典型用途是否可重复触发示例场景
onMouseDown鼠标按钮按下瞬间初始化操作、记录起点、创建初始对象一次(每次按下)开始绘制、选择对象、记录拖拽起点
onMouseDrag按住鼠标移动时连续添加点、动态更新形状、实时反馈多次(持续移动)自由绘制、调整大小、移动对象
onMouseMove鼠标移动(无论是否按下)悬停反馈、光标样式变化、预览效果高频触发显示提示、高亮目标、预览绘制位置
onMouseUp释放鼠标按钮时完成操作、清理临时对象、应用最终状态一次(每次释放)结束绘制、提交修改、释放资源
onKeyDown / onKeyUp键盘按键/释放快捷键控制、切换模式(如 Shift 正交)、删除(Delete 键)可重复(长按)按住 Shift 绘制正方形,Esc 取消操作

事件协同工作示例(正方形工具):

const squareTool = new Tool();
let rect, start;

squareTool.onMouseDown = function(event) {
  start = event.point;
  rect = new Path.Rectangle({ point: start, size: [0,0], strokeColor: 'red' });
};

squareTool.onMouseDrag = function(event) {
  let size = event.point.subtract(start);
  // 按住 Shift 绘制正方形
  if (event.modifiers.shift) {
    const max = Math.max(Math.abs(size.x), Math.abs(size.y));
    size = size.normalize(max); // 保持方向,长度为最大值
  }
  rect.rectangle = new Rectangle(start, size);
};

squareTool.onMouseUp = function() {
  if (rect.area < 1) rect.remove();
};

总结:

  • Tool 类是构建交互式绘图功能的基础,每个工具独立管理其事件。
  • 自定义工具如自由笔、矩形、橡皮擦,可通过组合 onMouseDown、onMouseDrag、onMouseUp 实现。
  • 事件生命周期清晰:down → drag → up 构成完整操作,move 用于实时反馈,key 用于增强控制。

通过 Tool,你可以构建出媲美专业设计软件的丰富交互体验。


第13章:序列化与数据交互

在图形应用中,将绘制内容保存、分享或与其他系统交互是常见需求。Paper.js 提供了强大的序列化功能,支持导出为 SVG、导入 SVG 内容以及使用 JSON 格式进行数据交换。

13.1 路径导出为 SVG 字符串:exportSVG()

exportSVG() 方法可将 Project、Layer、Group 或单个 Item(如 Path)导出为标准的 SVG(Scalable Vector Graphics)字符串。

方法语法用途说明代码示例注意事项
item.exportSVG(options)item.exportSVG({ asString: true })导出为 SVG 字符串const svgString = path.exportSVG({ asString: true }); console.log(svgString); // 输出 SVG 字符串asString: true 返回字符串;默认返回 SVGElement(DOM 节点)。
支持的选项 (options){ asString, precision, matchShapes }控制导出格式const svg = project.exportSVG({ asString: true, precision: 2, matchShapes: false });precision:坐标精度(默认 5);matchShapes:若为 true,Path.Circle 会导出为 <circle> 而非 <path>
导出整个项目project.exportSVG()导出所有图层和对象const fullSVG = project.exportSVG({ asString: true });包含所有 Layer 和 Item,保持层级结构(Group、Layer)。

SVG 导出用途: 保存用户绘制内容、与设计软件(如 Adobe Illustrator)交换数据、在网页中直接嵌入或通过 <img> 显示。

13.2 从 SVG 导入内容:importSVG()

importSVG() 方法允许从 SVG 字符串、URL 或 DOM 元素中导入内容,并转换为 Paper.js 的 Item 对象。

方法语法用途说明代码示例注意事项
project.importSVG(source)project.importSVG(svgString)project.importSVG(element)从源导入 SVG 并返回根 Itemconst imported = project.importSVG(svgContent); console.log(imported); // Group 或 Path 实例source 可以是字符串、URL 或 SVGElement;返回导入的根对象(通常是 Group)。
导入外部 SVG 文件project.importSVG('icon.svg')从文件加载fetch('logo.svg').then(res => res.text()).then(svg => { const item = project.importSVG(svg); item.position = view.center; });需处理异步加载;受同源策略(CORS)限制。
导入 DOM 中的 SVGproject.importSVG(document.getElementById('mySvg'))从页面内嵌 SVG 导入const imported = project.importSVG('mySvg'); imported.scale(2);适合复用预定义图标。

导入后的操作: 返回的对象可像普通 Item 一样进行 translate()rotate()scale() 等变换;可访问其子项:imported.childrenimported.layers

限制: 复杂滤镜或动画可能不完全支持;位图图像(<image>)会转换为 Raster。

13.3 JSON 序列化:toJSON() 与 project.importJSON()

JSON 是轻量级的数据交换格式,适合在应用间传递或存储 Paper.js 对象。

方法语法用途说明代码示例注意事项
item.toJSON()item.toJSON()将 Item 及其子项序列化为 JSON 对象const json = circle.toJSON();自动处理循环引用;包含所有可序列化属性。
project.toJSON()project.toJSON()序列化整个项目(所有图层)const fullJSON = project.toJSON(); const jsonString = JSON.stringify(fullJSON);最常用方式保存完整画布状态。
project.importJSON(json)project.importJSON(jsonString)project.importJSON(jsonObject)从 JSON 数据重建 Paper.js 对象const restored = project.importJSON(jsonString); restored.position = view.center;返回导入的根 Item;完全恢复对象结构和样式。
选择性序列化手动构造 JSON只保存必要数据const data = { paths: project.activeLayer.children.map(path => ({ type: path.type, bounds: path.bounds.toJSON() })) };减少数据量;用于自定义同步协议。

JSON 序列化优势:

  • 轻量高效:比 SVG 字符串更紧凑。
  • 易于存储:可保存到 localStorage、数据库或通过 WebSocket 传输。
  • 完整还原:importJSON() 能精确重建对象,包括自定义属性(如果可序列化)。

完整工作流示例(保存与加载):

// 1. 保存当前项目
function saveProject() {
  const json = project.toJSON();
  localStorage.setItem('paperProject', JSON.stringify(json));
  console.log('项目已保存');
}

// 2. 加载项目
function loadProject() {
  const saved = localStorage.getItem('paperProject');
  if (saved) {
    project.clear(); // 清空当前项目
    project.importJSON(saved);
    console.log('项目已加载');
  }
}

// 绑定按钮
document.getElementById('saveBtn').onclick = saveProject;
document.getElementById('loadBtn').onclick = loadProject;

总结:

  • exportSVG():将图形导出为标准 SVG 格式,适合与设计工具交互或网页嵌入。
  • importSVG():从 SVG 字符串、文件或 DOM 元素导入内容,实现内容复用。
  • toJSON() / importJSON():使用 JSON 格式进行高效的数据序列化与反序列化,适合应用内部状态保存、网络传输或持久化存储。

掌握这些序列化技术,你的图形应用将具备强大的数据交互能力,支持保存、加载、分享和跨平台协作。


第14章:性能优化与最佳实践

在构建复杂的交互式图形应用时,性能至关重要。本章介绍如何通过减少重绘、合理管理对象和内存、优化动画以及使用调试技巧,确保应用在各种设备上流畅运行。

14.1 减少重绘与合理使用 view.update()

view.update() 触发 Paper.js 重新渲染整个场景。频繁调用会导致性能下降。

策略说明代码示例注意事项
避免不必要的 view.update()默认情况下,修改 Item 属性会自动触发更新。手动调用仅在批量操作后需要。view.onFrame = function() { path.rotate(1); // 自动重绘,无需 view.update() };Paper.js 会自动在属性变化后重绘;仅在极少数情况下需手动调用。
批量操作后更新对多个对象进行修改时,先禁用自动更新,操作完成后再手动更新。view.update = false; // 暂停视图更新 for (let i = 0; i < 1000; i++) { const path = new Path.Circle([i*10, 50], 5); path.fillColor = 'blue'; } view.update = true; view.update();view.update = false 暂停自动重绘;操作完成后必须调用 view.update()
减少复杂路径的实时计算避免在 onFrame 中执行 path.smooth()path.intersect() 等耗时操作。const smoothedPath = originalPath.clone(); smoothedPath.smooth(); view.onFrame = function() { animatedItem.rotate(1); };几何操作(布尔运算、平滑)计算量大,应预计算。

重绘优化原则:

  • 信任自动更新:99% 的情况无需手动 view.update()
  • 批量处理:大量创建/修改时,临时关闭更新。
  • 预计算:将耗时操作移到初始化阶段。

14.2 对象复用与内存管理

频繁创建和销毁对象会导致内存压力和垃圾回收卡顿。

策略说明代码示例注意事项
对象池(Object Pooling)复用已创建的对象,避免重复 new 和 remove()。const pool = []; function getPooledCircle() { let circle = pool.pop(); if (!circle) { circle = new Path.Circle([0,0], 5); } return circle; } function returnToPool(circle) { circle.remove(); pool.push(circle); }适合大量相似对象(粒子、图标);显著减少 GC 压力。
及时移除无用对象使用 item.remove()group.removeChildren() 释放内存。if (oldPath) oldPath.remove(); layer.removeChildren(); // 清空图层remove() 会从项目中删除并释放资源;未移除的对象会持续占用内存。
避免闭包内存泄漏确保事件监听器不会意外持有对象引用。tool.onMouseDown = null; // 工具销毁时在工具或组件销毁时清理事件回调。
监控内存使用使用浏览器开发者工具(Chrome DevTools)检查内存占用。打开 Memory 面板,进行堆快照(Heap Snapshot);查找未释放的 Path、Point 等实例。

内存管理最佳实践:

  • 对于高频创建的对象(如粒子系统),必须使用对象池。
  • 使用 project.clear() 快速清空整个项目。
  • 定期检查是否有”幽灵对象”未被移除。

14.3 复杂动画的性能考量

动画是性能瓶颈的常见来源,尤其是在低端设备上。

优化策略说明实现方式注意事项
简化路径几何减少 Path 的 segments 数量。path.simplify(10); // 误差容忍度(像素) path.removeSegment(5);simplify() 可大幅减少点数;误差值越大,路径越简单(可能失真)。
使用 applyMatrix = true固化变换,避免每帧重新计算矩阵。animatedItem.rotate(360); animatedItem.applyMatrix = true;适合不再变化的静态元素;提升后续渲染性能。
分层渲染(Layer)将静态背景和动态前景分离到不同 Layer。const bgLayer = new Layer(); // 静态背景 const fgLayer = new Layer(); // 动画前景Paper.js 会智能优化,但分层有助于组织。
限制动画对象数量避免同时动画成百上千个对象。const maxParticles = 100; if (particles.length > maxParticles) { particles.shift().remove(); }考虑使用 Raster(位图)替代复杂矢量。
使用 Raster 预渲染将复杂矢量组合渲染为位图,再进行动画。const group = new Group([path1, path2, text]); const raster = group.rasterize(); group.remove(); raster.position = view.center;适合不需缩放的复杂图形;损失矢量清晰度。
优化 onFrame 逻辑确保 onFrame 内代码高效。view.onFrame = function(event) { const delta = speed * event.delta; items.forEach(item => { item.position.x += delta; }); };避免 new、DOM 操作、复杂计算。

动画性能检查清单:

  • 是否使用了对象池?
  • 路径是否过度复杂?能否 simplify()?
  • 是否有不必要的 onFrame 计算?
  • 静态元素是否已 applyMatrix = true?
  • 是否可以预渲染为 Raster?

14.4 调试技巧与常见问题排查

问题类型现象调试方法解决方案
对象未显示图形不出现检查 fillColor/strokeColor 是否设置;使用 item.selected = true 查看位置;检查 opacity 是否为 0;确认 view.viewSize 范围item.selected = true; console.log(item.bounds);
动画卡顿帧率低,不流畅打开浏览器 Performance 面板;录制运行过程,查看 CPU 占用;检查 onFrame 是否耗时过长应用性能优化策略;减少对象数量或复杂度
内存持续增长应用变慢,内存占用上升使用 Memory 面板进行堆快照;比较操作前后的对象数量;查找未 remove() 的 Path 实例实现对象池;确保及时 remove()
SVG 导入失败importSVG 无反应或报错检查 SVG 语法是否正确;确认跨域问题(CORS);查看浏览器控制台错误使用本地文件或配置 CORS;简化 SVG 结构测试
事件未触发onMouseDown 等无响应确认 Tool 已正确创建;检查是否有其他 DOM 元素遮挡 Canvas;查看是否被 preventDefault() 阻止确保 <canvas> 可点击;避免其他事件监听器干扰

调试工具推荐:

  • Chrome DevTools:Performance、Memory、Console 面板。
  • Paper.js 调试模式:设置 paper.settings.debug = true 可能输出额外信息。
  • console.log():输出 item.boundsitem.matrixproject.activeLayer.children.length 等关键状态。

总结:

  • 减少重绘:信任自动更新,仅在批量操作后手动 view.update()
  • 内存管理:使用对象池复用对象,及时 remove() 释放资源。
  • 动画优化:简化几何、固化变换、分层、预渲染为 Raster。
  • 调试:善用浏览器工具,检查显示、性能、内存和事件问题。

遵循这些最佳实践,你的 Paper.js 应用将更加高效、稳定,为用户提供流畅的交互体验。


第15章:综合项目实战

本章通过四个完整的项目,将前14章的知识融会贯通,从基础绘图到数据可视化、交互动画,再到游戏开发,全面提升你的 Paper.js 实战能力。

15.1 简易绘图板开发

目标: 创建一个支持自由绘制、选择颜色、调整画笔粗细和清除画布的绘图工具。

核心功能与实现

功能技术点代码实现
自由绘制Tool + onMouseDown/onMouseDrag见下方代码
颜色选择HTML <input type="color"> + 同步见下方代码
画笔粗细HTML <input type="range">见下方代码
清除画布project.clear()document.getElementById('clearBtn').onclick = function() { project.clear(); };
切换工具多个 Tool 实例管理见下方代码

自由绘制:

const drawTool = new Tool();
let path;

drawTool.onMouseDown = function(event) {
  path = new Path();
  path.add(event.point);
  path.strokeColor = currentColor; // 来自颜色选择器
  path.strokeWidth = strokeWidth;   // 来自滑块
};

drawTool.onMouseDrag = function(event) {
  path.add(event.point);
};

颜色选择:

<input type="color" id="colorPicker" value="#000000">
const colorPicker = document.getElementById('colorPicker');
let currentColor = colorPicker.value;
colorPicker.onchange = function() {
  currentColor = this.value;
};

画笔粗细:

<input type="range" id="strokeWidth" min="1" max="20" value="2">
const widthSlider = document.getElementById('strokeWidth');
let strokeWidth = parseInt(widthSlider.value);
widthSlider.oninput = function() {
  strokeWidth = parseInt(this.value);
};

清除画布:

document.getElementById('clearBtn').onclick = function() {
  project.clear();
};

切换工具:

// 橡皮擦工具(简单版)
const eraserTool = new Tool();
eraserTool.onMouseDown = function(event) {
  const hits = project.hitTestAll(event.point, { tolerance: 10 });
  hits.forEach(hit => hit.item.remove());
};

// 切换按钮
document.getElementById('drawMode').onclick = () => new Tool(); // 激活 drawTool
document.getElementById('eraseMode').onclick = () => eraserTool;

扩展功能:

  • 支持撤销/重做(维护操作栈)。
  • 保存为 SVG 或 PNG。
  • 添加预设形状(矩形、圆形)。

15.2 动态数据可视化图表

目标: 创建一个实时更新的折线图,模拟股票或传感器数据流。

项目结构与关键技术

模块技术点实现说明
坐标轴与网格Path 绘制直线见下方代码
数据映射坐标转换函数function dataToView(x, y) { ... }
动态折线Path + onFrame见下方代码
交互提示onMouseMove + HitResult见下方代码

坐标轴与网格:

// X/Y 轴
const xAxis = new Path.Line([0, height], [width, height]);
const yAxis = new Path.Line([0, 0], [0, height]);

// 网格线
for (let i = 0; i <= 10; i++) {
  new Path.Line([0, i*dy], [width, i*dy]).strokeColor = '#eee';
}

数据映射:

function dataToView(x, y) {
  const vx = x * scaleX; // x: 0~100 → vx: 0~width
  const vy = height - y * scaleY; // y 倒置
  return new Point(vx, vy);
}

动态折线 + 模拟数据流:

const graph = new Path();
graph.strokeColor = 'blue';
graph.strokeWidth = 2;

let data = [];

view.onFrame = function() {
  if (Math.random() > 0.7) {
    // 随机生成数据
    const newValue = 50 + 20 * Math.sin(Date.now() / 1000);
    data.push(newValue);
    // 限制数据点数量
    if (data.length > 50) data.shift();
    // 更新路径
    graph.removeSegments(); // 清除旧段
    graph.add(data.map((y, i) => dataToView(i, y))); // 重新绘制
  }
};

交互提示:

tool.onMouseMove = function(event) {
  const hit = graph.hitTest(event.point, { tolerance: 5 });
  if (hit) {
    tooltip.position = event.point;
    tooltip.content = `值: ${data[hit.location.index].toFixed(2)}`;
  }
};

扩展功能:

  • 多数据系列(不同颜色)。
  • 柱状图、饼图。
  • 数据导出(JSON/SVG)。

15.3 交互动画海报设计

目标: 设计一个响应鼠标悬停的动态海报,包含文字、图形和动画。

设计元素与动画逻辑

元素技术实现动画效果
背景粒子系统对象池 + onFrame见下方代码
标题文字PointText见下方代码
悬停放大onMouseMove + 缓动见下方代码
装饰图形Path + rotate()见下方代码

背景粒子系统:

const particles = [];
const pool = [];

function createParticle() {
  let p = pool.pop();
  if (!p) {
    p = new Path.Circle({
      center: [Math.random() * view.size.width, Math.random() * view.size.height],
      radius: 2 + Math.random() * 3,
      fillColor: 'rgba(255, 255, 255, 0.5)'
    });
  }
  p.opacity = 1;
  p.position = new Point(Math.random() * view.size.width, Math.random() * view.size.height);
  particles.push(p);
}

// onFrame 中更新粒子位置和透明度...

标题文字:

const title = new PointText({
  point: [200, 150],
  content: 'WELCOME',
  fontSize: 48,
  fillColor: 'white',
  fontWeight: 'bold'
});

悬停放大:

tool.onMouseMove = function(event) {
  const dist = event.point.getDistance(title.position);
  if (dist < 100) {
    // 鼠标靠近,距离越近放大越多
    title.scale(1 + (100 - dist) / 200);
  } else {
    title.scale(1); // 恢复
  }
};

装饰图形:

const orbit = new Path.Circle({
  center: view.center,
  radius: 200,
  strokeColor: 'rgba(255,255,255,0.2)'
});

const satellite = new Path.Circle([view.center.x + 200, view.center.y], 5, {
  fillColor: 'yellow'
});

view.onFrame = function() {
  satellite.rotate(1); // 绕中心旋转
};

设计理念:

  • 视觉层次:背景 → 装饰 → 文字。
  • 交互反馈:鼠标悬停提供动态响应。
  • 性能:粒子使用对象池,避免内存泄漏。

15.4 游戏原型:基于 Paper.js 的小游戏

项目: 弹球躲避游戏(Ball Dodge)

玩家控制一个小球,躲避从上方落下的障碍物。

游戏模块与代码骨架

// 1. 初始化
const player = new Path.Circle({
  center: view.center,
  radius: 10,
  fillColor: 'red'
});

let obstacles = [];
let score = 0;
let gameActive = true;

// 2. 玩家控制(鼠标)
tool.onMouseMove = function(event) {
  if (gameActive) {
    player.position = event.point.clamp(
      new Point(20, 20),
      view.bounds.bottomRight.subtract([20, 20])
    );
  }
};

// 3. 障碍物生成与移动
function createObstacle() {
  const width = 30 + Math.random() * 50;
  const obstacle = new Path.Rectangle({
    point: [Math.random() * (view.size.width - width), -50],
    size: [width, 20],
    fillColor: 'blue'
  });
  obstacles.push(obstacle);
}

view.onFrame = function(event) {
  if (!gameActive) return;

  // 移动障碍物
  obstacles.forEach((obs, i) => {
    obs.position.y += 5;
    // 移除屏幕外的障碍物
    if (obs.bounds.top > view.bounds.bottom) {
      obs.remove();
      obstacles.splice(i, 1);
      score++;
    }
    // 碰撞检测
    if (obs.bounds.intersects(player.bounds)) {
      gameOver();
    }
  });

  // 随机生成新障碍物
  if (Math.random() < 0.02) createObstacle();

  // 更新得分
  updateScoreDisplay();
};

// 4. 游戏结束
function gameOver() {
  gameActive = false;
  alert(`游戏结束!得分: ${score}`);
  // 可添加重玩按钮
}

// 5. 启动
createObstacle(); // 初始障碍物

关键游戏机制

机制实现技术
玩家控制onMouseMove + clamp() 限制范围
碰撞检测bounds.intersects() 粗略检测
动态生成onFrame 中随机 createObstacle()
状态管理gameActive 标志位控制游戏流程
得分系统障碍物通过屏幕时 score++

扩展功能:

  • 加分道具(绿色小球)。
  • 难度递增(速度加快)。
  • 音效与粒子爆炸效果。
  • 移动端支持(onTouch 事件)。

总结:

  • 绘图板:整合 Tool、UI 控件与状态管理,是交互工具的典型。
  • 数据可视化:将抽象数据映射为视觉元素,强调动态更新与坐标转换。
  • 交互动画海报:融合图形、动画与用户反馈,注重美学与体验。
  • 小游戏:综合运用动画、事件、碰撞检测和状态机,是复杂应用的缩影。