第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 项目,绑定指定 canvas | const 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)。 |
| fillColor | path.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.project | paper.project | 获取当前活动项目 | const proj = paper.project; | 所有图形操作默认作用于当前 activeProject。 |
| paper.view | paper.view | 获取当前活动视图 | const v = paper.view; | 用于访问视图尺寸、绑定事件、触发重绘等。 |
| project.activeLayer | project.activeLayer | 获取或设置当前活动图层 | const layer = paper.project.activeLayer; | 新创建的项目项(如 Path)自动添加到 activeLayer。 |
| project.layers | project.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.onFrame | paper.view.onFrame = function(event) | 注册每帧执行的回调函数,用于实现动画 | paper.view.onFrame = function(event) { circle.rotate(2); }; | event.delta 表示上一帧到当前帧的时间间隔(秒),可用于平滑动画。 |
| event.count | event.count | 表示当前是第几帧(从 1 开始) | console.log('Frame:', event.count); | 适用于需要计数的动画逻辑。 |
| event.time | event.time | 表示从页面加载到当前帧的总时间(秒) | const t = event.time; | 可用于基于时间的动画(如正弦波运动)。 |
| event.delta | event.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.closed | path.closed = true/false | 设置路径是否闭合(首尾连接) | path.closed = true; | 闭合路径会自动从最后一个点连回第一个点。 |
| path.strokeColor | path.strokeColor = color | 设置路径描边颜色 | path.strokeColor = 'black'; | 接受字符串、十六进制、Color 对象等。 |
| path.fillColor | path.fillColor = color | 设置路径填充颜色 | path.fillColor = '#ffcc00'; | 仅闭合路径支持填充。 |
3.2 直线路径:Path.Line
| 构造函数 | 语法 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Path.Line | new Path.Line(from, to) | 创建两点之间的直线路径 | const line = new Path.Line(new Point(20, 20), new Point(100, 100)); | 自动设置 strokeColor 可见,但默认无填充。 |
| from | line.from | 获取直线起点 | const start = line.from; | 返回 Point 对象。 |
| to | line.to | 获取直线终点 | const end = line.to; | 返回 Point 对象。 |
| strokeColor | line.strokeColor = color | 设置直线颜色 | line.strokeColor = 'blue'; | 必须设置描边才可见。 |
| strokeWidth | line.strokeWidth = width | 设置线宽 | line.strokeWidth = 3; | 默认为 1,可设为任意正数。 |
3.3 折线与多边形:Path.RegularPolygon、Path.Rectangle
| 构造函数 | 语法 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Path.RegularPolygon | new Path.RegularPolygon(center, sides, radius) | 创建正多边形 | const hexagon = new Path.RegularPolygon(new Point(100, 100), 6, 50); | sides 为边数(≥3),radius 为外接圆半径。 |
| center | polygon.center | 获取多边形中心点 | const center = hexagon.center; | 只读属性。 |
| sides | polygon.sides | 获取边数 | const n = hexagon.sides; | 只读属性。 |
| radius | polygon.radius | 获取外接圆半径 | const r = hexagon.radius; | 只读属性。 |
| Path.Rectangle | new Path.Rectangle(rect) 或 new Path.Rectangle(point, size) | 创建矩形路径 | const rectPath = new Path.Rectangle(new Point(50, 50), new Size(100, 80)); | 第一种用 Rectangle 对象,第二种用起点和尺寸。 |
| rectangle | rectPath.rectangle | 获取路径对应的矩形区域 | const rect = rectPath.rectangle; | 返回 Rectangle 对象。 |
| roundRadius | rectPath.roundRadius = radius 或 new Size(h, v) | 设置圆角半径 | rectPath.roundRadius = 10; // 或 new Size(15, 8) | 可设为单值(等圆角)或 Size(水平/垂直不同)。 |
3.4 圆形与椭圆:Path.Circle、Path.Ellipse
| 构造函数 | 语法 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Path.Circle | new Path.Circle(center, radius) | 创建圆形路径 | const circle = new Path.Circle(new Point(100, 100), 60); | center 为圆心,radius 为半径。 |
| center | circle.center | 获取圆心 | const center = circle.center; | 返回 Point 对象。 |
| radius | circle.radius | 获取半径 | const r = circle.radius; | 返回数值。 |
| Path.Ellipse | new Path.Ellipse(rect, rotation) | 创建椭圆路径 | const ellipse = new Path.Ellipse(new Rectangle(50, 50, 120, 80)); | rect 定义外接矩形,rotation 可选(弧度)。 |
| ellipse.rect | ellipse.rect | 获取椭圆外接矩形 | const rect = ellipse.rect; | 返回 Rectangle 对象。 |
| ellipse.rotation | ellipse.rotation = angle | 设置椭圆旋转角度(弧度) | ellipse.rotation = Math.PI / 4; | 仅当创建时未指定 rotation 才可修改。 |
3.5 弧线与曲线:Path.Arc、Path.BezierCurve
| 构造函数 | 语法 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Path.Arc | new Path.Arc(from, through, to) | 创建通过三点的弧线(起、经、终) | const arc = new Path.Arc([20, 100], [100, 20], [180, 100]); | 自动计算贝塞尔控制点,形成平滑弧线。 |
| from | arc.from | 获取弧线起点 | const start = arc.from; | 返回 Point 对象。 |
| through | arc.through | 获取弧线经过的中间点 | const mid = arc.through; | 返回 Point 对象。 |
| to | arc.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.onMouseDown | tool.onMouseDown = function(event) { } | 鼠标按下时触发,开始新路径 | tool.onMouseDown = function(e) { path = new Path(); path.add(e.point); }; | 通常在此创建新 Path。 |
| tool.onMouseDrag | tool.onMouseDrag = function(event) { } | 鼠标拖拽时持续触发 | tool.onMouseDrag = function(e) { path.add(e.point); }; | 每次拖动产生事件,添加点到路径。 |
| tool.onMouseUp | tool.onMouseUp = function(event) { } | 鼠标释放时触发 | tool.onMouseUp = function(e) { console.log('Drawn!'); }; | 可用于结束绘制、分析路径等。 |
| event.point | event.point | 获取事件发生时的坐标点 | const p = event.point; | 基于视图坐标系。 |
| event.delta | event.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 等
| 属性名称 | 语法 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| position | path.position | 获取或设置路径的中心位置(基于包围盒) | path.position = new Point(200, 150); | 移动整个路径,不影响其内部点的相对位置。 |
| bounds | path.bounds | 获取路径的边界矩形(Rectangle 对象) | const rect = path.bounds; console.log(rect.width, rect.height); | 包含旋转、缩放后的实际显示范围;只读。 |
| size | path.bounds.size | 获取路径包围盒的尺寸 | const size = path.bounds.size; | 返回 Size 对象,可用于缩放参考。 |
| area | path.area | 获取闭合路径的填充面积 | const area = path.area; | 仅对闭合路径有效;非闭合路径返回 0。 |
| length | path.length | 获取路径总长度(所有线段之和) | const len = path.length; | 可用于动画进度控制(如沿线移动)。 |
| strokeWidth | path.strokeWidth = width | 设置描边宽度 | path.strokeWidth = 4; | 默认为 1;可为任意正数,支持小数(如 0.5)。 |
| selected | path.selected = true/false | 设置路径是否处于选中状态(可视化高亮) | path.selected = true; | 便于调试或交互操作;样式由视图决定。 |
4.2 描边与填充:strokeColor、fillColor、strokeCap、strokeJoin
| 属性名称 | 语法 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| strokeColor | path.strokeColor = color | 设置描边颜色 | path.strokeColor = 'red'; 或 path.strokeColor = '#00ff00'; | 支持字符串、十六进制、RGB/HSB 对象。 |
| fillColor | path.fillColor = color | 设置填充颜色 | path.fillColor = new Color(0.5, 0.8, 1); // RGB 值 0~1 范围 | 仅闭合路径有效;可使用渐变(Gradient)。 |
| strokeCap | path.strokeCap = style | 设置线端样式 | path.strokeCap = 'round'; // 'butt', 'round', 'square' | 影响开放路径的起点和终点外观。 |
| strokeJoin | path.strokeJoin = style | 设置线段连接处样式 | path.strokeJoin = 'bevel'; // 'miter', 'bevel', 'round' | 仅对折线或路径拐角处有效。 |
| dashArray | path.dashArray = [on, off] | 设置虚线模式 | path.dashArray = [10, 5]; // 每 10 单位实线,5 单位空白 | 数组长度可为偶数多个值,实现复杂虚线;需配合 dashOffset 使用。 |
| dashOffset | path.dashOffset = offset | 设置虚线起始偏移量 | path.dashOffset = 3; | 控制虚线的相位,可用于动画滚动效果。 |
4.3 路径点操作:segments、add()、insert()、remove()
| 方法/属性 | 语法 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| segments | path.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 参数可选,默认到末尾。 |
| firstSegment | path.firstSegment | 获取第一个线段 | const first = path.firstSegment; | 返回 Segment 对象。 |
| lastSegment | path.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.y | point.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。 |
| length | point.length | 获取点到原点的距离(向量长度) | const dist = p1.length; | 即 √(x² + y²)。 |
| angle | point.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 / height | size.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.point | rect.point | 获取矩形左上角坐标 | const topLeft = rect.point; | 返回 Point 对象。 |
| rect.size | rect.size | 获取矩形尺寸 | const dim = rect.size; | 返回 Size 对象。 |
| rect.center | rect.center | 获取矩形中心点 | const center = rect.center; | 只读属性,基于 point 和 size 计算得出。 |
| rect.topLeft / topRight / bottomLeft / bottomRight | rect.topRight | 获取四个角点 | const tr = rect.topRight; | 返回 Point 对象,便于定位。 |
| rect.width / height | rect.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。 |
| isIdentity | matrix.isIdentity | 判断是否为单位矩阵(无任何变换) | if (matrix.isIdentity) { ... } | 用于状态检测。 |
| values | matrix.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: 0 |
| 灰度(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
| 属性/概念 | 语法 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| GradientColor | new 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.name | layer.name = '背景'; | 获取或设置图层名称 | layer.name = 'Foreground'; console.log(layer.name); | 便于识别和脚本操作,不影响渲染。 |
| layer.visible | layer.visible = false; | 控制图层是否可见(隐藏/显示) | layer.visible = !layer.visible; // 切换可见性 | 设置 false 后,图层内所有子项不可见,不影响图层结构。 |
| layer.locked | layer.locked = true; | 锁定图层,防止编辑 | layer.locked = true; // 无法选择或修改内容 | 常用于保护背景或参考线,锁定后仍可编程修改。 |
| layer.opacity | layer.opacity = 0.5; | 设置图层整体透明度 | layer.opacity = 0.8; // 20% 透明 | 0 = 完全透明,1 = 完全不透明,影响所有子项。 |
| project.layers | project.layers | 获取项目中所有图层的数组 | const allLayers = project.layers; | 按创建顺序排列,可用于遍历或重新排序。 |
| project.activeLayer | project.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.children | group.children | 获取组内所有子项的数组 | const items = group.children; | 顺序为添加顺序,可读取但不建议直接修改。 |
| group.applyMatrix | group.applyMatrix = false; | 是否将变换”压平”到子项 | group.rotate(45); group.applyMatrix = true; | true:变换应用到子项,组自身无变换;false(默认):变换作用于组容器。 |
| group.clipped | group.clipped = true; | 设置组是否启用剪切模式 | const group = new Group([clipPath, content]); group.clipped = true; | 第一个子项作为剪切路径,常用于实现遮罩效果。 |
| group.pivot | group.pivot = new Point(x, y); | 设置组的变换支点(旋转、缩放中心) | group.pivot = group.bounds.center; group.rotate(30); | 默认为 bounds.topLeft,影响 rotate(), scale() 等操作。 |
| group.bounds / strokeBounds | group.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.onFrame | view.onFrame = function(event) { ... } | 定义每帧执行的逻辑 | view.onFrame = function(event) { rotor.rotate(1); }; | 自动启动,无需手动调用。 |
| event.count | event.count | 当前是第几帧(从1开始) | if (event.count % 60 === 0) { ... } | 适合定时逻辑(如每秒更新)。 |
| event.delta | event.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
| 类型 | 语法与构造方式 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| PointText | new 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 字体、字号与文本样式
| 属性 | 语法 | 用途说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| content | text.content = '新文本'; | 设置或获取文本内容 | text.content = 'Welcome to Paper.js'; | 支持 Unicode 字符(包括中文)。 |
| fontSize | text.fontSize = 24; | 设置字体大小(单位:点 pt) | text.fontSize = 18; | 默认值通常为 10 或 12pt。 |
| font | text.font = 'Arial'; | 设置字体族或完整 CSS 风格字体 | text.font = 'bold italic 20pt "Times New Roman"'; | 必须确保字体已加载。 |
| fontWeight | text.fontWeight = 'bold'; | 设置字重 | text.fontWeight = 'normal'; | 可选值:normal, bold, 100~900 等。 |
| fontStyle | text.fontStyle = 'italic'; | 设置字体风格 | text.fontStyle = 'italic'; | 用于斜体文本。 |
| fillColor | text.fillColor = 'red'; | 设置文本填充颜色 | text.fillColor = '#333'; | 支持所有 Color 格式。 |
| strokeColor / strokeWidth | text.strokeColor = 'black'; text.strokeWidth = 0.5; | 设置描边颜色和宽度 | text.strokeColor = 'white'; text.strokeWidth = 1; | 实现”描边文字”效果。 |
| justification | text.justification = 'center'; | 设置文本对齐方式 | text.justification = 'right'; | ’left’, ‘center’, ‘right’。 |
| position / point | text.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.onLoad | raster.onLoad = function() { ... } | 图像加载完成后的回调 | img.onLoad = function() { console.log(this.size); }; | 必须使用:确保尺寸可用后再操作。 |
| raster.size / width / height | console.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.point | segment.point | 获取或设置锚点位置 | segment.point = new Point(100, 100); | 改变锚点会移动整个线段。 |
| Segment.handleIn | segment.handleIn | 入控制点 | segment.handleIn = new Point(-15, 0); | null 或 (0,0) 时为尖角或直线。 |
| Segment.handleOut | segment.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.firstSegment、path.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.matrix | console.log(item.matrix); | 获取对象的当前变换矩阵 | rect.rotate(30); console.log(rect.matrix); | 包含平移、旋转、缩放、剪切等信息;6个值:a, b, c, d, tx, ty(仿射变换)。 |
| item.applyMatrix | item.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. 停止动画 | 条件满足时清除 onFrame | if (ball.position.x > view.bounds.width + 100) { view.onFrame = null; } | 设为 null 可暂停全局帧循环。 |
动画最佳实践:
- 始终使用
event.delta,避免setTimeout或固定步长。 - 将动画状态(位置、角度、透明度等)存储在变量中,便于控制。
- 复杂动画可封装为独立函数或类。
11.4 缓动函数与动画库集成思路
缓动函数(Easing Functions)用于创建更自然的动画效果(如渐入渐出、弹跳等),而非线性运动。
| 概念 | 说明 | 常见缓动函数 | 代码示例 |
|---|---|---|---|
| 什么是缓动函数 | 输入时间比例 t(0 | linear(t) = t、easeInQuad(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.tool 或 view.tool | console.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 并返回根 Item | const 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 中的 SVG | project.importSVG(document.getElementById('mySvg')) | 从页面内嵌 SVG 导入 | const imported = project.importSVG('mySvg'); imported.scale(2); | 适合复用预定义图标。 |
导入后的操作: 返回的对象可像普通 Item 一样进行 translate()、rotate()、scale() 等变换;可访问其子项:imported.children、imported.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.bounds、item.matrix、project.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 控件与状态管理,是交互工具的典型。
- 数据可视化:将抽象数据映射为视觉元素,强调动态更新与坐标转换。
- 交互动画海报:融合图形、动画与用户反馈,注重美学与体验。
- 小游戏:综合运用动画、事件、碰撞检测和状态机,是复杂应用的缩影。