# Konva.js > Konva.js 是一个开源的 2D HTML5 Canvas JavaScript 框架。它为 Canvas 图形提供面向对象的 API,支持图形、动画、事件、拖放、滤镜,并集成 React、Vue、Svelte 和 Angular。 Konva 使用 Stage → Layer → Group → Shape 的层次结构。你创建一个 Stage(挂载到 DOM 容器),添加若干 Layer(每个 Layer 是一个独立的 `` 元素),然后在这些图层上绘制 Shape(Rect、Circle、Ellipse、Line、Text、Image、Path、Star、Ring、Arc、Arrow、Label、RegularPolygon、Wedge、Sprite、TextPath)。 核心能力:面向对象的图形管理、完整的事件系统(单击、悬停、触摸、拖动)、内置拖放、动画与补间、图像滤镜(模糊、提亮、对比度、灰度等)、使用 `toJSON()` 的 Canvas 序列化、高质量图像导出(`toDataURL()`、`toBlob()`)、节点嵌套与分组、命中检测,以及用于提升性能的缓存。 框架绑定: - React:`react-konva` —— 用于 Canvas 图形的声明式 React 组件 - Vue:`vue-konva` —— 用于 Konva 的 Vue 组件 - Svelte:`svelte-konva` —— 用于 Konva 的 Svelte 组件 - Angular:`ng2-konva` —— 用于 Konva 的 Angular 组件 安装:`npm install konva`(框架绑定另需 `react-konva`、`vue-konva`、`svelte-konva` 或 `ng2-konva`) ## 文档 - [Konva 入门 — HTML5 Canvas 2D 框架](https://konvajs.org/zh-Hans/docs/index.html): 开始使用 Konva.js 2D HTML5 Canvas JavaScript 框架。学习绘制图形、处理事件、拖放和动画,以及在 React、Vue、Svelte 和 Angular 中使用 Konva。 - [Konva 框架概览](https://konvajs.org/zh-Hans/docs/overview.html): 了解 Konva.js 的 Stage、Layer、Group 和 Shape 架构,以及元素组织、事件处理、图形样式和拖放功能。 - [API Reference](https://konvajs.org/api/Konva.html): Full API documentation for all classes - [Konva.js FAQ——常见问题](https://konvajs.org/zh-Hans/docs/faq.html): 解答有关 Konva.js 的常见问题,包括选择 Canvas 库、React/Vue/Svelte 集成、性能、TypeScript 和移动设备支持等。 - [关于 Konva.js——开源 HTML5 Canvas JavaScript 框架](https://konvajs.org/zh-Hans/docs/about.html): Konva.js 是 Anton Lavrenov 于 2015 年创建的开源 2D HTML5 Canvas JavaScript 框架,采用 MIT 许可证。了解它的历史、功能、应用情况和生态系统。 ## 指南与对比 - [为什么选择 Konva?何时在项目中使用 Konva.js](https://konvajs.org/zh-Hans/docs/guides/why-konva.html): 何时应使用 Konva.js?了解 Konva 解决的问题、不适合的任务、包含实际示例的理想使用场景,以及何时选择其他库。 - [2026 年最佳 JavaScript Canvas 库——如何选择](https://konvajs.org/zh-Hans/docs/guides/best-canvas-library.html): 如何选择 JavaScript Canvas 库。由 Konva 作者编写的快速决策指南,比较 Konva.js、Fabric.js、PixiJS、Paper.js 和 p5.js。 ## 应用示例 - [Canvas 编辑器 — 使用 JavaScript 和 HTML5 Canvas 构建设计编辑器](https://konvajs.org/zh-Hans/docs/sandbox/Canvas_Editor.html): 使用 Konva 和 React 在 HTML5 Canvas 上构建设计编辑器。选择、缩放手柄、撤销与重做、PNG 导出 — 以及对生产级编辑器还需要什么的诚实说明。 - [带缩放和平移的无限 Canvas——使用 JavaScript 构建](https://konvajs.org/zh-Hans/docs/sandbox/Infinite_Canvas.html): 使用 JavaScript 和 Konva.js 构建带缩放、平移和网格背景的无限 Canvas。滚动以缩放,拖动以平移。包含 React、Vue 和原生 JS 交互式示例。 - [React 流程图——使用 JavaScript 在 Canvas 上构建交互式流程图和图表](https://konvajs.org/zh-Hans/docs/sandbox/Connected_Objects.html): 使用 JavaScript 和 React 在 HTML5 Canvas 上构建交互式流程图。拖动节点、自动更新连接,并使用 Konva.js 和 react-konva 创建可视化工作流和图表。 - [如何使用 JavaScript Canvas 构建图像标注工具](https://konvajs.org/zh-Hans/docs/sandbox/Image_Labeling.html): 使用 JavaScript、HTML5 Canvas 和 Konva.js 构建图像注释和标注工具。此交互式示例适用于机器学习数据标注和图像标记。 - [如何使用 JavaScript Canvas 构建交互式楼层平面图和地图](https://konvajs.org/zh-Hans/docs/sandbox/Interactive_Building_Map.html): 使用 JavaScript、HTML5 Canvas 和 Konva.js 构建交互式建筑楼层平面图和地图。将鼠标悬停在区域上以查看说明。 - [如何使用 JavaScript Canvas 构建交互式座位图](https://konvajs.org/zh-Hans/docs/sandbox/Seats_Reservation.html): 使用 JavaScript、HTML5 Canvas 和 Konva.js 构建交互式座位预订组件。座位图示例支持单击选择。 - [Canvas 图像裁剪——使用 JavaScript 裁剪和提取图像区域](https://konvajs.org/zh-Hans/docs/sandbox/Canvas_Crop_Image.html): 学习使用 JavaScript 在 HTML5 Canvas 上裁剪图像。选择区域、调整裁剪区域的大小,然后使用 Konva.js 提取裁剪后的图像。包含交互式示例和源代码。 - [多人白板 — 使用 Konva 和 Yjs 构建实时协作画布](https://konvajs.org/zh-Hans/docs/sandbox/Multiplayer_Whiteboard.html): 使用 Konva、React 和 Yjs 在 HTML5 Canvas 上构建实时协作白板。了解共享文档 state、无冲突同步,以及生产环境还需要什么。 - [Canvas 绘图——使用 JavaScript 在 HTML5 Canvas 上自由绘制和涂画](https://konvajs.org/zh-Hans/docs/sandbox/Free_Drawing.html): 使用 JavaScript 在 HTML5 Canvas 上绘图。使用 Konva.js 构建带自由笔画、画笔工具和橡皮擦的 Canvas 绘图应用。介绍矢量图形和手动 2D 绘图两种方法。 - [示例](https://konvajs.org/zh-Hans/docs/sandbox.html): 使用 Konva.js 构建的交互式示例:签名板、图像编辑器、游戏、拖放和动画等。可在浏览器中直接运行。 ## 框架集成 - [React Canvas 库 — react-konva 入门](https://konvajs.org/zh-Hans/docs/react/index.html): react-konva 是一个用于绘制 2D 图形的 React Canvas 库。使用声明式 JSX 创建具有图形、拖放、事件和动画的交互式 Canvas 应用。 - [使用 Vue 和 Konva 开始 Canvas 开发](https://konvajs.org/zh-Hans/docs/vue/index.html): 开始使用 vue-konva,它是 Konva.js 的官方 Vue 绑定。使用 Vue 组件绘制图形、处理事件并构建交互式 Canvas 应用。 - [通过 Konva 开始使用 Svelte 和 Canvas](https://konvajs.org/zh-Hans/docs/svelte/index.html): 开始使用 svelte-konva,它是 Konva.js 的官方 Svelte 绑定。使用 Svelte 组件绘制图形、处理事件并构建交互式 Canvas 应用。 - [通过 Konva 开始使用 Angular 和 Canvas](https://konvajs.org/zh-Hans/docs/angular/index.html): 开始使用 ng2-konva,它是 Konva.js 的官方 Angular 绑定。使用 Angular 指令绘制图形、处理事件并构建交互式 Canvas 应用。 ## 教程 - [HTML5 Canvas 矩形教程](https://konvajs.org/zh-Hans/docs/shapes/Rect.html): 学习如何使用 Konva.js 在 HTML5 Canvas 上绘制矩形。使用 Konva.Rect 图形设置填充、描边、尺寸、位置、圆角、阴影等属性。 - [HTML5 Canvas 图形事件](https://konvajs.org/zh-Hans/docs/events/Binding_Events.html): 学习如何使用 Konva.js 为 HTML5 Canvas 图形绑定事件监听器,以及如何处理 click、dblclick、mouseover、mouseout、mousemove 等事件。 - [HTML5 Canvas 拖放教程](https://konvajs.org/zh-Hans/docs/drag_and_drop/Drag_and_Drop.html): 学习使用 Konva.js 为 HTML5 Canvas 图形添加拖放功能。通过一个属性使任意图形可以拖动,并处理拖动事件。 - [HTML5 Canvas Konva 动画教程](https://konvajs.org/zh-Hans/docs/animations/Create_an_Animation.html): 学习如何使用 Konva.js 在 HTML5 Canvas 上创建动画。使用 Konva.Animation 创建逐帧动画,使用 Konva.Tween 实现属性过渡。 - [HTML5 Canvas 图像模糊滤镜教程](https://konvajs.org/zh-Hans/docs/filters/Blur.html): 学习如何使用 Konva.js 对 HTML5 Canvas 图像应用模糊滤镜,并通过 blurRadius 属性调整模糊程度。 - [HTML5 Canvas Konva 全部性能技巧列表](https://konvajs.org/zh-Hans/docs/performance/All_Performance_Tips.html): Konva.js 的全部性能优化技巧,包括图层管理、图形缓存、禁用监听和批量绘制等内容。在 HTML5 Canvas 上高效渲染数千个图形。 - [HTML5 Canvas 舞台序列化教程](https://konvajs.org/zh-Hans/docs/data_and_serialization/Serialize_a_Stage.html): 学习如何使用 Konva.js 将 HTML5 Canvas 状态序列化并保存为 JSON,以及如何使用 stage.toJSON() 导出并通过 Konva.Node.create() 恢复 Canvas 内容。 - [HTML5 Canvas 图形选择、调整尺寸和旋转](https://konvajs.org/zh-Hans/docs/select_and_transform/Basic_demo.html): 学习如何使用 Konva.js Transformer 选择、调整 HTML5 Canvas 图形的尺寸并旋转图形。为任意图形添加可交互的调整尺寸和旋转手柄。 - [Node.js 配置](https://konvajs.org/zh-Hans/docs/nodejs/nodejs-setup): 使用 canvas 或 skia-canvas 后端,在 Node.js 中配置 Konva 以进行服务器端渲染、图像生成和批处理。 ## 可选 - [Konva.js 开发 AI 工具](https://konvajs.org/zh-Hans/docs/ai_tools.html): 使用 AI 更快地构建 Konva.js 应用。与通过 Konva 文档训练的 AI 机器人聊天,或将 Konva MCP 服务器连接到 Cursor、Claude Desktop、Windsurf 和其他 AI 编程工具。 - [需要 Konva 库的相关帮助?](https://konvajs.org/zh-Hans/docs/support.html): 通过 StackOverflow、GitHub Issues、Discord 聊天、咨询和其他社区资源获取 Konva 帮助。 - [GitHub Repository](https://github.com/konvajs/konva): Source code, issues, changelog - [npm Package](https://www.npmjs.com/package/konva): Installation and version info - [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=llms&utm_content=llms-optional): Commercial design editor SDK built on Konva by the Konva maintainers --- # Konva 入门 — HTML5 Canvas 2D 框架 > 开始使用 Konva.js 2D HTML5 Canvas JavaScript 框架。学习绘制图形、处理事件、拖放和动画,以及在 React、Vue、Svelte 和 Angular 中使用 Konva。 Source: https://konvajs.org/zh-Hans/docs/index.html ## Konva 是什么? Konva 是一个用于创建交互式 2D 图形的 HTML5 Canvas JavaScript 框架。它在 Canvas 之上提供对象模型。 你可以创建图形、组合图形、添加事件监听器、拖动图形和播放动画。Konva 负责渲染、命中检测和状态管理。 Konva 支持桌面设备和移动设备。它可以高效渲染数千个图形,并为 **React**、**Vue**、**Svelte** 和 **Angular** 提供官方集成。 ## 快速示例 ```javascript // Create a stage (container for all layers) const stage = new Konva.Stage({ container: 'container', width: 500, height: 400, }); // Create a layer const layer = new Konva.Layer(); stage.add(layer); // Create a draggable rectangle const rect = new Konva.Rect({ x: 50, y: 50, width: 100, height: 80, fill: 'cornflowerblue', shadowBlur: 5, cornerRadius: 4, draggable: true, }); layer.add(rect); // Add event listener rect.on('click tap', () => { rect.fill(Konva.Util.getRandomColor()); }); ``` 这段代码创建一个可拖动的矩形。单击矩形时,它会改变颜色。代码不需要额外模板或渲染循环。 ## 安装 Konva 如果你使用包管理器,请运行: ```bash npm install konva ``` 你也可以使用脚本标签: ```html ``` 你还可以从 CDN 下载文件: - [完整版本 konva.js](https://unpkg.com/konva@10/konva.js) - [压缩版本 konva.min.js](https://unpkg.com/konva@10/konva.min.js) ## 在框架中使用 Konva Konva 为主要前端框架提供官方绑定: | 框架 | 包 | 安装命令 | |-----------|---------|---------| | **React** | [`react-konva`](https://github.com/konvajs/react-konva) | `npm install react-konva konva` | | **Vue** | [`vue-konva`](https://github.com/konvajs/vue-konva) | `npm install vue-konva konva` | | **Svelte** | [`svelte-konva`](https://github.com/konvajs/svelte-konva) | `npm install svelte-konva konva` | | **Angular** | [`ng2-konva`](https://github.com/konvajs/ng2-konva) | `npm install ng2-konva konva` | 从对应框架的指南开始:[React](/zh-Hans/docs/react/index.html) · [Vue](/zh-Hans/docs/vue/index.html) · [Svelte](/zh-Hans/docs/svelte/index.html) · [Angular](/zh-Hans/docs/angular/index.html) ## 为什么使用 Konva? - **图形是对象** — 你可以创建矩形、圆形、线条、文本、图像和路径。每个图形都是可独立修改的 JavaScript 对象。 - **完整的事件系统** — 支持 `click`、`dblclick`、`mouseover`、`mouseout`、`touchstart`、`dragstart` 和 `dragend` 等事件。事件会像 DOM 事件一样经过组和图层冒泡。 - **拖放** — 为任意图形设置 `draggable: true`。你还可以添加拖动边界、吸附和放置事件。 - **缩放和旋转** — 内置的 [`Transformer`](/zh-Hans/docs/select_and_transform/Basic_demo.html) 组件可以为任意图形添加缩放和旋转控制柄。 - **多图层渲染** — 每个 Layer 都是独立的 `` 元素。交互图形变化时,静态背景不需要重新渲染。 - **序列化** — 使用 `stage.toJSON()` 保存节点树及其可序列化属性。使用 `Konva.Node.create(json)` 恢复这些内容。图像、事件处理函数和自定义绘制函数必须单独恢复。 - **滤镜和效果** — 可以对单个图形应用模糊、亮度、对比度、灰度和像素化等效果。 - **高性能** — Konva 可以处理数千个图形。请参阅[性能建议](/zh-Hans/docs/performance/All_Performance_Tips.html)和[压力测试示例](/zh-Hans/docs/sandbox/10000_Shapes_with_Tooltip.html)。 ## 可以创建什么? 开发者使用 Konva 创建设计编辑器、绘图应用、标注工具、交互式地图和数据可视化。下面是一些示例: - [Canvas 设计编辑器](/zh-Hans/docs/sandbox/Canvas_Editor.html) — 类似 Canva 的设计工具 - [自由绘图应用](/zh-Hans/docs/sandbox/Free_Drawing.html) — 白板和自由绘图 - [图像标注](/zh-Hans/docs/sandbox/Image_Labeling.html) — 机器学习标注工具 - [座位预订图](/zh-Hans/docs/sandbox/Seats_Reservation.html) — 交互式座位预订 - [交互式建筑地图](/zh-Hans/docs/sandbox/Interactive_Building_Map.html) — 楼层平面图可视化 - [连接对象](/zh-Hans/docs/sandbox/Connected_Objects.html) — 图表和流程图编辑器 [查看 60 多个示例 →](/zh-Hans/docs/sandbox.html) 如果你需要完整的设计编辑器,而不只是 Canvas 库,可以使用 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=getting-started)。它是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK:`npm install polotno`。 ## 后续步骤 - [Konva 概览](/zh-Hans/docs/overview.html) — 了解 Stage → Layer → Shape 架构 - [图形](/zh-Hans/docs/shapes/Rect.html) — 了解所有可用图形 - [事件](/zh-Hans/docs/events/Binding_Events.html) — 处理单击、悬停和触摸等事件 - [拖放](/zh-Hans/docs/drag_and_drop/Drag_and_Drop.html) — 使图形可以拖动 - [动画](/zh-Hans/docs/animations/Create_an_Animation.html) — 为图形属性添加动画 - [关于 Konva](/zh-Hans/docs/about.html) — 了解使用者、主要信息和相关链接 --- # Konva 框架概览 > 了解 Konva.js 的 Stage、Layer、Group 和 Shape 架构,以及元素组织、事件处理、图形样式和拖放功能。 Source: https://konvajs.org/zh-Hans/docs/overview.html ## Konva 是什么? Konva 是一个 HTML5 Canvas JavaScript 框架。它扩展了 2D 上下文,并为桌面应用和移动应用提供 Canvas 交互功能。 Konva 支持高性能动画、过渡、节点嵌套、图层、滤镜、缓存和事件处理。 ## Konva 如何工作? 所有内容都从 `Konva.Stage` 开始。一个舞台包含一个或多个 `Konva.Layer`。 每个图层有两个 `` 渲染器:场景渲染器和命中图渲染器。场景渲染器绘制可见内容。 命中图渲染器使用一个隐藏的 Canvas 来高效检测事件。 每个图层可以包含图形、图形组或嵌套组。舞台、图层、组和图形都是虚拟节点,类似 HTML 页面中的 DOM 节点。 下面是一个节点层级示例: ``` Stage | +------+------+ | | Layer Layer | | +-----+-----+ Shape | | Group Group | | + +---+---+ | | | Shape Group Shape | + | Shape ``` 所有节点都可以设置样式和变换。`Konva` 提供矩形、圆形、图像、精灵、文本、线条、多边形、正多边形、路径和星形等预置图形。 你也可以实例化 Shape 类,并提供绘制函数来创建自定义图形。 创建舞台、图层和图形后,你可以绑定事件、变换节点、播放动画和应用滤镜。 下面是一个最小代码示例: ```js // first we need to create a stage var stage = new Konva.Stage({ container: 'container', // id of container
width: 500, height: 500, }); // then create layer var layer = new Konva.Layer(); // create our shape var circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); // add the shape to the layer layer.add(circle); // add the layer to the stage stage.add(layer); ``` 结果: ![最小代码示例](/assets/overview-circle.png) ## 基本图形 Konva.js 支持以下图形:[Rect](/zh-Hans/docs/shapes/Rect.html)、[Circle](/zh-Hans/docs/shapes/Circle.html)、[Ellipse](/zh-Hans/docs/shapes/Ellipse.html)、[Line](/zh-Hans/docs/shapes/Line_-_Simple_Line.html)、[Polygon](/zh-Hans/docs/shapes/Line_-_Polygon.html)、[Spline](/zh-Hans/docs/shapes/Line_-_Spline.html)、[Blob](/zh-Hans/docs/shapes/Line_-_Blob.html)、[Image](/zh-Hans/docs/shapes/Image.html)、[Text](/zh-Hans/docs/shapes/Text.html)、[TextPath](/zh-Hans/docs/shapes/TextPath.html)、[Star](/zh-Hans/docs/shapes/Star.html)、[Label](/zh-Hans/docs/shapes/Label.html)、[SVG Path](/zh-Hans/docs/shapes/Path.html) 和 [RegularPolygon](/zh-Hans/docs/shapes/RegularPolygon.html)。 你也可以创建[自定义图形](/zh-Hans/docs/shapes/Custom.html): ```js var triangle = new Konva.Shape({ sceneFunc: function (context) { context.beginPath(); context.moveTo(20, 50); context.lineTo(220, 80); context.quadraticCurveTo(150, 100, 260, 170); context.closePath(); // special Konva.js method context.fillStrokeShape(this); }, fill: '#00D2FF', stroke: 'black', strokeWidth: 4, }); ``` ![自定义图形](/assets/overview-custom.png) ## 样式 每个图形都支持以下样式属性: - 填充:纯色、渐变或图像 - 描边:颜色和宽度 - 阴影:颜色、偏移、不透明度和模糊程度 - 不透明度 ```js var pentagon = new Konva.RegularPolygon({ x: stage.width() / 2, y: stage.height() / 2, sides: 5, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, shadowOffsetX: 20, shadowOffsetY: 25, shadowBlur: 40, opacity: 0.5, }); ``` ![样式](/assets/overview-styles.png) ## 事件 `Konva` 可以监听用户输入事件,例如 `click`、`dblclick`、`mouseover`、`tap`、`dbltap` 和 `touchstart`。 它也可以监听属性变化事件和拖放事件。属性变化事件包括 `scaleXChange` 和 `fillChange`。拖放事件包括 `dragstart`、`dragmove` 和 `dragend`。 ```js circle.on('mouseout touchend', function () { console.log('user input'); }); circle.on('xChange', function () { console.log('position change'); }); circle.on('dragend', function () { console.log('drag stopped'); }); ``` 请查看[可运行示例](/zh-Hans/docs/events/Binding_Events.html)。 ## 拖放 `Konva` 内置拖动支持。Konva 当前没有 `drop`、`dragenter`、`dragleave` 和 `dragover` 事件。 你可以[使用 Konva 事件实现这些事件](/zh-Hans/docs/drag_and_drop/Drop_Events.html)。 将 draggable 属性设置为 true,使图形可以拖动。 ``` shape.draggable('true'); ``` 然后,你可以监听拖放事件,并设置[移动范围](/zh-Hans/docs/drag_and_drop/Complex_Drag_and_Drop.html)。 ## 滤镜 `Konva` 提供模糊、反色和噪点等滤镜。请在 [Filters API](/zh-Hans/api/Konva.Filters.html) 中查看所有滤镜。 示例: ![滤镜](/assets/overview-filter.png) ## 动画 你可以用两种方式创建动画: 1. 使用 `Konva.Animation`。请查看[示例](/zh-Hans/docs/animations/Moving.html): ```js var anim = new Konva.Animation(function (frame) { var time = frame.time, timeDiff = frame.timeDiff, frameRate = frame.frameRate; // update stuff }, layer); anim.start(); ``` 2. 使用 `Konva.Tween`。请查看[示例](/zh-Hans/docs/tweens/Linear_Easing.html): ```js var tween = new Konva.Tween({ node: rect, duration: 1, x: 140, rotation: Math.PI * 2, opacity: 1, strokeWidth: 6, }); tween.play(); // or new shorter method: circle.to({ duration: 1, fill: 'green', }); ``` ## 选择器 大型应用经常需要查找元素。`Konva` 支持使用选择器查找元素。 使用 `find()` 方法返回集合。使用 `findOne()` 方法返回集合中的第一个元素。 ```js var circle = new Konva.Circle({ radius: 10, fill: 'red', id: 'face', name: 'red circle', }); layer.add(circle); // then try to search // find by type layer.find('Circle'); // returns array of all circles // find by id layer.findOne('#face'); // find by name (like css class) layer.find('.red'); ``` ## 序列化和反序列化 你可以将创建的对象保存为 JSON。你可以将 JSON 保存到服务器或本地存储。 ```js var json = stage.toJSON(); ``` 你也可以从 JSON 恢复对象: ```js var json = '{"attrs":{"width":578,"height":200},"className":"Stage","children":[{"attrs":{},"className":"Layer","children":[{"attrs":{"x":100,"y":100,"sides":6,"radius":70,"fill":"red","stroke":"black","strokeWidth":4},"className":"RegularPolygon"}]}]}'; var stage = Konva.Node.create(json, 'container'); ``` ## 性能 `Konva` 提供多种提高应用速度的工具。下面是最重要的两种方法: 1. 缓存可以将元素绘制到缓冲 Canvas。之后,Konva 从该 Canvas 绘制元素。缓存可以明显提高复杂文本、阴影和描边图形的性能。 ```js shape.cache(); ``` [查看示例](/zh-Hans/docs/performance/Shape_Caching.html) 2. 使用多个图层。Konva 支持多个 `` 元素,因此你可以按更新频率组织对象。 例如,应用可以包含复杂背景和多个移动图形。将背景放在一个图层中,并将移动图形放在另一个图层中。 更新图形时,不需要更新背景 Canvas。请查看[示例](/zh-Hans/docs/performance/Layer_Management.html)。 请在以下页面中查看所有性能建议: [https://konvajs.org/docs/performance/All_Performance_Tips.html](/zh-Hans/docs/performance/All_Performance_Tips.html) ## 延伸阅读 - [为什么使用 Konva?— 何时为项目选择 Konva](/zh-Hans/docs/guides/why-konva.html) - [最佳 JavaScript Canvas 库 — 对比指南](/zh-Hans/docs/guides/best-canvas-library.html) - [Canvas 库对比](/zh-Hans/docs/guides/best-canvas-library.html) - [常见问题](/zh-Hans/docs/faq.html) - [关于 Konva — 主要信息和使用情况](/zh-Hans/docs/about.html) --- # Konva.js FAQ——常见问题 > 解答有关 Konva.js 的常见问题,包括选择 Canvas 库、React/Vue/Svelte 集成、性能、TypeScript 和移动设备支持等。 Source: https://konvajs.org/zh-Hans/docs/faq.html ## 常见问题 ### Konva.js 是什么? Konva.js 是一个开源 2D HTML5 Canvas JavaScript 框架,为 Canvas 图形提供面向对象的 API。它支持图形、动画、事件、拖放和滤镜,并提供 React、Vue、Svelte 和 Angular 的官方集成。它是 npm 上下载量最高的 2D Canvas 框架。 Konva 使用 Stage → Layer → Shape 层级结构。每个 Layer 都是独立的 `` 元素,以获得最佳渲染性能。 [阅读完整概览 →](/zh-Hans/docs/overview.html) ### Konva.js 可以免费使用吗? 可以。Konva.js 采用 MIT 许可证,商业和个人用途均完全免费。它没有付费层级或高级功能。源代码可在 [GitHub](https://github.com/konvajs/konva) 上获取。 ### 有基于 Konva 的现成设计编辑器吗? 有。[Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=faq-editor) 是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK。它提供 Canva 类编辑器在画布之外所需的功能:模板、支持字体加载的文本引擎、侧边面板和工具栏、历史记录,以及 PNG、PDF 和视频导出。如果编辑器本身就是你的产品,而且你需要掌控文档模型,请直接使用 Konva。如果编辑器用于支持你销售的其他产品,可以使用 Polotno。[Canvas 编辑器示例](/zh-Hans/docs/sandbox/Canvas_Editor.html)展示了自行构建的方式,以及开发成本较高的部分。 ### 应使用哪个 JavaScript Canvas 库? 这取决于使用场景: - **Konva.js** — 最适合交互式 2D Canvas 应用程序,例如设计编辑器、标注工具、图表、交互式地图和仪表板。它提供最佳的框架集成(React、Vue、Svelte、Angular)以及最全面的拖放和事件系统。 - **PixiJS** — 最适合使用 WebGL 的 2D 游戏和高帧率渲染。它使用 WebGL 进行 GPU 加速。 - **Fabric.js** — 适合图像编辑和操作工具。功能集与 Konva 相似,但没有官方框架绑定。 - **Paper.js** — 最适合矢量图形、数学艺术和路径操作。 - **p5.js** — 最适合创意编程、生成艺术和教育用途。 有关详细比较,请参阅 [Canvas 库比较指南](/zh-Hans/docs/guides/best-canvas-library.html)。另请参阅[为什么选择 Konva?](/zh-Hans/docs/guides/why-konva.html),深入了解 Konva 适合和不适合的任务。 ### 如何结合 React 使用 Canvas? 使用 Konva.js 的官方 React 绑定 [`react-konva`](https://github.com/konvajs/react-konva): ```bash npm install react-konva konva ``` ```jsx import { Stage, Layer, Rect, Circle } from 'react-konva'; function App() { return ( ); } ``` `react-konva` 为所有 Konva 图形提供声明式 React 组件,并完整支持 React state、props 和事件处理。 [阅读完整 React 教程 →](/zh-Hans/docs/react/index.html) ### 如何结合 Vue 使用 Canvas? 使用 [`vue-konva`](https://github.com/konvajs/vue-konva): ```bash npm install vue-konva konva ``` [阅读完整 Vue 教程 →](/zh-Hans/docs/vue/index.html) ### 如何结合 Svelte 使用 Canvas? 使用 [`svelte-konva`](https://github.com/konvajs/svelte-konva): ```bash npm install svelte-konva konva ``` [阅读完整 Svelte 教程 →](/zh-Hans/docs/svelte/index.html) ### 如何结合 Angular 使用 Canvas? 使用 [`ng2-konva`](https://github.com/konvajs/ng2-konva): ```bash npm install ng2-konva konva ``` [阅读完整 Angular 教程 →](/zh-Hans/docs/angular/index.html) ### Konva 与 Fabric.js——应选择哪一个? 两者都是 2D Canvas 框架,但在以下主要方面有所不同: - **框架支持**:Konva 提供 React、Vue、Svelte 和 Angular 的官方绑定。Fabric.js 没有官方框架绑定。 - **架构**:Konva 使用多图层方法(每个 Layer 都是独立的 Canvas)来提高渲染性能。Fabric.js 使用单个 Canvas。 - **拖放**:两者均内置拖放功能。Konva 的事件系统支持事件冒泡和委托。 - **TypeScript**:两者均附带 TypeScript 定义。 对于交互式应用程序,尤其是使用 React、Vue 或 Svelte 的应用程序,请选择 Konva。如果需要 Fabric.js 特有的图像操作功能,请选择 Fabric.js。 [阅读完整比较 →](/zh-Hans/docs/guides/best-canvas-library.html) ### Konva 与 PixiJS——应选择哪一个? 它们适用于不同用途: - **Konva** 使用 2D Canvas API 和面向对象的方法。它最适合交互式应用程序、UI 元素、设计编辑器和标注工具。 - **PixiJS** 使用 WebGL 进行 GPU 加速渲染。它最适合 2D 游戏、包含数千个移动精灵的动画和高帧率图形。 如果构建包含 UI 交互(单击、拖动、调整大小和悬停)的应用程序,请选择 Konva。如果构建包含许多动画精灵的游戏,请选择 PixiJS。 [阅读完整比较 →](/zh-Hans/docs/guides/best-canvas-library.html) ### Konva 支持 TypeScript 吗? 支持。Konva 附带内置 TypeScript 类型定义,无需额外的 `@types` 包。只需安装 `konva`,TypeScript 就会自动识别这些类型: ```bash npm install konva ``` 这也适用于 `react-konva`、`vue-konva` 和其他官方绑定。 ### Konva 可以处理数千个图形吗? 可以,但需要适当优化。主要技术包括: 1. **图层管理** — 使用多个图层分隔静态内容和动态内容 2. **图形缓存** — 使用 `shape.cache()` 缓存复杂图形,将其渲染为图像 3. **禁用监听** — 对不需要事件的图形设置 `listening: false` 4. **交给 Konva 重绘** — 自 Konva 8 起重绘是自动且批量的,因此在修改之后调用 `layer.draw()` 和 `layer.batchDraw()` [并无必要](/zh-Hans/docs/performance/Batch_Draw.html) 5. **禁用完美绘制** — 对同时具有填充和描边的图形设置 `perfectDrawEnabled: false` Konva 提供渲染 [10,000 个图形](/zh-Hans/docs/sandbox/10000_Shapes_with_Tooltip.html)和 [20,000 个节点](/zh-Hans/docs/sandbox/20000_Nodes.html)的示例。 [阅读所有性能提示 →](/zh-Hans/docs/performance/All_Performance_Tips.html) ### Konva 可以在移动设备上运行吗? 可以。Konva 完整支持移动浏览器,包括: - 触摸事件:`tap`、`dbltap`、`touchstart`、`touchmove`、`touchend` - 多点触控手势(捏合缩放、双指旋转) - 响应式 Canvas 尺寸 - 基于触摸的拖放 [阅读移动事件教程 →](/zh-Hans/docs/events/Mobile_Events.html) ### 可以结合 Node.js 使用 Konva 吗? 可以。Konva 支持使用 [`canvas`](https://www.npmjs.com/package/canvas) npm 包进行服务器端渲染: ```bash npm install konva canvas ``` 这样可以在服务器上生成图像和创建缩略图,或在没有浏览器的情况下在 Node.js 中执行 Canvas 操作。 [阅读 Node.js 教程 →](/zh-Hans/docs/nodejs/nodejs-setup) ### 如何将 Canvas 导出为图像或 PDF? **图像导出:** ```javascript const dataURL = stage.toDataURL({ pixelRatio: 2 }); // PNG by default const jpegURL = stage.toDataURL({ mimeType: 'image/jpeg', quality: 0.8 }); ``` 可以使用 jsPDF 等第三方库进行 **PDF 导出**。请参阅 [Canvas 转 PDF 示例](/zh-Hans/docs/sandbox/Canvas_to_PDF.html)。 [阅读导出教程 →](/zh-Hans/docs/data_and_serialization/High-Quality-Export.html) ### 如何实现拖放? 为任意图形设置 `draggable: true`: ```javascript const rect = new Konva.Rect({ x: 50, y: 50, width: 100, height: 100, fill: 'red', draggable: true, }); ``` Konva 支持拖动边界、网格吸附、放置事件以及图层之间的拖放。 [阅读拖放教程 →](/zh-Hans/docs/drag_and_drop/Drag_and_Drop.html) ### Konva 仍在积极维护吗? 是的。Konva 仍在积极维护,并定期发布版本。查看[更新日志](https://github.com/konvajs/konva/blob/master/CHANGELOG.md)以了解近期更新,查看 [GitHub 仓库](https://github.com/konvajs/konva)以了解持续的开发活动。Konva 由 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=faq-maintained) 团队维护。Polotno 是基于 Konva 的商业设计编辑器 SDK,并为 Konva 的持续开发提供资助。 --- # 关于 Konva.js——开源 HTML5 Canvas JavaScript 框架 > Konva.js 是 Anton Lavrenov 于 2015 年创建的开源 2D HTML5 Canvas JavaScript 框架,采用 MIT 许可证。了解它的历史、功能、应用情况和生态系统。 Source: https://konvajs.org/zh-Hans/docs/about.html ## 关于 Konva.js Konva.js 是一个开源 2D HTML5 Canvas JavaScript 框架。它为交互式 Canvas 应用程序提供面向对象的 API。它支持图形、动画、事件、拖放、滤镜、序列化和高质量导出。Konva 集成了 React、Vue、Svelte 和 Angular。它采用 MIT 许可证,并从 2015 年起持续维护。 ## 主要信息 | | | |---|---| | **创建时间** | 2015 年(从 KineticJS 分支而来,KineticJS 始于 2012 年) | | **创建者** | Anton Lavrenov | | **许可证** | MIT(可免费用于商业和个人用途) | | **语言** | JavaScript 和 TypeScript(内置类型定义) | | **npm 包** | [`konva`](https://www.npmjs.com/package/konva) | | **GitHub** | [github.com/konvajs/konva](https://github.com/konvajs/konva) | | **网站** | [konvajs.org](https://konvajs.org) | | **社区** | [Discord](https://discord.gg/8FqZwVT)、[Stack Overflow(`konvajs`)](https://stackoverflow.com/questions/tagged/konvajs) | ## 框架集成 Konva 为所有主流 JavaScript 框架提供官方绑定: | 框架 | 包 | 安装命令 | |-----------|---------|---------| | React | [`react-konva`](https://github.com/konvajs/react-konva) | `npm install react-konva konva` | | Vue | [`vue-konva`](https://github.com/konvajs/vue-konva) | `npm install vue-konva konva` | | Svelte | [`svelte-konva`](https://github.com/konvajs/svelte-konva) | `npm install svelte-konva konva` | | Angular | [`ng2-konva`](https://github.com/konvajs/ng2-konva) | `npm install ng2-konva konva` | ## 谁在使用 Konva 全球各地的团队都在使用 Konva,其中包括: - **Meta** — Facebook/Instagram - **Microsoft** - **Labelbox** — AI 数据标注平台 - **Zazzle** — 定制产品设计 - **Polotno** — 基于 Konva 构建的设计编辑器 SDK 在公开的 `package.json` 中声明使用 Konva 的开源项目: - **[peaks.js](https://github.com/bbc/peaks.js)** —— BBC 的音频波形编辑器 - **[Label Studio](https://github.com/HumanSignal/label-studio)** —— 数据标注平台 - **[Weave.js](https://github.com/InditexTech/weavejs)** —— Inditex 的协作画布 - **[DWV](https://github.com/ivmartel/dwv)** —— DICOM 医学影像查看器 此外,还有数千家公司和个人开发者使用 Konva 构建设计编辑器、标注工具、白板应用程序、交互式地图、数据可视化、游戏等产品。 ## 架构 Konva 使用分层节点结构: ``` Stage (one per canvas area) └── Layer (each layer is a separate element) └── Group (optional, for organizing shapes) └── Shape (Rect, Circle, Text, Image, Line, etc.) ``` - **Stage**:附加到 DOM 元素的根容器。包含一个或多个 Layer。 - **Layer**:每个 Layer 都是独立的 `` 元素,拥有自己的场景 Canvas 和命中检测 Canvas。使用多个 Layer 可以优化渲染。 - **Group**:可选容器,用于组织和统一变换多个 Shape。 - **Shape**:可视元素,包括 Rect、Circle、Ellipse、Line、Arrow、Text、Image、Path、Star、Ring、Arc、RegularPolygon、Wedge、Sprite、TextPath、Label 和自定义图形。 ## 核心功能 - **图形**:Rect、Circle、Ellipse、Line、Arrow、Arc、Ring、Wedge、Star、RegularPolygon、Path、Text、TextPath、Image、Sprite、Label 和自定义图形 - **事件系统**:单击、双击、mouseover、mouseout、touchstart、touchmove、tap 和拖动事件,支持冒泡和委托 - **拖放**:内置拖放功能,支持边界、吸附和放置事件 - **动画**:通过 `Konva.Animation` 实现基于帧的动画,通过 `Konva.Tween` 和 30 多种缓动函数实现属性补间 - **滤镜**:Blur、Brightness、Contrast、Grayscale、HSL、Invert、Noise、Pixelate、Sepia、Threshold 和自定义滤镜 - **序列化**:使用 `toJSON()` 和 `Konva.Node.create()` 保存并恢复节点树及其可序列化属性。图像、事件处理函数和自定义绘制函数必须单独恢复。 - **导出**:使用 `toDataURL()` 和 `toBlob()` 进行高质量图像导出(PNG、JPEG),通过第三方库导出 PDF - **选择与变换**:使用内置 `Transformer` 进行交互式调整大小、旋转和缩放 - **性能**:基于图层的渲染、图形缓存和优化 API,可处理数千个图形 - **跨平台**:支持完整触摸事件,可在桌面和移动浏览器上运行 - **Node.js**:通过 `canvas` npm 包进行服务器端 Canvas 渲染 - **TypeScript**:内置 TypeScript 类型定义 ## 链接 - [入门教程](/zh-Hans/docs/index.html) - [为什么选择 Konva?——何时使用 Konva](/zh-Hans/docs/guides/why-konva.html) - [API 参考](/zh-Hans/api/Konva.html) - [交互式示例](/zh-Hans/docs/sandbox.html) - [FAQ——常见问题](/zh-Hans/docs/faq.html) - [最佳 Canvas 库——比较指南](/zh-Hans/docs/guides/best-canvas-library.html) - [GitHub 仓库](https://github.com/konvajs/konva) - [npm 包](https://www.npmjs.com/package/konva) - [更新日志](https://github.com/konvajs/konva/blob/master/CHANGELOG.md) - [Discord 社区](https://discord.gg/8FqZwVT) - [Stack Overflow](https://stackoverflow.com/questions/tagged/konvajs) ## “Made with Konva”徽章 将此徽章添加到项目 README 中,以表明项目使用 Konva 构建: ```markdown [![Made with Konva](https://img.shields.io/badge/Made%20with-Konva-blue)](https://konvajs.org) ``` [![使用 Konva 构建](https://img.shields.io/badge/Made%20with-Konva-blue)](https://konvajs.org) --- # 为什么选择 Konva?何时在项目中使用 Konva.js > 何时应使用 Konva.js?了解 Konva 解决的问题、不适合的任务、包含实际示例的理想使用场景,以及何时选择其他库。 Source: https://konvajs.org/zh-Hans/docs/guides/why-konva.html ## 为什么选择 Konva? Konva.js 解决一个特定问题:**在 HTML5 Canvas 上构建交互式 2D 图形**。如果用户需要在 Canvas 上单击、拖动、调整图形大小或操作图形,Konva 会提供所有这些功能。 ## Konva 解决哪些问题 HTML5 Canvas API 属于底层 API。它只提供绘图表面,不提供对象、图形事件或拖放。绘制像素后,Canvas 会立即忘记绘制的内容。 Konva 补充了这些缺失的功能: - **对象模型** — 每个图形都是 JavaScript 对象。可以单独移动、隐藏图形,为图形设置动画,或销毁图形。 - **事件系统** — 单击矩形、将鼠标悬停在圆形上或拖动组。事件会像 DOM 事件一样,从图形通过组和图层冒泡。 - **拖放** — 为任意图形设置 `draggable: true` 即可。可以根据需要添加边界、吸附和放置区域。 - **选择与变换** — 内置 `Transformer` 可为任意图形添加调整大小和旋转控制手柄。 - **序列化** — 将节点树及其可序列化属性保存为 JSON。图像、事件处理函数和自定义绘制函数必须单独恢复。 - **多图层架构** — 每个 Layer 都是独立的 `` 元素。交互式图形发生变化时,静态背景不会重新渲染。 - **框架集成** — 为 React(`react-konva`)、Vue(`vue-konva`)、Svelte(`svelte-konva`)和 Angular(`ng2-konva`)提供官方绑定。 ## 理想使用场景 当应用程序需要**可由用户操作的交互式 Canvas 图形**时,Konva 是合适的选择: - **设计编辑器** — 用户可在其中放置、移动对象,调整对象大小并设置对象样式的 Canva 类工具([示例](/zh-Hans/docs/sandbox/Canvas_Editor.html)) - **标注工具** — 在图像上绘制包围盒、多边形或标记,用于 ML 训练或审核([示例](/zh-Hans/docs/sandbox/Image_Labeling.html)) - **绘图或白板应用程序** — 手绘、图形和协作式 Canvas([示例](/zh-Hans/docs/sandbox/Free_Drawing.html)) - **交互式图表** — 包含可拖动连接节点的流程图、组织结构图和网络图([示例](/zh-Hans/docs/sandbox/Connected_Objects.html)) - **座位图和平面图** — 用户通过单击区域进行选择或预订的交互式地图([示例](/zh-Hans/docs/sandbox/Seats_Reservation.html)) - **数据可视化仪表板** — 图表库无法提供的自定义可视化,包含交互式工具提示和点击跳转功能 - **表单构建器和配置器** — 拖放布局编辑器和产品配置器([示例](/zh-Hans/docs/sandbox/Window_Frame_Designer.html)) 如果设计编辑器只是产品的一项功能,而不是产品本身,可以使用 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=why-konva)。它是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK,提供模板、文本编辑和导出功能。你可以直接集成编辑器,而不必自行构建。[Canvas 编辑器示例](/zh-Hans/docs/sandbox/Canvas_Editor.html#自建还是集成)介绍了何时适合自行构建,何时适合集成现成编辑器。 ## Konva 不适合哪些任务 Konva 是一个专用工具,不会尝试完成所有任务: - **不是游戏引擎** — Konva 使用 Canvas 2D,而不是 WebGL。对于以 60fps 显示数千个动画精灵的 2D 游戏,请使用 [PixiJS](https://pixijs.com/)。Konva 可以处理简单游戏,但它针对交互式应用程序而不是游戏循环进行了优化。 - **不是 3D 库** — 对于 3D 图形,请使用 Three.js 或 Babylon.js。 - **不是图表库** — 对于标准图表(柱状图、折线图和饼图),请使用 Chart.js、D3 或 Recharts。如果需要图表库无法提供的**自定义交互式可视化**,请使用 Konva。 - **不是 SVG 库** — Konva 渲染到 Canvas,而不是 SVG。它可以[把 SVG 绘制到 Canvas 上](/zh-Hans/docs/sandbox/SVG_On_Canvas.html),但无法导出 SVG。如果需要 SVG 输出,可以考虑 Fabric.js 或 Paper.js。 - **不是 CSS 的替代品** — 如果可以使用 HTML/CSS 构建 UI,请不要使用 Canvas。Canvas 适合 HTML 无法处理的图形,例如自由形状、像素级操作和复杂的分层视觉效果。 ## 何时使用其他工具 我们建议为任务选择合适的工具: | 如果需要... | 改用 | |---|---| | 具有 WebGL 性能的 2D 游戏 | [PixiJS](https://pixijs.com/) | | SVG 导出 | [Fabric.js](https://fabricjs.com/) | | 矢量图形和贝塞尔数学 | [Paper.js](https://paperjs.org/) | | 创意编程和生成艺术 | [p5.js](https://p5js.org/) | | 3D 图形 | [Three.js](https://threejs.org/) | | 标准图表 | [Chart.js](https://www.chartjs.org/) 或 [D3](https://d3js.org/) | 有关完整比较,请参阅[最佳 JavaScript Canvas 库](/zh-Hans/docs/guides/best-canvas-library.html)。 ## 开发者为何选择 Konva 而不是其他工具 与其他 Canvas 2D 框架相比: 1. **下载量最高** — npm 上下载量最高的 Canvas 2D 框架。 2. **最佳框架支持** — 唯一为 React、Vue、Svelte 和 Angular 提供官方绑定的 Canvas 库。`react-konva` 是下载量最高的 React Canvas 库。 3. **多图层渲染** — 其他框架使用单个 Canvas。Konva 的多图层方法可以为复杂应用程序提供更好的性能。 4. **完整的交互系统** — 拖放、Transformer(调整大小和旋转控制手柄)、事件冒泡和命中检测均为内置功能。使用其他工具时,需要从头构建这些功能。 5. **MIT 许可证** — 可免费用于商业用途,没有限制。 6. **持续维护** — 定期发布版本,并及时处理问题。请参阅[更新日志](https://github.com/konvajs/konva/blob/master/CHANGELOG.md)。 ## 开始使用 - [安装和快速示例](/zh-Hans/docs/index.html) - [框架概览](/zh-Hans/docs/overview.html) - [React 教程](/zh-Hans/docs/react/index.html) - [60 多个交互式示例](/zh-Hans/docs/sandbox.html) --- # 2026 年最佳 JavaScript Canvas 库——如何选择 > 如何选择 JavaScript Canvas 库。由 Konva 作者编写的快速决策指南,比较 Konva.js、Fabric.js、PixiJS、Paper.js 和 p5.js。 Source: https://konvajs.org/zh-Hans/docs/guides/best-canvas-library.html ## 如何选择 JavaScript Canvas 库 我是 Konva.js 的创建者 [Anton Lavrenov](https://lavrton.com)。我的观点难免有所偏向,但我会坦诚说明,包括何时不应使用 Konva。 目前有多种流行的 Canvas 库。它们表面上相似,但为不同任务而设计。选择与使用场景匹配的库,可以避免许多问题。 ### 要构建交互式应用程序? 设计编辑器、白板、标注工具、图表、座位图和仪表板,即任何需要用户在 Canvas 上单击、拖动和调整对象大小的应用程序。 **使用 [Konva.js](/zh-Hans/docs/index.html)。** 它提供对象模型、支持冒泡的事件系统、拖放、调整大小和旋转控制手柄(`Transformer`)、序列化,以及 React、Vue、Svelte 和 Angular 的官方绑定。Konva 正是为此类应用程序构建的。 ### 要构建 2D 游戏? **使用 [PixiJS](https://pixijs.com/)。** 它是一个使用 GPU 加速的 WebGL 渲染引擎,专为包含许多移动对象的高帧率场景构建。Konva 使用 Canvas 2D,在游戏工作负载中无法达到 WebGL 的性能。 ### 需要导入或导出 SVG? 如果你需要把内容写回 SVG,**请使用 [Fabric.js](https://fabricjs.com/)**。Konva 可以*渲染* SVG——[有三种方式](/zh-Hans/docs/sandbox/SVG_On_Canvas.html):`Konva.Image`、`Konva.Path` 或 canvg——但它没有 SVG 导出功能,因为它绘制到 Canvas。Fabric.js 还内置绘图画笔,并侧重于图像编辑。 ### 要进行创意编程或生成艺术? 对于创意草图和教育项目,**使用 [p5.js](https://p5js.org/)**。如果需要矢量数学、贝塞尔曲线和路径布尔运算,请使用 **[Paper.js](https://paperjs.org/)**。 ### 图表与数据驱动的图形? **使用 [D3](https://d3js.org/)。** D3 不是渲染器——它是一套数据绑定与布局工具,通常驱动 SVG。当难点在于*数据*时就用它:比例尺、坐标轴、力导向布局、地理投影、数据集之间的过渡。 两者并不互斥。由 D3 计算位置、由 Konva 负责绘制,这是图表元素多到 SVG 难以在 DOM 中承载之后的常见组合。如果你的图表基本是静态的、节点数在几千以内,直接用 D3 加 SVG 会更简单。 ### 白板或图示类产品? **请先看看 [tldraw](https://tldraw.dev/) 或 [Excalidraw](https://excalidraw.com/)。** 它们提供的是一个完整的白板——工具、撤销、多人协作、导出——你只需集成。这省下大量工作;如果它们的外观和行为适合你的产品,就直接采用。 当你需要掌控模型时,才在 Konva 上自己构建:你自己的图形类型、你自己的持久化格式、你自己的编辑规则,或者一个其实并不是白板的画布。你选择的是一条更长的路,换来的是没有上限。如果你想要开箱即用的节点与连线,[React Flow](https://reactflow.dev/) 也适用同样的取舍。 ### 设计编辑器产品? **请先看看 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=best-library)。** 它提供完整的 Canva 类编辑器,包括模板、文本编辑、图像工具、侧边面板、历史记录,以及 PNG、PDF 和视频导出,你只需将其集成到应用中。Polotno 是由 Konva 维护者基于 Konva 开发的商业软件,因此其底层画布使用的正是本页介绍的 Konva。 如果你需要掌控文档模型、编辑器本身就是产品,或者你构建的并非设计编辑器,请直接使用 Konva。[Canvas 编辑器示例](/zh-Hans/docs/sandbox/Canvas_Editor.html)可以作为自行构建的起点。 ### 仍不确定? 如果你正在阅读此页面,可能正在构建包含交互式图形的 Web 应用程序。这正是 Konva 擅长的领域。[尝试入门指南](/zh-Hans/docs/index.html),10 分钟内即可了解它是否合适。 ## 数据 下载量是现有最不含糊的信号,因此这里列出截至 2026 年 8 月的数据。它们说明哪些库被广泛使用,而不是哪一个适合你。 | | npm 月下载量 | 渲染方式 | 框架绑定 | | --- | ---: | --- | --- | | **Konva** | 10.1M | Canvas 2D | React、Vue、Svelte、Angular | | PixiJS | 3.8M | WebGL / WebGPU | 仅社区维护 | | Fabric.js | 3.7M | Canvas 2D | 仅社区维护 | | Paper.js | 0.8M | Canvas 2D | 无 | *最后核对时间:2026 年 8 月。* ## Konva 的不同之处 - **框架支持** — 为 React(`react-konva`)、Vue、Svelte 和 Angular 提供官方绑定。其他 Canvas 库均未提供这些绑定。 - **多图层渲染** — 每个 Layer 都是独立的 ``。交互式图形移动时,静态内容不会重新渲染。 - **内置交互** — 拖放、调整大小和旋转控制手柄、事件冒泡以及命中检测均为内置功能。使用其他库时,需要从头构建这些功能。 - **序列化** — `stage.toJSON()` 保存节点树及其可序列化属性,`Konva.Node.create(json)` 恢复这些内容。图像、事件处理函数和自定义绘制函数必须单独恢复。 - **作者主导** — 我维护 Konva 已有 10 多年。API 保持一致,决策迅速,并且每个 PR 都由我亲自评审。 ## 延伸阅读 - [为什么选择 Konva?——何时使用(以及何时不使用)Konva](/zh-Hans/docs/guides/why-konva.html) - [npm 下载趋势:fabric、konva 与 pixi.js](https://npmtrends.com/fabric-vs-konva-vs-pixi.js) - [Canvas 引擎性能基准测试](https://benchmarks.slaylines.io/)——请注意,它锁定了 2021 年 9 月的 Konva 8.1.4 和 PixiJS 6.1.3,因此其数据无法反映两者的当前版本。 --- # Canvas 编辑器 — 使用 JavaScript 和 HTML5 Canvas 构建设计编辑器 > 使用 Konva 和 React 在 HTML5 Canvas 上构建设计编辑器。选择、缩放手柄、撤销与重做、PNG 导出 — 以及对生产级编辑器还需要什么的诚实说明。 Source: https://konvajs.org/zh-Hans/docs/sandbox/Canvas_Editor.html 设计编辑器让人们在页面上排布文字、图片和图形并导出结果 — 这是 Canva、Figma 以及每一个 社交媒体图形工具背后的模式。Konva 很适合作为它的基础:它在 Canvas 之上提供对象模型、 命中检测、事件、拖放和缩放手柄。 本页构建一个可用的编辑器,包含选择、移动、缩放控件、撤销、重做和 PNG 导出。最后会诚实 说明它与产品之间还差什么。 ## 看一个成品编辑器 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=canvas-editor) 是一个基于 Konva 构建的商业设计编辑器 SDK。它最清楚地展示了这个模式能扩展到多远, 因此在阅读任何代码之前,值得先花一分钟看看: [Open the interactive demo](https://polotno.com/docs/examples/polotno-demo/index.html?utm_source=konvajs&utm_medium=docs&utm_content=canvas-editor-embed) 你刚刚用到的一切 — 工具栏、侧边面板、模板、字体和导出 — 都建立在下面介绍的同一套 Konva 基础之上。 ## 自己动手构建一个 **操作说明:** 在画布上或对象列表中点击一个对象。拖动它,或使用缩放手柄。添加对象、 撤销,然后导出结果。 ```jsx import React from 'react'; import { Stage, Layer, Rect, Circle, Star, Text, Transformer } from 'react-konva'; const WIDTH = 760; const HEIGHT = 420; const initialShapes = [ { id: 'card', type: 'rect', x: 60, y: 60, width: 300, height: 300, fill: '#1e3a8a', cornerRadius: 16 }, { id: 'accent', type: 'circle', x: 470, y: 150, radius: 74, fill: '#f59e0b' }, { id: 'badge', type: 'star', x: 620, y: 300, numPoints: 5, innerRadius: 26, outerRadius: 58, fill: '#ec4899' }, { id: 'headline', type: 'text', x: 92, y: 120, text: 'Spring\nSale', fontSize: 58, fontStyle: 'bold', lineHeight: 1.1, fill: '#ffffff' }, { id: 'caption', type: 'text', x: 92, y: 268, text: 'up to 40% off', fontSize: 22, fill: '#bfdbfe' }, ]; function useResponsiveWidth(maxWidth) { const containerRef = React.useRef(null); const [width, setWidth] = React.useState(1); React.useEffect(() => { const container = containerRef.current; if (!container) return; const update = () => setWidth(Math.max(1, Math.min(maxWidth, container.clientWidth))); update(); const observer = new ResizeObserver(update); observer.observe(container); return () => observer.disconnect(); }, [maxWidth]); return { containerRef, width, scale: width / maxWidth }; } // One renderer per shape type. The document stores plain data, never Konva nodes. function EditableShape({ shape, selected, onSelect, onCommit }) { const shapeRef = React.useRef(null); const transformerRef = React.useRef(null); React.useEffect(() => { if (selected && shapeRef.current && transformerRef.current) { transformerRef.current.nodes([shapeRef.current]); transformerRef.current.getLayer().batchDraw(); } }, [selected]); // The Transformer resizes by changing node scale. Fold that scale back into // the shape's own size properties so the saved document stays readable. const handleTransformEnd = () => { const node = shapeRef.current; const scaleX = node.scaleX(); const scaleY = node.scaleY(); const average = (scaleX + scaleY) / 2; node.scaleX(1); node.scaleY(1); const base = { ...shape, x: node.x(), y: node.y(), rotation: node.rotation() }; if (shape.type === 'rect') { onCommit({ ...base, width: Math.max(20, node.width() * scaleX), height: Math.max(20, node.height() * scaleY), }); } else if (shape.type === 'circle') { onCommit({ ...base, radius: Math.max(10, shape.radius * average) }); } else if (shape.type === 'star') { onCommit({ ...base, innerRadius: Math.max(6, shape.innerRadius * average), outerRadius: Math.max(12, shape.outerRadius * average), }); } else { onCommit({ ...base, fontSize: Math.max(8, shape.fontSize * average) }); } }; const common = { ref: shapeRef, draggable: true, onClick: onSelect, onTap: onSelect, onDragEnd: (event) => onCommit({ ...shape, x: event.target.x(), y: event.target.y() }), onTransformEnd: handleTransformEnd, }; const { id, type, ...props } = shape; return ( <> {type === 'rect' && } {type === 'circle' && } {type === 'star' && } {type === 'text' && } {selected && ( newBox.width < 20 || newBox.height < 20 ? oldBox : newBox } /> )} ); } export default function App() { const stageRef = React.useRef(null); const nextId = React.useRef(1); const { containerRef, width: displayWidth, scale } = useResponsiveWidth(WIDTH); const [selectedId, setSelectedId] = React.useState('headline'); const [history, setHistory] = React.useState({ past: [], present: initialShapes, future: [], }); // One history entry per finished action — never one per pointer move. const commit = (nextShapes) => setHistory((current) => ({ past: [...current.past, current.present], present: nextShapes, future: [], })); const updateShape = (next) => commit(history.present.map((s) => (s.id === next.id ? next : s))); const addShape = (type) => { const id = `${type}-${nextId.current++}`; const offset = history.present.length * 14; const presets = { rect: { width: 150, height: 100, fill: '#10b981', cornerRadius: 10 }, circle: { radius: 52, fill: '#8b5cf6' }, text: { text: 'Double-click to retype', fontSize: 24, fill: '#0f172a' }, }; commit([ ...history.present, { id, type, x: 160 + offset, y: 150 + offset, ...presets[type] }, ]); setSelectedId(id); }; const undo = () => setHistory((c) => c.past.length === 0 ? c : { past: c.past.slice(0, -1), present: c.past[c.past.length - 1], future: [c.present, ...c.future], } ); const redo = () => setHistory((c) => c.future.length === 0 ? c : { past: [...c.past, c.present], present: c.future[0], future: c.future.slice(1), } ); // Hide the selection handles so they never appear in the exported file. const exportPng = () => { const stage = stageRef.current; const transformers = stage.find('Transformer'); transformers.forEach((t) => t.hide()); let dataUrl; try { dataUrl = stage.toDataURL({ pixelRatio: 2 / scale }); } finally { transformers.forEach((t) => t.show()); stage.batchDraw(); } const link = document.createElement('a'); link.download = 'design.png'; link.href = dataUrl; link.click(); }; const button = { padding: '6px 12px', border: '1px solid #cbd5e1', borderRadius: 6, background: '#fff', cursor: 'pointer', }; return (
{ if (e.target === e.target.getStage()) setSelectedId(null); }} onTouchStart={(e) => { if (e.target === e.target.getStage()) setSelectedId(null); }} > {history.present.map((shape) => ( setSelectedId(shape.id)} onCommit={updateShape} /> ))}
{/* Canvas pixels mean nothing to a screen reader. Mirror the document in HTML. */}

Objects:

{history.present.map((shape) => ( ))}
); } ``` ## 让文档数据与 Konva 节点分离 `history.present` 数组就是文档。每个对象都有稳定的 ID 和普通的可序列化属性。Konva 节点只是一个交互表面 — 示例在拖动或变换*结束*时读取它的值,而不是在过程中。 `Transformer` 通过改变节点的 **scale** 来缩放。如果你保存这个 scale,文档里就会填满 不断累积的乘数。示例在提交前把 scale 折算回 width、height、radius 或 fontSize,因此 保存的文件保持可读,并且独立于 Konva 内部实现。 正是这种分离让之后的历史记录、协作和服务端渲染成为可能。如果早期做错了,这些都会变得 非常昂贵。 ## 定义历史记录的边界 每个完成的用户操作保存一条记录,而不是每个指针事件一条。把图形拖过画布是*一*步撤销, 不是两百步。 示例在拖动或变换结束时提交。对于文字,在用户确认编辑时提交;对于颜色控件,在控件关闭时 提交。相关示例:[画布撤销与重做](/zh-Hans/docs/posts/canvas-undo-redo.html)。 ## 处理坐标与导出 这个编辑器没有相机变换,因此文档坐标与舞台坐标一致。一旦添加平移或缩放,你就必须用舞台 变换的逆矩阵转换指针位置 — 参见 [相对于指针缩放](/zh-Hans/docs/sandbox/Zooming_Relative_To_Pointer.html)和 [无限画布](/zh-Hans/docs/sandbox/Infinite_Canvas.html)。 预览会缩放到容器大小,因此导出时用该缩放值去除 `pixelRatio`,无论屏幕大小都能得到稳定的 1520 × 840 图像。它还会先隐藏 Transformer,因此选择手柄绝不会进入文件。 来自其他源的图片需要正确的 CORS 响应头,否则浏览器会污染画布并阻止导出。参见 [高质量导出](/zh-Hans/docs/data_and_serialization/High-Quality-Export.html)。 ## 添加无障碍界面 Canvas 是单个元素。辅助技术看不到其中的对象、选择状态或结构。用户能用指针做的每件事, 都需要一个 HTML 等价物。 示例把文档镜像成一组按钮。生产级编辑器还需要键盘移动、合理的焦点顺序,以及选择变化时的 播报。把文字编辑保留在真正的 `input` 或 `textarea` 中 — 原生输入框提供 canvas 无法提供的 选择、光标移动和输入法支持。参见[可编辑文字](/zh-Hans/docs/sandbox/Editable_Text.html)。 ## 为生产规模做规划 当文档包含很多对象时,使用归一化的实体映射,对组件做记忆化,并只重新渲染发生变化的部分。 绝不要把图片数据放进历史记录数组 — 存储资源引用,并在文档模型之外管理该资源。 示例省略了持久化、复制粘贴、编组、对齐和并发编辑。在构建界面*之前*,先把每一项加入 文档模型。 ## 自建还是集成 上面的示例是一个真正的编辑器,同时也如实衡量了还有多远的路要走。Konva 提供渲染、命中 测试、事件和 Transformer。产品级设计编辑器还需要: - 文字引擎,支持重排、逐字符样式和 Web 字体度量 - 字体加载与回退,并且在导出时仍然一致 - 模板、多页面和素材管理 - 能对操作分组并在刷新后保留的历史记录 - 导出流水线,包括超出浏览器画布尺寸上限的尺寸和适合印刷的输出 这份清单通常需要数月工作,而且其中大部分并不是 canvas 工作。 当编辑器*就是*你的产品、当你的文档模型不同寻常、或者当你需要完全控制输出时,就自己构建。 当编辑器只是支撑你真正销售的东西时,就集成一个 SDK。Polotno 覆盖了上面的清单;它是付费 软件,并且对文档模型有自己的主张,因此在决定采用前请阅读它的文档。 如果你读到这里仍然想自己构建,上面的示例就是正确的起点。继续阅读 [变换器](/zh-Hans/docs/select_and_transform/Basic_demo.html)、 [对象吸附](/zh-Hans/docs/sandbox/Objects_Snapping.html)和 [自由绘制](/zh-Hans/docs/sandbox/Free_Drawing.html)。 --- # 带缩放和平移的无限 Canvas——使用 JavaScript 构建 > 使用 JavaScript 和 Konva.js 构建带缩放、平移和网格背景的无限 Canvas。滚动以缩放,拖动以平移。包含 React、Vue 和原生 JS 交互式示例。 Source: https://konvajs.org/zh-Hans/docs/sandbox/Infinite_Canvas.html 无限 Canvas 让用户可以在无边界工作区中自由平移和缩放,它是 Figma、Miro 和 tldraw 等工具的基础。此示例展示如何使用 Konva 构建无限 Canvas:滚动时以光标为中心缩放,拖动空白区域进行平移,并通过拖动移动图形。 **操作说明:**滚动以缩放。拖动背景以平移。拖动图形以移动它们。 ```js import Konva from 'konva'; var width = window.innerWidth; var height = window.innerHeight; var stage = new Konva.Stage({ container: 'container', width: width, height: height, draggable: true, }); var layer = new Konva.Layer(); stage.add(layer); // Generate grid of dots var gridSpacing = 40; var gridRange = 2000; for (var x = -gridRange; x <= gridRange; x += gridSpacing) { for (var y = -gridRange; y <= gridRange; y += gridSpacing) { layer.add(new Konva.Circle({ x: x, y: y, radius: 1, fill: '#ccc', listening: false, })); } } // Scatter colorful shapes across the space var shapeDefs = [ { type: 'rect', x: 80, y: 60, width: 120, height: 80, fill: '#FF6B6B', rotation: 5 }, { type: 'circle', x: 350, y: 120, radius: 50, fill: '#4ECDC4' }, { type: 'rect', x: 600, y: -80, width: 90, height: 90, fill: '#45B7D1', rotation: -10 }, { type: 'star', x: -150, y: 250, numPoints: 5, innerRadius: 20, outerRadius: 45, fill: '#FFE66D' }, { type: 'circle', x: -400, y: -200, radius: 65, fill: '#DDA0DD' }, { type: 'rect', x: 200, y: -350, width: 140, height: 60, fill: '#98D8C8', rotation: 15 }, { type: 'star', x: 500, y: 300, numPoints: 6, innerRadius: 25, outerRadius: 55, fill: '#F7DC6F' }, { type: 'circle', x: -300, y: 450, radius: 40, fill: '#82E0AA' }, { type: 'rect', x: -550, y: 100, width: 100, height: 100, fill: '#F1948A', rotation: -20 }, { type: 'star', x: 750, y: -250, numPoints: 5, innerRadius: 30, outerRadius: 60, fill: '#AED6F1' }, { type: 'circle', x: 100, y: 550, radius: 55, fill: '#D2B4DE' }, { type: 'rect', x: -100, y: -500, width: 110, height: 70, fill: '#A3E4D7', rotation: 8 }, ]; shapeDefs.forEach(function(d) { var shape; if (d.type === 'rect') { shape = new Konva.Rect({ x: d.x, y: d.y, width: d.width, height: d.height, fill: d.fill, cornerRadius: 8, rotation: d.rotation || 0, draggable: true, shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 10, shadowOffsetY: 4, }); } else if (d.type === 'circle') { shape = new Konva.Circle({ x: d.x, y: d.y, radius: d.radius, fill: d.fill, draggable: true, shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 10, shadowOffsetY: 4, }); } else { shape = new Konva.Star({ x: d.x, y: d.y, numPoints: d.numPoints, innerRadius: d.innerRadius, outerRadius: d.outerRadius, fill: d.fill, draggable: true, shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 10, shadowOffsetY: 4, }); } layer.add(shape); }); // Zoom relative to pointer var scaleBy = 1.05; stage.on('wheel', function(e) { e.evt.preventDefault(); var oldScale = stage.scaleX(); var pointer = stage.getPointerPosition(); var mousePointTo = { x: (pointer.x - stage.x()) / oldScale, y: (pointer.y - stage.y()) / oldScale, }; var direction = e.evt.deltaY > 0 ? -1 : 1; if (e.evt.ctrlKey) { direction = -direction; } var newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy; newScale = Math.max(0.1, Math.min(10, newScale)); stage.scale({ x: newScale, y: newScale }); var newPos = { x: pointer.x - mousePointTo.x * newScale, y: pointer.y - mousePointTo.y * newScale, }; stage.position(newPos); }); ``` ```js import { Stage, Layer, Rect, Circle, Star } from 'react-konva'; import { useRef, useState } from 'react'; var INITIAL_SHAPES = [ { id: 'rect-1', type: 'rect', x: 80, y: 60, width: 120, height: 80, fill: '#FF6B6B', rotation: 5 }, { id: 'circle-1', type: 'circle', x: 350, y: 120, radius: 50, fill: '#4ECDC4' }, { id: 'rect-2', type: 'rect', x: 600, y: -80, width: 90, height: 90, fill: '#45B7D1', rotation: -10 }, { id: 'star-1', type: 'star', x: -150, y: 250, numPoints: 5, innerRadius: 20, outerRadius: 45, fill: '#FFE66D' }, { id: 'circle-2', type: 'circle', x: -400, y: -200, radius: 65, fill: '#DDA0DD' }, { id: 'rect-3', type: 'rect', x: 200, y: -350, width: 140, height: 60, fill: '#98D8C8', rotation: 15 }, { id: 'star-2', type: 'star', x: 500, y: 300, numPoints: 6, innerRadius: 25, outerRadius: 55, fill: '#F7DC6F' }, { id: 'circle-3', type: 'circle', x: -300, y: 450, radius: 40, fill: '#82E0AA' }, { id: 'rect-4', type: 'rect', x: -550, y: 100, width: 100, height: 100, fill: '#F1948A', rotation: -20 }, { id: 'star-3', type: 'star', x: 750, y: -250, numPoints: 5, innerRadius: 30, outerRadius: 60, fill: '#AED6F1' }, { id: 'circle-4', type: 'circle', x: 100, y: 550, radius: 55, fill: '#D2B4DE' }, ]; // Pre-generate grid dots var gridDots = []; var spacing = 40; var range = 2000; for (var gx = -range; gx <= range; gx += spacing) { for (var gy = -range; gy <= range; gy += spacing) { gridDots.push({ x: gx, y: gy }); } } var App = function() { var stageRef = useRef(null); var [shapes, setShapes] = useState(INITIAL_SHAPES); var W = window.innerWidth; var H = window.innerHeight; var scaleBy = 1.05; var handleWheel = function(e) { e.evt.preventDefault(); var stage = stageRef.current; var oldScale = stage.scaleX(); var pointer = stage.getPointerPosition(); var mousePointTo = { x: (pointer.x - stage.x()) / oldScale, y: (pointer.y - stage.y()) / oldScale, }; var direction = e.evt.deltaY > 0 ? -1 : 1; if (e.evt.ctrlKey) { direction = -direction; } var newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy; newScale = Math.max(0.1, Math.min(10, newScale)); stage.scale({ x: newScale, y: newScale }); stage.position({ x: pointer.x - mousePointTo.x * newScale, y: pointer.y - mousePointTo.y * newScale, }); }; var handleShapeDragEnd = function(id, e) { var position = e.target.position(); setShapes(function(currentShapes) { return currentShapes.map(function(shape) { return shape.id === id ? Object.assign({}, shape, position) : shape; }); }); }; return ( {gridDots.map(function(d, i) { return ; })} {shapes.map(function(s) { if (s.type === 'rect') return ; if (s.type === 'circle') return ; return ; })} ); }; export default App; ``` ```js ``` --- # React 流程图——使用 JavaScript 在 Canvas 上构建交互式流程图和图表 > 使用 JavaScript 和 React 在 HTML5 Canvas 上构建交互式流程图。拖动节点、自动更新连接,并使用 Konva.js 和 react-konva 创建可视化工作流和图表。 Source: https://konvajs.org/zh-Hans/docs/sandbox/Connected_Objects.html 直接在 HTML5 Canvas 上构建交互式流程图和连接图。拖动节点时,连接箭头会实时跟随。这是工作流构建器、思维导图、组织结构图和可视化编程工具的常见模式。它支持原生 JavaScript、React(通过 [react-konva](/zh-Hans/docs/react/index.html))和 Vue。 **操作说明:** 拖动任意节点以调整其位置。连接会自动更新。单击“Add Node”添加更多节点。 ```js import Konva from 'konva'; var width = window.innerWidth; var height = window.innerHeight; var stage = new Konva.Stage({ container: 'container', width: width, height: height, }); var layer = new Konva.Layer(); stage.add(layer); var container = document.getElementById('container'); var controls = document.createElement('div'); controls.style.cssText = 'margin-bottom: 8px; display: flex; gap: 8px; align-items: center; font: 13px Arial, sans-serif;'; var addBtn = document.createElement('button'); addBtn.textContent = '+ Add Node'; addBtn.style.cssText = 'padding: 6px 14px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 600;'; addBtn.onclick = function() { var x = 100 + Math.random() * (width - 200); var y = 80 + Math.random() * (height - 160); var node = makeNode('New Node', '#2196F3', x, y); node.moveToTop(); }; controls.appendChild(addBtn); var hint = document.createElement('span'); hint.textContent = 'Drag nodes to reposition. Arrows follow automatically.'; hint.style.cssText = 'color: #888; font-size: 12px;'; controls.appendChild(hint); container.parentNode.insertBefore(controls, container); var nodeList = []; var arrowList = []; function makeNode(label, color, x, y) { var group = new Konva.Group({ x: x, y: y, draggable: true }); var rect = new Konva.Rect({ width: 120, height: 50, fill: color, cornerRadius: 8, shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 6, shadowOffsetY: 2, offsetX: 60, offsetY: 25, }); var text = new Konva.Text({ text: label, fontSize: 14, fontFamily: 'Arial', fill: '#fff', width: 120, height: 50, align: 'center', verticalAlign: 'middle', offsetX: 60, offsetY: 25, }); group.add(rect); group.add(text); layer.add(group); var entry = { group: group }; nodeList.push(entry); group.on('dragmove', updateArrows); return group; } function connect(from, to) { var arrow = new Konva.Arrow({ points: [from.x(), from.y(), to.x(), to.y()], pointerLength: 10, pointerWidth: 8, fill: '#555', stroke: '#555', strokeWidth: 2, listening: false, }); layer.add(arrow); arrow.moveToBottom(); arrowList.push({ shape: arrow, from: from, to: to }); } function updateArrows() { for (var i = 0; i < arrowList.length; i++) { var a = arrowList[i]; a.shape.points([a.from.x(), a.from.y(), a.to.x(), a.to.y()]); } layer.batchDraw(); } var start = makeNode('Start', '#4CAF50', 100, 120); var procA = makeNode('Process A', '#2196F3', 300, 80); var decide = makeNode('Decision', '#FF9800', 500, 120); var procB = makeNode('Process B', '#2196F3', 300, 260); var end = makeNode('End', '#f44336', 500, 260); connect(start, procA); connect(procA, decide); connect(decide, procB); connect(decide, end); connect(procB, end); ``` ```js import React, { useState, useCallback } from 'react'; import { Stage, Layer, Group, Rect, Text, Arrow } from 'react-konva'; var INITIAL_NODES = [ { id: 'start', label: 'Start', color: '#4CAF50', x: 100, y: 120 }, { id: 'procA', label: 'Process A', color: '#2196F3', x: 300, y: 80 }, { id: 'decide', label: 'Decision', color: '#FF9800', x: 500, y: 120 }, { id: 'procB', label: 'Process B', color: '#2196F3', x: 300, y: 260 }, { id: 'end', label: 'End', color: '#f44336', x: 500, y: 260 }, ]; var CONNECTIONS = [ { from: 'start', to: 'procA' }, { from: 'procA', to: 'decide' }, { from: 'decide', to: 'procB' }, { from: 'decide', to: 'end' }, { from: 'procB', to: 'end' }, ]; function FlowNode({ id, label, color, x, y, onDragMove }) { return ( ); } var App = function () { var [nodes, setNodes] = useState(INITIAL_NODES); var [connections, setConnections] = useState(CONNECTIONS); var [counter, setCounter] = useState(1); var handleDragMove = useCallback(function (id, x, y) { setNodes(function (prev) { return prev.map(function (n) { return n.id === id ? Object.assign({}, n, { x: x, y: y }) : n; }); }); }, []); var nodeMap = {}; nodes.forEach(function (n) { nodeMap[n.id] = n; }); var addNode = function () { var newId = 'new_' + counter; setCounter(counter + 1); setNodes(function (prev) { return prev.concat({ id: newId, label: 'New Node', color: '#2196F3', x: 100 + Math.random() * 400, y: 80 + Math.random() * 200, }); }); }; return (
Drag nodes to reposition. Arrows follow automatically.
{connections.map(function (c, i) { var fromNode = nodeMap[c.from]; var toNode = nodeMap[c.to]; if (!fromNode || !toNode) return null; return ( ); })} {nodes.map(function (n) { return ( ); })}
); }; export default App; ``` ```js ``` ## 设计图数据 上面的示例把每个节点保存在一处,并让每条连接只存储两个节点 ID。这种归一化比看上去 更重要:绝不要把节点位置复制到边的数据中,否则每次移动都会留下过时的副本。应该在绘制 时读取当前节点位置。 对于小图,在每个 drag-move 事件中保存位置并重新计算箭头既清晰又足够快。对于大图, 在手势过程中直接移动被拖动的节点,并在拖动结束时提交一次文档变更 — 每个操作一条历史 记录,而不是每帧一条。 本示例连接两个矩形的水平边,这对从左到右的流程图可读性很好。真实的编辑器需要显式的 **端口**:把端口 ID 存储在边上,并从拥有它的节点计算其绝对位置。在位置稳定后再计算 正交连接线的路由 — 每帧重新计算路由开销很大。 如果你添加平移和缩放,在任何命中或放置计算之前,用舞台变换的逆矩阵转换指针位置,并 让图坐标独立于相机 state。参见[相对于指针缩放](/zh-Hans/docs/sandbox/Zooming_Relative_To_Pointer.html)。 对于大图,只绘制可见节点,用空间索引处理视口和选择查询,把连接线保持在节点图形下方, 并对不接收指针输入的连接线设置 `listening: false`。参见 [性能建议](/zh-Hans/docs/performance/All_Performance_Tips.html)。 端口、边选择、环路规则、自动布局、历史记录和持久化都属于图规则。在构建界面之前,先在 数据模型中定义每一条。 --- # 如何使用 JavaScript Canvas 构建图像标注工具 > 使用 JavaScript、HTML5 Canvas 和 Konva.js 构建图像注释和标注工具。此交互式示例适用于机器学习数据标注和图像标记。 Source: https://konvajs.org/zh-Hans/docs/sandbox/Image_Labeling.html 图像注释工具用于标注机器学习训练数据、查看医学图像,以及标记照片以提供反馈。Konva.js 内置了 Canvas 拖放、图形绘制和事件处理功能,因此可以用它构建此类工具。 此示例展示使用 React 和 react-konva 构建的图像标注系统。阅读[完整案例研究](https://lavrton.com/case-study-image-labeling-for-machine-learning/),了解开发过程。 此小组件使用 `React`、`Konva` 和 `react-konva` 库构建。 此类应用不要求使用 `React`。此处选择 React,是因为使用该系统的主应用由 `React` 构建。 [Open the interactive demo](https://codesandbox.io/embed/github/konvajs/site/tree/master/react-demos/image-label-tool?hidenavigation=1&fontsize=10) --- # 如何使用 JavaScript Canvas 构建交互式楼层平面图和地图 > 使用 JavaScript、HTML5 Canvas 和 Konva.js 构建交互式建筑楼层平面图和地图。将鼠标悬停在区域上以查看说明。 Source: https://konvajs.org/zh-Hans/docs/sandbox/Interactive_Building_Map.html 交互式楼层平面图和建筑地图可用于房地产、设施管理和寻路应用。Konva.js 可绘制复杂的多边形图形,并为每个区域添加悬停检测、工具提示和自定义样式。 **操作说明:将鼠标悬停在建筑物的各个区域上以查看其说明。** ```js import Konva from 'konva'; function getData() { return { '1st Floor': { color: 'blue', points: [366, 298, 500, 284, 499, 204, 352, 183, 72, 228, 74, 274], }, '2nd Floor': { color: 'red', points: [72, 228, 73, 193, 340, 96, 498, 154, 498, 191, 341, 171], }, '3rd Floor': { color: 'yellow', points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93], }, Gym: { color: 'green', points: [498, 283, 503, 146, 560, 136, 576, 144, 576, 278, 500, 283], }, }; } function updateTooltip(tooltip, x, y, text) { tooltip.getText().text(text); tooltip.position({ x: x, y: y, }); tooltip.show(); } const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); // add background image first const imageLayer = new Konva.Layer(); stage.add(imageLayer); Konva.Image.fromURL('https://konvajs.org/assets/line-building.png', function (bgImage) { bgImage.setAttrs({ x: 1, y: 0, }); imageLayer.add(bgImage); }); const shapesLayer = new Konva.Layer(); const tooltipLayer = new Konva.Layer(); const tooltip = new Konva.Label({ opacity: 0.75, visible: false, listening: false, }); tooltip.add( new Konva.Tag({ fill: 'black', pointerDirection: 'down', pointerWidth: 10, pointerHeight: 10, lineJoin: 'round', shadowColor: 'black', shadowBlur: 10, shadowOffsetX: 10, shadowOffsetY: 10, shadowOpacity: 0.5, }) ); tooltip.add( new Konva.Text({ text: '', fontFamily: 'Calibri', fontSize: 18, padding: 5, fill: 'white', }) ); tooltipLayer.add(tooltip); // get areas data const areas = getData(); // draw areas for (const key in areas) { const area = areas[key]; const points = area.points; const shape = new Konva.Line({ points: points, fill: area.color, opacity: 0, closed: true, name: 'area', // custom attr key: key, }); shapesLayer.add(shape); } // add layers in correct order stage.add(shapesLayer); stage.add(tooltipLayer); stage.on('mouseover', (evt) => { const shape = evt.target; if (shape && shape.name() === 'area') { // only change opacity if it's an area shape shape.opacity(0.5); } }); stage.on('mouseout', (evt) => { const shape = evt.target; if (shape && shape.name() === 'area') { // only change opacity if it's an area shape shape.opacity(0); tooltip.hide(); } }); stage.on('mousemove', (evt) => { const shape = evt.target; if (shape && shape.name() === 'area') { // only change opacity if it's an area shape const mousePos = stage.getPointerPosition(); const x = mousePos.x; const y = mousePos.y - 5; updateTooltip(tooltip, x, y, shape.getAttr('key')); } }); ``` ```jsx import { Stage, Layer, Line, Label, Tag, Text, Image } from 'react-konva'; import { useState } from 'react'; import useImage from 'use-image'; const getData = () => ({ '1st Floor': { color: 'blue', points: [366, 298, 500, 284, 499, 204, 352, 183, 72, 228, 74, 274], }, '2nd Floor': { color: 'red', points: [72, 228, 73, 193, 340, 96, 498, 154, 498, 191, 341, 171], }, '3rd Floor': { color: 'yellow', points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93], }, Gym: { color: 'green', points: [498, 283, 503, 146, 560, 136, 576, 144, 576, 278, 500, 283], }, }); const App = () => { const [tooltipProps, setTooltipProps] = useState({ visible: false, x: 0, y: 0, text: '', }); const [hoveredArea, setHoveredArea] = useState(null); const [backgroundImage] = useImage('https://konvajs.org/assets/line-building.png'); const areas = getData(); const handleMouseOver = (key) => { setHoveredArea(key); }; const handleMouseOut = () => { setHoveredArea(null); setTooltipProps(prev => ({ ...prev, visible: false })); }; const handleMouseMove = (e, key) => { const stage = e.target.getStage(); const mousePos = stage.getPointerPosition(); setTooltipProps({ visible: true, x: mousePos.x, y: mousePos.y - 5, text: key, }); }; return ( {Object.entries(areas).map(([key, area]) => ( handleMouseOver(key)} onMouseOut={handleMouseOut} onMouseMove={(e) => handleMouseMove(e, key)} /> ))} ); }; export default App; ``` ```js ``` --- # 如何使用 JavaScript Canvas 构建交互式座位图 > 使用 JavaScript、HTML5 Canvas 和 Konva.js 构建交互式座位预订组件。座位图示例支持单击选择。 Source: https://konvajs.org/zh-Hans/docs/sandbox/Seats_Reservation.html 交互式座位图用于活动售票、剧院预订和场馆管理。Konva.js 很适合构建此类座位图,因为它能直接处理数百个可单击图形,并支持命中检测、悬停状态和缩放/平移。 此示例展示使用 React 和 react-konva 构建的可运行座位预订组件。有关开发过程,请阅读[完整案例研究](https://lavrton.com/case-study-seat-reservation-widget/)。 该组件使用 `React`、`Konva` 和 `react-konva` 库构建。 此类组件不要求使用 `React`。此处使用它的原因是承载该组件的主应用也使用 `React` 构建。 [Open the interactive demo](https://codesandbox.io/embed/github/konvajs/site/tree/master/react-demos/seats-reservation?hidenavigation=1&view=split&fontsize=10) --- # Canvas 图像裁剪——使用 JavaScript 裁剪和提取图像区域 > 学习使用 JavaScript 在 HTML5 Canvas 上裁剪图像。选择区域、调整裁剪区域的大小,然后使用 Konva.js 提取裁剪后的图像。包含交互式示例和源代码。 Source: https://konvajs.org/zh-Hans/docs/sandbox/Canvas_Crop_Image.html 拖动蓝色裁剪矩形以选择所需区域,使用控制柄调整其大小,然后单击按钮提取该区域。 **操作说明:** 拖动裁剪框以移动它。使用角部控制柄调整大小。单击“Crop Image”提取图像。 ```js import Konva from 'konva'; var width = window.innerWidth; var height = window.innerHeight; var stage = new Konva.Stage({ container: 'container', width: width, height: height, }); var layer = new Konva.Layer(); stage.add(layer); var container = document.getElementById('container'); var controls = document.createElement('div'); controls.style.cssText = 'margin-bottom: 8px; display: flex; gap: 10px; align-items: center; font-family: Arial, sans-serif;'; var cropBtn = document.createElement('button'); cropBtn.textContent = 'Crop Image'; cropBtn.style.cssText = 'padding: 8px 16px; font-size: 14px; background: #0088ff; color: white; border: none; border-radius: 4px; cursor: pointer;'; controls.appendChild(cropBtn); var resultContainer = document.createElement('div'); resultContainer.style.cssText = 'margin-bottom: 8px;'; container.parentNode.insertBefore(controls, container); container.parentNode.insertBefore(resultContainer, container); var imageObj = new Image(); imageObj.onload = function() { var bgImage = new Konva.Image({ image: imageObj, width: width, height: height, }); layer.add(bgImage); var cropRect = new Konva.Rect({ x: width * 0.15, y: height * 0.15, width: width * 0.35, height: height * 0.4, fill: 'rgba(0, 150, 255, 0.15)', stroke: '#0088ff', strokeWidth: 2, dash: [6, 3], draggable: true, }); layer.add(cropRect); var tr = new Konva.Transformer({ nodes: [cropRect], enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], boundBoxFunc: function(oldBox, newBox) { if (newBox.width < 20 || newBox.height < 20) return oldBox; return newBox; }, }); layer.add(tr); cropBtn.onclick = function() { var dataUrl = stage.toDataURL({ x: cropRect.x(), y: cropRect.y(), width: cropRect.width() * cropRect.scaleX(), height: cropRect.height() * cropRect.scaleY(), }); resultContainer.innerHTML = ''; var label = document.createElement('p'); label.textContent = 'Cropped result:'; label.style.cssText = 'margin: 0 0 4px 0; font-size: 13px; color: #666;'; resultContainer.appendChild(label); var img = document.createElement('img'); img.src = dataUrl; img.style.cssText = 'max-width: 100%; border: 1px solid #ddd; border-radius: 4px;'; resultContainer.appendChild(img); }; }; imageObj.src = 'https://konvajs.org/assets/landscape.jpg'; ``` ```js import { Stage, Layer, Image as KonvaImage, Rect, Transformer } from 'react-konva'; import { useRef, useState, useEffect } from 'react'; var App = function() { var stageRef = useRef(null); var cropRef = useRef(null); var trRef = useRef(null); var [imageObj, setImageObj] = useState(null); var [croppedSrc, setCroppedSrc] = useState(null); var width = window.innerWidth; var height = window.innerHeight; var [crop, setCrop] = useState({ x: width * 0.15, y: height * 0.15, width: width * 0.35, height: height * 0.4, }); useEffect(function() { var img = new window.Image(); img.onload = function() { setImageObj(img); }; img.src = 'https://konvajs.org/assets/landscape.jpg'; }, []); useEffect(function() { if (cropRef.current && trRef.current) { trRef.current.nodes([cropRef.current]); trRef.current.getLayer().batchDraw(); } }, [imageObj]); var handleCrop = function() { if (!stageRef.current || !cropRef.current) return; var node = cropRef.current; var dataUrl = stageRef.current.toDataURL({ x: node.x(), y: node.y(), width: node.width() * node.scaleX(), height: node.height() * node.scaleY(), }); setCroppedSrc(dataUrl); }; var handleTransformEnd = function() { var node = cropRef.current; if (!node) return; var scaleX = node.scaleX(); var scaleY = node.scaleY(); node.scale({ x: 1, y: 1 }); setCrop({ x: node.x(), y: node.y(), width: Math.max(20, node.width() * scaleX), height: Math.max(20, node.height() * scaleY), }); }; return (
{croppedSrc && (

Cropped result:

)} {imageObj && ( <> )}
); }; export default App; ``` ```js ``` --- # 多人白板 — 使用 Konva 和 Yjs 构建实时协作画布 > 使用 Konva、React 和 Yjs 在 HTML5 Canvas 上构建实时协作白板。了解共享文档 state、无冲突同步,以及生产环境还需要什么。 Source: https://konvajs.org/zh-Hans/docs/sandbox/Multiplayer_Whiteboard.html 协作画布 — Figma、Miro、Excalidraw — 都区分两件事:所有人共享的文档,以及每个人 看到的绘图表面。Konva 是后者。它渲染文档并处理指针交互,但它不会为你同步 state。 本示例把共享的那一半放在 [Yjs](https://yjs.dev/) 中,这是一个 CRDT 库,无需中心 权威即可合并并发编辑。`BroadcastChannel` 在标签页之间传输更新,因此你不需要服务器 就能试用真正的协作。 **操作说明:** 在两个浏览器标签页中打开本页,然后在其中一个中拖动便笺。另一个标签页 会跟随。 ## 在两个标签页中试用 ```jsx import React, { useEffect, useRef, useState } from 'react'; import { Stage, Layer, Group, Rect, Text } from 'react-konva'; import * as Y from 'yjs'; const WIDTH = 760; const HEIGHT = 400; const REMOTE_ORIGIN = Symbol('broadcast-channel'); function useResponsiveWidth(maxWidth) { const containerRef = React.useRef(null); const [width, setWidth] = React.useState(1); React.useEffect(() => { const container = containerRef.current; if (!container) return; const updateWidth = () => setWidth(Math.max(1, Math.min(maxWidth, container.clientWidth))); updateWidth(); const observer = new ResizeObserver(updateWidth); observer.observe(container); return () => observer.disconnect(); }, [maxWidth]); return { containerRef, width, scale: width / maxWidth }; } const initialNotes = [ { id: 'note-1', x: 80, y: 90, text: 'Define the problem', color: '#fde68a', updatedBy: 'initial-data' }, { id: 'note-2', x: 310, y: 190, text: 'Sketch the flow', color: '#bfdbfe', updatedBy: 'initial-data' }, { id: 'note-3', x: 540, y: 80, text: 'List the risks', color: '#fecdd3', updatedBy: 'initial-data' }, ]; export default function App() { const { containerRef, width: displayWidth, scale: displayScale, } = useResponsiveWidth(WIDTH); const [notes, setNotes] = useState(initialNotes); const [selectedId, setSelectedId] = useState('note-1'); const [channelReady, setChannelReady] = useState(false); const [clientName] = useState(() => `tab-${Math.floor(Math.random() * 900 + 100)}`); const notesMapRef = useRef(null); const docRef = useRef(null); useEffect(() => { const doc = new Y.Doc(); const notesMap = doc.getMap('notes'); const channel = new BroadcastChannel('konva-yjs-whiteboard-v1'); docRef.current = doc; notesMapRef.current = notesMap; const readNotes = () => { const nextNotes = Array.from(notesMap.values()).sort((a, b) => a.id.localeCompare(b.id) ); setNotes(nextNotes); }; const publishLocalUpdate = (update, origin) => { if (origin === REMOTE_ORIGIN) return; channel.postMessage({ type: 'update', update }); }; channel.onmessage = (event) => { if (event.data.type === 'sync-request') { channel.postMessage({ type: 'update', update: Y.encodeStateAsUpdate(doc), }); return; } if (event.data.type === 'update') { Y.applyUpdate(doc, new Uint8Array(event.data.update), REMOTE_ORIGIN); } }; notesMap.observe(readNotes); doc.on('update', publishLocalUpdate); readNotes(); channel.postMessage({ type: 'sync-request' }); const seedTimer = window.setTimeout(() => { if (notesMap.size > 0) return; doc.transact(() => { initialNotes.forEach((note) => notesMap.set(note.id, note)); }); }, 120); setChannelReady(true); return () => { window.clearTimeout(seedTimer); notesMap.unobserve(readNotes); doc.off('update', publishLocalUpdate); channel.close(); doc.destroy(); notesMapRef.current = null; docRef.current = null; }; }, []); const updateNote = (id, patch) => { const notesMap = notesMapRef.current; const doc = docRef.current; if (!notesMap || !doc) return; const current = notesMap.get(id); if (!current) return; doc.transact(() => { notesMap.set(id, { ...current, ...patch, updatedBy: clientName }); }); }; const moveSelected = (dx, dy) => { const selected = notes.find((note) => note.id === selectedId); if (!selected) return; updateNote(selected.id, { x: selected.x + dx, y: selected.y + dy }); }; const selectedNote = notes.find((note) => note.id === selectedId); return (

{channelReady ? `Local channel ready as ${clientName}.` : 'Opening local channel.'} {' '} {selectedNote ? `${selectedNote.text} was changed by ${selectedNote.updatedBy}.` : ''}

{notes.map((note) => ( ))}
{ if (event.target === event.target.getStage()) setSelectedId(null); }} > {notes.map((note) => { const selected = note.id === selectedId; return ( setSelectedId(note.id)} onTap={() => setSelectedId(note.id)} onDragEnd={(event) => updateNote(note.id, { x: event.target.x(), y: event.target.y(), }) } > ); })}
); } ``` ## 共享数据设计 Yjs 是共享文档模型。`notes` Y.Map 存储普通记录,React state 存储这些记录的视图。 Konva 节点不进入共享文档。每个客户端都根据相同的共享数据创建自己的节点。 本示例在每次位置变化时替换完整的便笺记录。如果用户可以并发更改不同的属性,请使用嵌套的 Y.Map 值。 将本地视口数据保留在 Yjs 外部。用户可以平移或缩放,而不会移动其他用户的视口。 ## 同步和反馈循环 每个本地 Yjs 更新都会发送到 `BroadcastChannel`。收到的每个更新都会带着 `REMOTE_ORIGIN` 标记进入 Yjs。 发布处理程序会忽略该标记。此规则可防止收到的更新在反馈循环中返回频道。 Yjs 更新具有交换性和幂等性。同步请求会将当前文档 state 发送到新打开的标签页。 `BroadcastChannel` 只能在同一源和本地浏览器配置文件中受支持的浏览器上下文之间工作。它不能连接远程用户。 ## 坐标和交互 共享便笺位置使用白板坐标。平移或缩放后,每个客户端都必须通过本地 Stage 变换来转换指针位置。 对于简单对象,只发送最终拖动位置。要显示实时拖动预览,请限制更新频率,并将感知数据与文档数据分开发送。 不要将指针移动同步为永久的文档更改。在线状态数据可以在没有文档更新的情况下过期。 ## 无障碍功能 HTML 按钮可在不使用指针的情况下选择和移动每个便笺。实时区域会标识客户端和最近一次所选便笺更改的来源。 生产白板需要所有对象的有序 HTML 表示。文本编辑必须使用带标签的原生 HTML 输入框。 远程光标需要为必要信息提供文本替代内容。装饰性光标移动可以对辅助技术隐藏。 ## 性能 先将 Yjs 更新应用于数据模型。然后,让 React 只更新值发生变化的节点。 如果应用发送中间位置,请限制拖动更新频率。大型白板还需要视口剔除,并对远处的图形使用简化形式。 在服务器上压缩持久更新日志。使用具有代表性的白板测量文档加载时间和内存使用量。 ## 生产限制 本示例没有远程网络提供程序、用户身份、授权、持久化、在线状态或离线状态界面。 对远程用户使用 Yjs WebSocket 或 WebRTC 提供程序。对连接进行身份验证,授权文档访问,并在可信基础设施上持久保存更新。 在数据格式更改前添加 schema 版本。使用跟踪本地来源的 Y.UndoManager 来实现按用户撤销。 测试网络中断、重新连接、重复更新、大型文档和并发编辑。本地双标签页示例无法确保这些情况正常工作。 --- # Canvas 绘图——使用 JavaScript 在 HTML5 Canvas 上自由绘制和涂画 > 使用 JavaScript 在 HTML5 Canvas 上绘图。使用 Konva.js 构建带自由笔画、画笔工具和橡皮擦的 Canvas 绘图应用。介绍矢量图形和手动 2D 绘图两种方法。 Source: https://konvajs.org/zh-Hans/docs/sandbox/Free_Drawing.html 构建一个 Canvas 绘图工具,让用户直接在 HTML5 Canvas 上绘制自由笔画。这是 Canvas 应用最常见的功能之一,包括白板、标注工具、设计编辑器和签名板。 使用 Konva 进行 Canvas 绘图有两种常见方法: 1. **基于 Konva 的矢量图形**——每条笔画都是一个 Konva.Line 对象,以后可以选择、移动和删除(简单,适合大多数场景) 2. **手动 2D Canvas 绘图**——直接绘制到 Canvas 像素缓冲区,在包含大量笔画时获得最高性能(高级) ## 使用 Konva 节点自由绘制 第一种方法可能也是最简单的方法: 1. 创建新的 `Konva.Line`,并在 `mousedown`/`touchstart` 时开始绘制 2. 在 `mousemove`/`touchmove` 时向线条添加新点 此方法适用于许多应用。也可以在某处以矢量形式轻松存储绘图 state,例如存储在 React store 中或以 JSON 格式保存到数据库。 ```js import Konva from 'konva'; // create tool select const select = document.createElement('select'); select.innerHTML = ` `; document.body.appendChild(select); const width = window.innerWidth; const height = window.innerHeight - 25; // first we need Konva core things: stage and layer const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); let isPaint = false; let mode = 'brush'; let lastLine; stage.on('mousedown touchstart', function (e) { isPaint = true; const pos = stage.getPointerPosition(); lastLine = new Konva.Line({ stroke: '#df4b26', strokeWidth: 5, globalCompositeOperation: mode === 'brush' ? 'source-over' : 'destination-out', // round cap for smoother lines lineCap: 'round', lineJoin: 'round', // add point twice, so we have some drawings even on a simple click points: [pos.x, pos.y, pos.x, pos.y], }); layer.add(lastLine); }); stage.on('mouseup touchend', function () { isPaint = false; }); // and core function - drawing stage.on('mousemove touchmove', function (e) { if (!isPaint) { return; } // prevent scrolling on touch devices e.evt.preventDefault(); const pos = stage.getPointerPosition(); const newPoints = lastLine.points().concat([pos.x, pos.y]); lastLine.points(newPoints); }); select.addEventListener('change', function () { mode = select.value; }); ``` ```js import React from 'react'; import { Stage, Layer, Line } from 'react-konva'; const App = () => { const [tool, setTool] = React.useState('brush'); const [lines, setLines] = React.useState([]); const isDrawing = React.useRef(false); const handleMouseDown = (e) => { isDrawing.current = true; const pos = e.target.getStage().getPointerPosition(); setLines([...lines, { tool, points: [pos.x, pos.y] }]); }; const handleMouseMove = (e) => { // no drawing - skipping if (!isDrawing.current) { return; } // prevent scrolling on touch devices e.evt.preventDefault(); const stage = e.target.getStage(); const point = stage.getPointerPosition(); // To draw line let lastLine = lines[lines.length - 1]; // add point lastLine.points = lastLine.points.concat([point.x, point.y]); // replace last lines.splice(lines.length - 1, 1, lastLine); setLines(lines.concat()); }; const handleMouseUp = () => { isDrawing.current = false; }; return ( <> {lines.map((line, i) => ( ))} ); }; export default App; ``` ```js ``` ## 手动自由绘制 如果要直接使用底层 2D Canvas API,第一种方法存在限制。如果需要对 Canvas 进行高级访问,最好使用[原生上下文访问](/zh-Hans/docs/sandbox/Native_Context_Access.html)。 我们将创建一个特殊的离屏 Canvas,并在其中添加所有绘图。 通过原生 Canvas 访问,可以使用底层 2D 上下文函数。 要在舞台上显示此 Canvas,将使用 `Konva.Image`。 ```js import Konva from 'konva'; // create tool select const select = document.createElement('select'); select.innerHTML = ` `; document.body.appendChild(select); const width = window.innerWidth; const height = window.innerHeight - 25; // first we need Konva core things: stage and layer const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); // then we are going to draw into special canvas element const canvas = document.createElement('canvas'); canvas.width = stage.width(); canvas.height = stage.height(); // created canvas we can add to layer as "Konva.Image" element const image = new Konva.Image({ image: canvas, x: 0, y: 0, }); layer.add(image); // Good. Now we need to get access to context element const context = canvas.getContext('2d'); context.strokeStyle = '#df4b26'; context.lineJoin = 'round'; context.lineWidth = 5; let isPaint = false; let lastPointerPosition; let mode = 'brush'; // now we need to bind some events // we need to start drawing on mousedown // and stop drawing on mouseup image.on('mousedown touchstart', function () { isPaint = true; lastPointerPosition = stage.getPointerPosition(); }); stage.on('mouseup touchend', function () { isPaint = false; }); // and core function - drawing stage.on('mousemove touchmove', function () { if (!isPaint) { return; } if (mode === 'brush') { context.globalCompositeOperation = 'source-over'; } if (mode === 'eraser') { context.globalCompositeOperation = 'destination-out'; } context.beginPath(); const localPos = { x: lastPointerPosition.x - image.x(), y: lastPointerPosition.y - image.y(), }; context.moveTo(localPos.x, localPos.y); const pos = stage.getPointerPosition(); const newLocalPos = { x: pos.x - image.x(), y: pos.y - image.y(), }; context.lineTo(newLocalPos.x, newLocalPos.y); context.closePath(); context.stroke(); lastPointerPosition = pos; // redraw manually layer.batchDraw(); }); select.addEventListener('change', function () { mode = select.value; }); ``` ```js import React from 'react'; import { Stage, Layer, Image } from 'react-konva'; const App = () => { const [tool, setTool] = React.useState('brush'); const isDrawing = React.useRef(false); const imageRef = React.useRef(null); const lastPos = React.useRef(null); const { canvas, context } = React.useMemo(() => { const canvas = document.createElement('canvas'); canvas.width = window.innerWidth; canvas.height = window.innerHeight - 25; const context = canvas.getContext('2d'); context.strokeStyle = '#df4b26'; context.lineJoin = 'round'; context.lineWidth = 5; return { canvas, context }; }, []); const handleMouseDown = (e) => { isDrawing.current = true; lastPos.current = e.target.getStage().getPointerPosition(); }; const handleMouseUp = () => { isDrawing.current = false; }; const handleMouseMove = (e) => { if (!isDrawing.current) { return; } const image = imageRef.current; const stage = e.target.getStage(); context.globalCompositeOperation = tool === 'eraser' ? 'destination-out' : 'source-over'; context.beginPath(); const localPos = { x: lastPos.current.x - image.x(), y: lastPos.current.y - image.y(), }; context.moveTo(localPos.x, localPos.y); const pos = stage.getPointerPosition(); const newLocalPos = { x: pos.x - image.x(), y: pos.y - image.y(), }; context.lineTo(newLocalPos.x, newLocalPos.y); context.closePath(); context.stroke(); lastPos.current = pos; image.getLayer().batchDraw(); }; return ( <> ); }; export default App; ``` ```js ``` --- # React Canvas 库 — react-konva 入门 > react-konva 是一个用于绘制 2D 图形的 React Canvas 库。使用声明式 JSX 创建具有图形、拖放、事件和动画的交互式 Canvas 应用。 Source: https://konvajs.org/zh-Hans/docs/react/index.html ![React Konva 标志 — React Canvas 库](https://cloud.githubusercontent.com/assets/1443320/12193428/3bda2fcc-b623-11e5-8319-b1ccfc95eaec.png) `react-konva` 是一个用于绘制复杂 2D 图形的 React Canvas 库。它为 [Konva 框架](https://konvajs.org/)提供声明式绑定。你可以使用熟悉的 React 组件和数据流。 [GitHub 仓库](https://github.com/konvajs/react-konva) 使用 react-konva 时,你可以像编写 React DOM 一样编写 Canvas 图形。你可以使用 JSX 组件、props、state 和 hooks。 每个 Konva 图形都可以作为 React 组件使用。这些图形包括 Rect、Circle、Line、Text、Image 和 Star,并完整支持事件。 **注意:`react-konva` 只能在浏览器中使用,不支持 React Native。** React Native 没有 DOM,也没有 `` 元素,因此 Konva 没有可以绘制的目标。如果你在 Web 上已经有一个 Konva 编辑器,把它放进 [WebView](https://github.com/react-native-webview/react-native-webview) 中运行是可行的方案。如果是新的原生应用,请使用 [React Native Skia](https://shopify.github.io/react-native-skia/)。 你可以在 [konvajs.org](https://konvajs.org/) 查看数十个交互式示例。浏览器中的场景图功能可以通过 React 组件或 Konva 节点 ref 使用。请直接使用 Konva 进行 Node.js 渲染。 可以这样理解:`Konva` 与 `react-konva` 的关系,类似于 `DOM` 与 `React` 的关系。 ## 安装 `react-konva` 的主版本必须与 React 的主版本匹配。React 19 项目使用当前版本: ```bash npm install react-konva konva ``` React 18 项目必须安装 `react-konva` 18: ```bash npm install react-konva@18 konva ``` 下面的基本示例创建一个包含多个图形的简单 Canvas: ```js import React, { useState } from 'react'; import { Stage, Layer, Rect, Circle, Text } from 'react-konva'; const App = () => { const [rectPosition, setRectPosition] = useState({ x: 20, y: 50 }); const [circlePosition, setCirclePosition] = useState({ x: 200, y: 100 }); return ( setRectPosition(e.target.position())} /> setCirclePosition(e.target.position())} /> ); }; export default App; ``` ## 人们使用 react-konva 构建什么 大多数生产级 Canvas 应用可以分为几类。下面每个示例都可以运行,其中一些还并排展示了原生 JavaScript 和 Vue 版本: - [设计编辑器](/zh-Hans/docs/sandbox/Canvas_Editor.html) — 选择、调整大小手柄、历史记录和图像导出 - [无限 Canvas 白板](/zh-Hans/docs/sandbox/Infinite_Canvas.html) — 使用稳定坐标平移和缩放 - [节点编辑器](/zh-Hans/docs/sandbox/Connected_Objects.html) — 带有动态连线的可拖动节点 - [图像标注工具](/zh-Hans/docs/sandbox/Image_Labeling.html) — 在图像上绘制结构化区域 - [平面图](/zh-Hans/docs/sandbox/Interactive_Building_Map.html)和[座位预订图](/zh-Hans/docs/sandbox/Seats_Reservation.html) — 大规模交互式几何图形 - [图像编辑器](/zh-Hans/docs/sandbox/Canvas_Crop_Image.html) — 变换和高分辨率导出 - [多人白板](/zh-Hans/docs/sandbox/Multiplayer_Whiteboard.html) — 通过 Yjs 共享 state 如果你需要完整的编辑器,而不只是基础图形组件,可以使用 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=react-index)。它是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK。它的界面使用 React,你可以将其作为组件集成到 React 应用中:`npm install polotno`。 --- # 使用 Vue 和 Konva 开始 Canvas 开发 > 开始使用 vue-konva,它是 Konva.js 的官方 Vue 绑定。使用 Vue 组件绘制图形、处理事件并构建交互式 Canvas 应用。 Source: https://konvajs.org/zh-Hans/docs/vue/index.html ## 如何在 Vue 中使用 Canvas? ![VueKonva 标志](https://raw.githubusercontent.com/konvajs/vue-konva/master/vue-konva.png) Vue Konva 是一个使用 Vue 绘制复杂 Canvas 图形的 JavaScript 库。 它提供对 [Konva 框架](https://konvajs.org/)的声明式响应式绑定。 所有 `vue-konva` 组件都对应同名的 `Konva` 组件,并带有前缀 'v-'。`Konva` 对象支持的所有参数都可以作为 `config` 添加到对应 `vue-konva` 组件的 prop 中。 核心图形包括:`v-rect`、`v-circle`、`v-ellipse`、`v-line`、`v-image`、`v-text`、`v-text-path`、`v-star`、`v-label`、`v-path`、`v-regular-polygon`。 你还可以创建[自定义图形](/zh-Hans/docs/vue/Custom_Shape.html)。 如需详细了解 `Konva`,请阅读 [Konva 概览](/zh-Hans/docs/overview.html)。 如果你需要完整的设计编辑器,可以使用 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=vue-index)。它是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK,提供模板、文本编辑和导出功能,并附有 [Vue 集成指南](https://polotno.com/docs/vue-js?utm_source=konvajs&utm_medium=docs&utm_content=vue-index)。 ## 快速开始 需要 [Vue.js](https://vuejs.org) 3。 ### 1. 通过 npm 安装 ```bash npm install vue-konva konva --save ``` ### 2. 选择导入方式 可以通过两种方式使用 `vue-konva`: #### 选项 A:全局注册(建议初学者使用) 使用插件全局注册所有组件: ```js import { createApp } from 'vue'; import App from './App.vue'; import VueKonva from 'vue-konva'; const app = createApp(App); app.use(VueKonva); app.mount('#app'); ``` 完成全局注册后,所有组件都可以使用 `v-` 前缀:`v-stage`、`v-layer`、`v-rect`、`v-circle` 等。 #### 选项 B:按需导入 仅导入需要的组件: ```vue ``` ### 3. 示例:可拖动星形 **操作说明:** 尝试拖动星形。拖动时,星形会放大。松开后,星形会恢复正常大小。 ```js ``` ### 或者使用 CDN ```html
``` --- # 通过 Konva 开始使用 Svelte 和 Canvas > 开始使用 svelte-konva,它是 Konva.js 的官方 Svelte 绑定。使用 Svelte 组件绘制图形、处理事件并构建交互式 Canvas 应用。 Source: https://konvajs.org/zh-Hans/docs/svelte/index.html ## 如何在 Svelte 中使用 Canvas? [svelte-konva](https://github.com/konvajs/svelte-konva) 是一个使用 Svelte 绘制复杂 Canvas 图形的 JavaScript 库。它为 [Konva 框架](https://konvajs.org/) 提供声明式响应式绑定。所有 `svelte-konva` 组件都对应同名的 `Konva` 组件。`Konva` 对象的所有可用参数都可以作为单独的 props 添加到对应的 `svalte-konva` 组件中。 使用 svelte-konva 前,需要基本了解 `Konva`。可以参阅 [Konva 概览](/zh-Hans/docs/overview.html)。 如果你需要完整的设计编辑器,可以使用 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=svelte-index)。它是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK,提供模板、文本编辑和导出功能,并附有适用于 Svelte 的[不依赖框架的集成指南](https://polotno.com/docs/frameworkless-integration?utm_source=konvajs&utm_medium=docs&utm_content=svelte-index)。 ## 快速开始 ### 1 通过 npm 安装 ```npm npm i svelte-konva konva ``` ### 2 导入并使用 svelte-konva 组件 ```js ``` [Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/basic_demo?file=/src/App.svelte) --- # 通过 Konva 开始使用 Angular 和 Canvas > 开始使用 ng2-konva,它是 Konva.js 的官方 Angular 绑定。使用 Angular 指令绘制图形、处理事件并构建交互式 Canvas 应用。 Source: https://konvajs.org/zh-Hans/docs/angular/index.html ## Angular 如何使用 Canvas? `ng2-konva` 是一个 JavaScript 库,可使用 [Angular](https://angular.dev/) 绘制复杂的 Canvas 图形。它为 [Konva 框架](https://konvajs.org/) 提供声明式和响应式绑定。 [GitHub 仓库](https://github.com/konvajs/ng2-konva) 它尝试让 [Angular](https://angular.dev/) 与 HTML5 Canvas 库配合使用。其目标是提供与普通 Angular 类似的声明式标记和数据流模型。 所有 `ng2-konva` 组件都对应带有 'ko-' 前缀的同名 `Konva` 组件。`Konva` 对象的所有可用参数都可以作为 `config`,添加到对应 `ng2-konva` 组件的属性绑定中。 核心图形包括:`ko-rect`、`ko-circle`、`ko-ellipse`、`ko-line`、`ko-image`、`ko-text`、`ko-text-path`、`ko-star`、`ko-label`、`ko-path`、`ko-regular-polygon`。 你还可以创建 [自定义图形](/zh-Hans/docs/angular/Custom_Shape.html)。 有关 `Konva` 的更多信息,请阅读 [Konva 概述](/zh-Hans/docs/overview.html)。 普通静态 config 适合简单示例。如果通过图像加载等异步回调更新 config,请优先使用 Angular signals。 如果你需要完整的设计编辑器,可以使用 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=angular-index)。它是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK,提供模板、文本编辑和导出功能,并附有 [Angular 集成指南](https://polotno.com/docs/angular?utm_source=konvajs&utm_medium=docs&utm_content=angular-index)。 ## 快速开始 需要 [Angular](https://angular.dev/) 21 或 22。 ### 1. 通过 npm 安装 ```bash npm install ng2-konva konva --save ``` ### 2. 导入并使用 ng2-konva ```ts import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ // ... other config standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App { public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configCircle: CircleConfig = { x: 100, y: 100, radius: 50, fill: 'red', }; } ``` ### 3. 在组件中使用 **操作说明**:尝试拖动星形。拖动时,星形会放大。释放后,星形会恢复正常大小。 ```js import { Component, OnInit, viewChild } from '@angular/core'; import Konva from 'konva'; import { StarConfig } from 'konva/lib/shapes/Star'; import { StageConfig } from 'konva/lib/Stage'; import { CoreShapeComponent, NgKonvaEventObject, StageComponent, } from 'ng2-konva'; type ExtStartConfig = StarConfig & { startScale: number }; @Component({ selector: 'app-root', standalone: true, template: ` @for (config of starConfigs; track trackConfig($index, config)) { } `, imports: [StageComponent, CoreShapeComponent], }) export default class StarExampleComponent implements OnInit { public width = 800; public height = 800; public starConfigs: ExtStartConfig[] = []; public configStage: Partial = { width: this.width, height: this.height, }; public handleDragstart( event: NgKonvaEventObject, ): void { const shape = event.target; this.starConfigs = this.starConfigs.map((conf) => { if ( conf.name !== shape.name()) { return conf; } return { ...conf, shadowOffsetX: 15, shadowOffsetY: 15, scaleX: conf.startScale * 1.2, scaleY: conf.startScale * 1.2, }; }); this.starConfigs = [ ...this.starConfigs.filter((conf) => conf.name !== shape.name()), this.starConfigs.find((conf) => conf.name === shape.name())!, ]; } public handleDragend( event: NgKonvaEventObject, ): void { const shape = event.target; this.starConfigs = this.starConfigs.map((conf) => { if (conf.name !== shape.name()) { return conf; } return { ...conf, x: shape.x(), y: shape.y(), scaleX: conf.startScale, scaleY: conf.startScale, }; }); } trackConfig(index: number, config: ExtStartConfig): string | undefined { return config.name; } public ngOnInit(): void { for (let n = 0; n < 100; n++) { const scale = Math.random(); this.starConfigs.push({ x: Math.random() * this.width, y: Math.random() * this.height, rotation: Math.random() * 180, numPoints: 5, innerRadius: 30, outerRadius: 50, fill: '#89b717', opacity: 0.8, draggable: true, scaleX: scale, scaleY: scale, shadowColor: 'black', shadowBlur: 10, shadowOffsetX: 5, shadowOffsetY: 5, shadowOpacity: 0.6, startScale: scale, name: n.toString(), }); } } } ``` 有关完整的属性和方法列表,请参阅 [Konva API 参考](/zh-Hans/api/Konva.html)。 --- # HTML5 Canvas 矩形教程 > 学习如何使用 Konva.js 在 HTML5 Canvas 上绘制矩形。使用 Konva.Rect 图形设置填充、描边、尺寸、位置、圆角、阴影等属性。 Source: https://konvajs.org/zh-Hans/docs/shapes/Rect.html 要使用 `Konva` 创建矩形,可以实例化一个 `Konva.Rect()` 对象。 有关完整的属性和方法列表,请参阅 [Konva.Rect 文档](/zh-Hans/api/Konva.Rect.html)。 你可以为 `Konva.Rect` 定义圆角半径。该值可以是一个数字,也可以是数字数组 `[topLeft, topRight, bottomRight, bottomLeft]`。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', // id of container
width: window.innerWidth, height: window.innerHeight }); const layer = new Konva.Layer(); stage.add(layer); const rect1 = new Konva.Rect({ x: 20, y: 20, width: 100, height: 50, fill: 'green', stroke: 'black', strokeWidth: 4 }); layer.add(rect1); const rect2 = new Konva.Rect({ x: 150, y: 40, width: 100, height: 50, fill: 'red', shadowBlur: 10, cornerRadius: 10 }); layer.add(rect2); const rect3 = new Konva.Rect({ x: 50, y: 120, width: 100, height: 100, fill: 'blue', cornerRadius: [0, 10, 20, 30] }); layer.add(rect3); stage.add(layer); ``` ```js import { Stage, Layer, Rect } from 'react-konva'; const App = () => { return ( ); }; export default App; ``` ```js ``` **后续步骤:** - [为图形添加事件 →](/zh-Hans/docs/events/Binding_Events.html) - [使图形可拖动 →](/zh-Hans/docs/drag_and_drop/Drag_and_Drop.html) - [添加调整尺寸和旋转手柄 →](/zh-Hans/docs/select_and_transform/Basic_demo.html) - [为图形属性添加动画 →](/zh-Hans/docs/animations/Create_an_Animation.html) - [Konva.Rect API 参考 →](/zh-Hans/api/Konva.Rect.html) --- # HTML5 Canvas 图形事件 > 学习如何使用 Konva.js 为 HTML5 Canvas 图形绑定事件监听器,以及如何处理 click、dblclick、mouseover、mouseout、mousemove 等事件。 Source: https://konvajs.org/zh-Hans/docs/events/Binding_Events.html 要使用 Konva 检测图形事件,可以使用 `on()` 方法将事件处理程序绑定到节点。 `on()` 方法需要一个事件类型,以及一个在事件发生时执行的函数。 鼠标事件:`mouseover`、`mouseout`、`mouseenter`、`mouseleave`、`mousemove`、`mousedown`、`mouseup`、`wheel`、`click`、`dblclick`。 触摸事件:`touchstart`、`touchmove`、`touchend`、`tap`、`dbltap`。 指针事件:`pointerdown`、`pointermove`、`pointereup`、`pointercancel`、`pointerover`、`pointerenter`、`pointerout`、`pointerleave`、`pointerclick`、`pointerdblclick`。 拖放事件:`dragstart`、`dragmove` 和 `dragend`。 变换事件:`transformstart`、`transform`、`transformend`。 **操作说明:在三角形上触发 mouseover 和 mouseout,并在圆形上触发 mouseover、mouseout、mousedown 和 mouseup。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); const text = new Konva.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black', }); const triangle = new Konva.RegularPolygon({ x: 80, y: 120, sides: 3, radius: 80, fill: '#00D2FF', stroke: 'black', strokeWidth: 4, }); const circle = new Konva.Circle({ x: 230, y: 100, radius: 60, fill: 'red', stroke: 'black', strokeWidth: 4, }); function writeMessage(message) { text.text(message); } triangle.on('mouseout', () => { writeMessage('Mouseout triangle'); }); triangle.on('mousemove', () => { const mousePos = stage.getPointerPosition(); writeMessage('x: ' + mousePos.x + ', y: ' + mousePos.y); }); circle.on('mouseover', () => { writeMessage('Mouseover circle'); }); circle.on('mouseout', () => { writeMessage('Mouseout circle'); }); circle.on('mousedown', () => { writeMessage('Mousedown circle'); }); circle.on('mouseup', () => { writeMessage('Mouseup circle'); }); layer.add(triangle); layer.add(circle); layer.add(text); stage.add(layer); ```` ```jsx import { Stage, Layer, RegularPolygon, Circle, Text } from 'react-konva'; import { useRef, useState } from 'react'; const App = () => { const [message, setMessage] = useState(''); const stageRef = useRef(); const writeMessage = (text) => { setMessage(text); }; return ( writeMessage('Mouseout triangle')} onMousemove={() => { const mousePos = stageRef.current.getPointerPosition(); writeMessage('x: ' + mousePos.x + ', y: ' + mousePos.y); }} /> writeMessage('Mouseover circle')} onMouseout={() => writeMessage('Mouseout circle')} onMousedown={() => writeMessage('Mousedown circle')} onMouseup={() => writeMessage('Mouseup circle')} /> ); }; export default App; ```` ```html ``` --- # HTML5 Canvas 拖放教程 > 学习使用 Konva.js 为 HTML5 Canvas 图形添加拖放功能。通过一个属性使任意图形可以拖动,并处理拖动事件。 Source: https://konvajs.org/zh-Hans/docs/drag_and_drop/Drag_and_Drop.html 如需使用 Konva 拖放图形,请在创建图形时将 `draggable` 属性设置为 true。你也可以使用 `draggable()` 方法。 `draggable()` 方法会自动为桌面应用和移动应用启用拖放功能。 如需检测 Konva 拖放事件,请使用 `on()` 方法将 `dragstart`、`dragmove` 或 `dragend` 事件绑定到节点。 `on()` 方法需要事件类型和回调函数。事件发生时,Konva 会运行该函数。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, draggable: true, }); // add cursor styling circle.on('mouseover', function () { document.body.style.cursor = 'pointer'; }); circle.on('mouseout', function () { document.body.style.cursor = 'default'; }); layer.add(circle); ``` ```jsx import { Stage, Layer, Circle } from 'react-konva'; import { useState } from 'react'; const App = () => { const [position, setPosition] = useState({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); return ( { document.body.style.cursor = 'pointer'; }} onMouseLeave={(e) => { document.body.style.cursor = 'default'; }} onDragEnd={(e) => { setPosition({ x: e.target.x(), y: e.target.y() }); }} /> ); }; export default App; ``` ```vue ``` --- # HTML5 Canvas Konva 动画教程 > 学习如何使用 Konva.js 在 HTML5 Canvas 上创建动画。使用 Konva.Animation 创建逐帧动画,使用 Konva.Tween 实现属性过渡。 Source: https://konvajs.org/zh-Hans/docs/animations/Create_an_Animation.html 如需使用 Konva 创建自定义动画,可以使用 `Konva.Animation` 构造函数。此构造函数接受两个参数:必需的更新函数,以及 可选的图层或图层数组。每个动画帧都会更新这些图层。 动画函数会收到一个 `frame` 对象。其 `time` 属性表示动画已经运行的 毫秒数。`timeDiff` 属性表示自上一帧以来经过的 毫秒数。`frameRate` 属性表示当前帧率, 单位为每秒帧数。 更新函数绝不能重绘舞台或图层,因为动画引擎会自动处理重绘。 更新函数应仅包含更新节点属性的逻辑, 例如 `position`、`rotation`、`scale`、`width`、`height`、`radius` 和 `colors` 等。 创建动画后,可以随时使用 `start()` 方法启动它。 如需查看完整的属性和方法列表,请参阅 [Konva.Animation 文档](/zh-Hans/api/Konva.Animation.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const rect = new Konva.Rect({ x: 50, y: 50, width: 50, height: 50, fill: 'green', }); layer.add(rect); const anim = new Konva.Animation(function(frame) { const time = frame.time; const timeDiff = frame.timeDiff; const frameRate = frame.frameRate; // Example: move rectangle in a circle const radius = 50; const x = radius * Math.cos(frame.time * 2 * Math.PI / 2000) + 100; const y = radius * Math.sin(frame.time * 2 * Math.PI / 2000) + 100; rect.position({ x, y }); }, layer); anim.start(); ```` ```js import { Stage, Layer, Rect } from 'react-konva'; import { useEffect, useRef } from 'react'; const App = () => { const rectRef = useRef(null); useEffect(() => { const anim = new Konva.Animation((frame) => { const time = frame.time; const timeDiff = frame.timeDiff; const frameRate = frame.frameRate; // Example: move rectangle in a circle const radius = 50; const x = radius * Math.cos(frame.time * 2 * Math.PI / 2000) + 100; const y = radius * Math.sin(frame.time * 2 * Math.PI / 2000) + 100; rectRef.current.position({ x, y }); }, rectRef.current.getLayer()); anim.start(); return () => { anim.stop(); }; }, []); return ( ); }; export default App; ```` ```js ``` --- # HTML5 Canvas 图像模糊滤镜教程 > 学习如何使用 Konva.js 对 HTML5 Canvas 图像应用模糊滤镜,并通过 blurRadius 属性调整模糊程度。 Source: https://konvajs.org/zh-Hans/docs/filters/Blur.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 模糊图像,可以使用 `Konva.Filters.Blur` 滤镜 并通过 `blurRadius` 属性设置模糊程度。 **操作说明**:滑动控件以调整模糊半径。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Blur]); image.blurRadius(10); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0'; slider.max = '40'; slider.value = image.blurRadius(); slider.style.position = 'absolute'; slider.style.top = '20px'; slider.style.left = '20px'; slider.addEventListener('input', (e) => { const value = parseInt(e.target.value); image.blurRadius(value); }); document.body.appendChild(slider); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [blurRadius, setBlurRadius] = useState(10); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Blur]} blurRadius={blurRadius} /> setBlurRadius(parseInt(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas Konva 全部性能技巧列表 > Konva.js 的全部性能优化技巧,包括图层管理、图形缓存、禁用监听和批量绘制等内容。在 HTML5 Canvas 上高效渲染数千个图形。 Source: https://konvajs.org/zh-Hans/docs/performance/All_Performance_Tips.html 不想把时间花在性能问题上?申请[性能评审](https://lavrton.com/consulting/)。 ### 为什么这很重要 HTML5 Canvas 可以高效完成绘制工作,`Konva` 内部也有许多用于提高性能的功能。但是,随着项目复杂度增加或舞台上的图形数量增多,性能必然会受到影响。 ### 优化目标 这里的优化主要遵循两条通用规则: * 尽量减少计算:所有计算都需要时间。单次计算可能只需极短时间,但你的代码、Konva、JavaScript 和更底层代码产生的数千或数百万次计算会不断累积。如果原本流畅的动画或效果出现卡顿,人眼就能察觉这种影响。 * **尽量减少绘制**:所有绘制都有性能成本,因此这条规则很重要。成本分为两类:一类是上一点介绍的绘制计算,另一类是将绘制内容从内存传输到屏幕。某些情况下,还会有中间的离屏合成或逐像素处理。因此,请尽量减少绘制。 ### 舞台 1. 优化舞台尺寸——按照“尽量减少绘制”的规则,请避免创建大型舞台。将大量字节从内存传输到屏幕会降低性能。[此处](/zh-Hans/docs/sandbox/Canvas_Scrolling.html)提供了一些解决超大舞台问题的替代方法。 2. 在移动端设置视口——缩放图像会明显降低性能。因此,请为移动端应用设置视口:``。这样可以避免对 Konva 输出进行不必要的缩放。 3. 在 Retina 设备上使用 `Konva.pixelRatio = 1`——Konva 会自动调整像素比,从而在所有设备上清晰渲染。如果 Retina 设备的性能不佳,请设置 `Konva.pixelRatio = 1`,以减少 Konva 的缩放工作。在某些情况下,此设置可能影响输出,因此请确认结果质量符合要求。 ### 图层 1. [图层管理](/zh-Hans/docs/performance/Layer_Management.html)——在内部,每个 Konva 图层都是一个单独的 HTML5 Canvas 元素。这样可以只刷新发生变化的图层,从而避免刷新整个舞台的性能成本。但是,每个图层都会增加性能开销,因此应尽量减少图层数量。 2. 使用 `layer.listening(false)`——Konva 为绘制的所有图形提供鼠标和触摸事件监听器。每个监听器都有性能成本。对于包含许多图形的图层,Konva 必须花费大量周期检查可能触发的监听器。如果图层中的所有图形都不需要响应事件,请设置 `layer.listening(false)` 以消除此开销。请参阅[示例](/zh-Hans/docs/sandbox/Animation_Stress_Test.html)。图形部分也有类似技巧。 3. 优化拖动成本——在图层上拖动图形时,每次移动事件循环都必须重绘该图层。为避免此性能成本,请在拖动时将图形移动到专用图层,并在拖动结束时将其移回原图层。请参阅[示例](/zh-Hans/docs/sandbox/Drag_and_Drop_Stress_Test.html) ### 图形 1. [图形缓存](/zh-Hans/docs/performance/Shape_Caching.html)——Konva 在内部创建图形的图像,并在需要绘制图形时使用该图像。绘制图像可以避免按照绘制指令组合图形的开销,并能明显提高复杂图形和组的性能。 2. 整理图形——舞台中的每个图形都会产生存在成本。要优化性能,请隐藏或从图层中移除不可见、不透明度为 0 或移出视野的对象。 3. 使用 `shape.listening(false)`——与图层一样(请参阅上文第 7 点),Konva 会检查何时应为图形触发事件,这会产生性能成本。让图形停止监听事件可以降低此成本,详情请参阅[禁用监听](/zh-Hans/docs/performance/Listening_False.html)。 4. 禁用完美绘制——在某些情况下,HTML5 Canvas 的绘制结果可能与预期不同。有关示例,请参阅[禁用完美绘制](/zh-Hans/docs/performance/Disable_Perfect_Draw.html)。Konva 使用完美绘制功能执行额外工作以修正结果,但这会产生性能成本。当图形同时具有填充、描边和不透明度时,设置 `shape.perfectDrawEnabled(false)` 可以避免此成本,并且不会降低输出质量。 5. [优化描边绘制](/zh-Hans/docs/performance/Optimize_Strokes.html)——当图形同时具有描边和阴影时,Konva 会执行一次额外的内部绘制,以获得符合预期的结果。禁用 Konva 为描边添加的阴影,可以避免此性能负担。 ### 动画 1. [优化动画](/zh-Hans/docs/performance/Optimize_Animation.html)——避免为两次视觉变化之间的动画步骤执行不必要的重绘。 ### 内存 1. [避免内存泄漏](/zh-Hans/docs/performance/Avoid_Memory_Leaks.html)——Konva 会处理许多可能产生内存泄漏的情况,但你仍需正确创建和移除图形与补间。 2. **了解一个图层的开销。** 每个图层都会分配两块 canvas:一块按设备像素比创建的场景 canvas,以及一块像素比始终为 1 的命中检测 canvas。在 Retina 屏幕上,一个 1920 × 1080 的舞台大约是 33 MB 加 8 MB,也就是在绘制任何内容之前每个图层约 41 MB。这正是 Konva 在图层超过五个时发出警告的原因——到那时,仅仅是空图层就已经接近 200 MB 的 canvas 内存。 3. **移动版 Safari 有硬性上限。** 超过之后会出现 `Total canvas memory use exceeds the maximum limit`,在部分设备上报告为 256 MB,在另一些设备上为 384 MB,而且 canvas 会直接变成空白,而不是逐步降级。按效果排序的手段是:减少图层数量、缩小舞台,以及设置 `Konva.pixelRatio = 1`——在 2× 屏幕上这会把场景 canvas 的开销降到四分之一。缓存也计入同一份预算,因此当节点不再可见时,请用 `node.clearCache()` 释放它。 ### 超大场景 1. **剔除屏幕外的内容。** 无论节点是否落在舞台范围内,Konva 都会绘制图层上的每个节点。对于远大于视口的场景,请隐藏视口之外的节点——`visible(false)` 会同时跳过绘制和命中检测,比移除再重新添加节点便宜得多。 ```js function cull() { const view = { x: -stage.x() / stage.scaleX(), y: -stage.y() / stage.scaleY(), width: stage.width() / stage.scaleX(), height: stage.height() / stage.scaleY(), }; layer.children.forEach((node) => { node.visible(Konva.Util.haveIntersection(view, node.getClientRect())); }); } ``` 请在视图发生变化时调用它——平移或缩放之后——而不是每一帧都调用。`getClientRect()` 并不便宜,因此对于数万个节点,请自行维护一份位置索引并基于它做判断,而不是逐个询问节点。 2. **拖动过程中跳过命中检测。** 当节点正在被拖动时,Konva 不会执行命中检测,这让拖动保持低开销。如果你需要在拖动过程中知道指针下方是什么——例如高亮放置目标——就重新开启它,并接受相应开销: ```js Konva.hitOnDragEnabled = true; // default is false ``` 3. **用一个图形代替许多图形。** 一千个各自拥有属性、变换和命中区域的节点,其开销远高于一个在单个 `sceneFunc` 中绘制一千个元素的自定义图形。代价是你会失去逐元素的事件与拖动,因此这是一种取舍,而不是白得的收益——参见[自定义图形](/zh-Hans/docs/shapes/Custom.html)。 以下示例展示了其中一些性能技巧的实际效果: ```js import Konva from 'konva'; // Create stage with good performance settings const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); // Create layers with performance optimizations const backgroundLayer = new Konva.Layer({ listening: false }); const mainLayer = new Konva.Layer(); const dragLayer = new Konva.Layer(); stage.add(backgroundLayer); stage.add(mainLayer); stage.add(dragLayer); // Create a shape with caching const star = new Konva.Star({ x: 200, y: 200, numPoints: 6, innerRadius: 40, outerRadius: 70, fill: 'yellow', stroke: 'black', strokeWidth: 4, draggable: true, perfectDrawEnabled: false, // performance optimization }); // Cache the shape for better performance star.cache(); // Optimize dragging performance star.on('dragstart', () => { star.moveTo(dragLayer); }); star.on('dragend', () => { star.moveTo(mainLayer); }); // Create background with listening disabled const rect = new Konva.Rect({ x: 0, y: 0, width: stage.width(), height: stage.height(), fill: 'lightgray', listening: false, }); backgroundLayer.add(rect); mainLayer.add(star); ``` ```js import { Stage, Layer, Star, Rect } from 'react-konva'; import { Portal } from 'react-konva-utils'; import { useState, useRef, useEffect } from 'react'; const App = () => { const [isDragging, setIsDragging] = useState(false); const [position, setPosition] = useState({ x: 200, y: 200 }); const starRef = useRef(null); useEffect(() => { // Cache the shape for better performance if (starRef.current) { starRef.current.cache(); } }, []); const handleDragStart = () => { setIsDragging(true); }; const handleDragEnd = (event) => { setPosition(event.target.position()); setIsDragging(false); }; return ( ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 舞台序列化教程 > 学习如何使用 Konva.js 将 HTML5 Canvas 状态序列化并保存为 JSON,以及如何使用 stage.toJSON() 导出并通过 Konva.Node.create() 恢复 Canvas 内容。 Source: https://konvajs.org/zh-Hans/docs/data_and_serialization/Serialize_a_Stage.html 要使用 Konva 序列化舞台,可以使用 `toJSON()` 方法。 `toJSON()` 方法返回一个包含节点所有属性的 JSON 字符串。 事件处理程序和图像无法序列化。 ```js import Konva from 'konva'; // Create wrapper with relative positioning const stage = new Konva.Stage({ container: 'container', width: 400, height: 400 }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: 100, y: 100, radius: 50, fill: 'red', stroke: 'black', strokeWidth: 3 }); layer.add(circle); // Add button on top of stage const button = document.createElement('button'); button.textContent = 'Serialize Stage'; button.style.position = 'absolute'; button.style.top = '10px'; button.style.left = '10px'; document.body.appendChild(button); button.addEventListener('click', () => { const json = stage.toJSON(); console.log(json); alert('Stage serialized! Check the console for the JSON string.'); }); ``` **注意:**虽然可以在 React 中直接序列化舞台,但这通常是一种反模式。在 React 应用中,应单独管理应用状态,并序列化该状态而不是舞台。 ```js import { Stage, Layer, Circle } from 'react-konva'; import { useRef, useState } from 'react'; const App = () => { const stageRef = useRef(null); const [circle, setCircle] = useState({ x: 100, y: 100, radius: 50, fill: 'red', stroke: 'black', strokeWidth: 3 }); const handleSerialize = () => { // In a real app, prefer saving app state, not stage JSON const json = JSON.stringify({ shapes: [circle] }); console.log('Serialized state:', json); alert('State serialized! Check the console for the JSON string.'); }; return (
{ setCircle({ ...circle, x: e.target.x(), y: e.target.y() }); }} />
); }; export default App; ``` **注意:**虽然可以在 Vue 中直接序列化舞台,但这通常是一种反模式。在 Vue 应用中,应使用响应式数据管理应用状态,并序列化该状态而不是舞台。 ```js ``` --- # HTML5 Canvas 图形选择、调整尺寸和旋转 > 学习如何使用 Konva.js Transformer 选择、调整 HTML5 Canvas 图形的尺寸并旋转图形。为任意图形添加可交互的调整尺寸和旋转手柄。 Source: https://konvajs.org/zh-Hans/docs/select_and_transform/Basic_demo.html `Transformer` 是一种特殊的 `Konva.Group`。使用它可以调整任意节点或一组节点的尺寸并旋转这些节点。 要启用它,请执行以下操作: 1. 使用 `new Konva.Transformer()` 创建新实例 2. 将它添加到图层 3. 使用 `transformer.nodes([shape]);` 将它附加到节点 _注意:_ 调整节点尺寸时,变换工具不会更改节点的 `width` 和 `height` 属性。它会改为更改 `scaleX` 和 `scaleY` 属性。 **操作说明:尝试调整图形尺寸并旋转图形。单击空白区域可取消选择。使用 SHIFT 或 CTRL 将图形添加到选区或从选区移除。尝试在 Canvas 上框选一个区域。** ```js import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); // create rectangle const rect1 = new Konva.Rect({ x: 60, y: 60, width: 100, height: 90, fill: 'red', name: 'rect', draggable: true, }); layer.add(rect1); const rect2 = new Konva.Rect({ x: 250, y: 100, width: 150, height: 90, fill: 'green', name: 'rect', draggable: true, }); layer.add(rect2); // create transformer const tr = new Konva.Transformer(); layer.add(tr); // add a new feature, lets add ability to draw selection rectangle let selectionRectangle = new Konva.Rect({ fill: 'rgba(0,0,255,0.5)', visible: false, }); layer.add(selectionRectangle); let x1, y1, x2, y2; stage.on('mousedown touchstart', (e) => { // do nothing if we mousedown on any shape if (e.target !== stage) { return; } x1 = stage.getPointerPosition().x; y1 = stage.getPointerPosition().y; x2 = stage.getPointerPosition().x; y2 = stage.getPointerPosition().y; selectionRectangle.setAttrs({ x: x1, y: y1, width: 0, height: 0, visible: true, }); }); stage.on('mousemove touchmove', () => { // do nothing if we didn't start selection if (!selectionRectangle.visible()) { return; } x2 = stage.getPointerPosition().x; y2 = stage.getPointerPosition().y; selectionRectangle.setAttrs({ x: Math.min(x1, x2), y: Math.min(y1, y2), width: Math.abs(x2 - x1), height: Math.abs(y2 - y1), }); }); stage.on('mouseup touchend', () => { // do nothing if we didn't start selection if (!selectionRectangle.visible()) { return; } // update visibility in timeout, so we can check it in click event setTimeout(() => { selectionRectangle.visible(false); }); var shapes = stage.find('.rect'); var box = selectionRectangle.getClientRect(); var selected = shapes.filter((shape) => Konva.Util.haveIntersection(box, shape.getClientRect()) ); tr.nodes(selected); }); // clicks should select/deselect shapes stage.on('click tap', function (e) { // if we are selecting with rect, do nothing if (selectionRectangle.visible() && selectionRectangle.width() > 0 && selectionRectangle.height() > 0) { return; } // if click on empty area - remove all selections if (e.target === stage) { tr.nodes([]); return; } // do nothing if clicked NOT on our rectangles if (!e.target.hasName('rect')) { return; } // do we pressed shift or ctrl? const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey; const isSelected = tr.nodes().indexOf(e.target) >= 0; if (!metaPressed && !isSelected) { // if no key pressed and the node is not selected // select just one tr.nodes([e.target]); } else if (metaPressed && isSelected) { // if we pressed keys and node was selected // we need to remove it from selection: const nodes = tr.nodes().slice(); // use slice to have new copy of array // remove node from array nodes.splice(nodes.indexOf(e.target), 1); tr.nodes(nodes); } else if (metaPressed && !isSelected) { // add the node into selection const nodes = tr.nodes().concat([e.target]); tr.nodes(nodes); } }); ```` ```js import { Stage, Layer, Rect, Transformer } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; const initialRectangles = [ { x: 60, y: 60, width: 100, height: 90, fill: 'red', id: 'rect1', name: 'rect', rotation: 0, }, { x: 250, y: 100, width: 150, height: 90, fill: 'green', id: 'rect2', name: 'rect', rotation: 0, }, ]; // Helper functions for calculating bounding boxes of rotated rectangles const degToRad = (angle) => (angle / 180) * Math.PI; const getCorner = (pivotX, pivotY, diffX, diffY, angle) => { const distance = Math.sqrt(diffX * diffX + diffY * diffY); angle += Math.atan2(diffY, diffX); const x = pivotX + distance * Math.cos(angle); const y = pivotY + distance * Math.sin(angle); return { x, y }; }; const getClientRect = (element) => { const { x, y, width, height, rotation = 0 } = element; const rad = degToRad(rotation); const p1 = getCorner(x, y, 0, 0, rad); const p2 = getCorner(x, y, width, 0, rad); const p3 = getCorner(x, y, width, height, rad); const p4 = getCorner(x, y, 0, height, rad); const minX = Math.min(p1.x, p2.x, p3.x, p4.x); const minY = Math.min(p1.y, p2.y, p3.y, p4.y); const maxX = Math.max(p1.x, p2.x, p3.x, p4.x); const maxY = Math.max(p1.y, p2.y, p3.y, p4.y); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY, }; }; const App = () => { const [rectangles, setRectangles] = useState(initialRectangles); const [selectedIds, setSelectedIds] = useState([]); const [selectionRectangle, setSelectionRectangle] = useState({ visible: false, x1: 0, y1: 0, x2: 0, y2: 0, }); const isSelecting = useRef(false); const transformerRef = useRef(); const rectRefs = useRef(new Map()); // Update transformer when selection changes useEffect(() => { if (selectedIds.length && transformerRef.current) { // Get the nodes from the refs Map const nodes = selectedIds .map(id => rectRefs.current.get(id)) .filter(node => node); transformerRef.current.nodes(nodes); } else if (transformerRef.current) { // Clear selection transformerRef.current.nodes([]); } }, [selectedIds]); // Click handler for stage const handleStageClick = (e) => { // If we are selecting with rect, do nothing // But allow point clicks through (when width/height are 0) const selWidth = Math.abs(selectionRectangle.x2 - selectionRectangle.x1); const selHeight = Math.abs(selectionRectangle.y2 - selectionRectangle.y1); if (selectionRectangle.visible && selWidth > 0 && selHeight > 0) { return; } // If click on empty area - remove all selections if (e.target === e.target.getStage()) { setSelectedIds([]); return; } // Do nothing if clicked NOT on our rectangles if (!e.target.hasName('rect')) { return; } const clickedId = e.target.id(); // Do we pressed shift or ctrl? const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey; const isSelected = selectedIds.includes(clickedId); if (!metaPressed && !isSelected) { // If no key pressed and the node is not selected // select just one setSelectedIds([clickedId]); } else if (metaPressed && isSelected) { // If we pressed keys and node was selected // we need to remove it from selection setSelectedIds(selectedIds.filter(id => id !== clickedId)); } else if (metaPressed && !isSelected) { // Add the node into selection setSelectedIds([...selectedIds, clickedId]); } }; const handleMouseDown = (e) => { // Do nothing if we mousedown on any shape if (e.target !== e.target.getStage()) { return; } // Start selection rectangle isSelecting.current = true; const pos = e.target.getStage().getPointerPosition(); setSelectionRectangle({ visible: true, x1: pos.x, y1: pos.y, x2: pos.x, y2: pos.y, }); }; const handleMouseMove = (e) => { // Do nothing if we didn't start selection if (!isSelecting.current) { return; } const pos = e.target.getStage().getPointerPosition(); setSelectionRectangle({ ...selectionRectangle, x2: pos.x, y2: pos.y, }); }; const handleMouseUp = () => { // Do nothing if we didn't start selection if (!isSelecting.current) { return; } isSelecting.current = false; // Update visibility in timeout, so we can check it in click event setTimeout(() => { setSelectionRectangle({ ...selectionRectangle, visible: false, }); }); const selBox = { x: Math.min(selectionRectangle.x1, selectionRectangle.x2), y: Math.min(selectionRectangle.y1, selectionRectangle.y2), width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1), height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1), }; // Only select shapes if selection box has actual size (not just a point click) if (selBox.width > 0 && selBox.height > 0) { const selected = rectangles.filter(rect => { // Check if rectangle intersects with selection box return Konva.Util.haveIntersection(selBox, getClientRect(rect)); }); setSelectedIds(selected.map(rect => rect.id)); } }; const handleDragEnd = (e) => { const id = e.target.id(); setRectangles(prevRects => { const newRects = [...prevRects]; const index = newRects.findIndex(r => r.id === id); if (index !== -1) { newRects[index] = { ...newRects[index], x: e.target.x(), y: e.target.y() }; } return newRects; }); }; const handleTransformEnd = (e) => { // Find which rectangle(s) were transformed const id = e.target.id(); const node = e.target; setRectangles(prevRects => { const newRects = [...prevRects]; // Update each transformed node const index = newRects.findIndex(r => r.id === id); if (index !== -1) { const scaleX = node.scaleX(); const scaleY = node.scaleY(); // Reset scale node.scaleX(1); node.scaleY(1); // Update the state with new values newRects[index] = { ...newRects[index], x: node.x(), y: node.y(), width: Math.max(5, node.width() * scaleX), height: Math.max(5, node.height() * scaleY), rotation: node.rotation(), }; } return newRects; }); }; return ( {/* Render rectangles directly */} {rectangles.map(rect => ( { if (node) { rectRefs.current.set(rect.id, node); } }} onDragEnd={handleDragEnd} onTransformEnd={handleTransformEnd} /> ))} {/* Single transformer for all selected shapes */} { // Limit resize if (newBox.width < 5 || newBox.height < 5) { return oldBox; } return newBox; }} /> {/* Selection rectangle */} {selectionRectangle.visible && ( )} ); }; export default App; ``` ```js ``` ## Transformer 不会做什么 `Transformer` 绘制控制手柄并应用缩放。对象吸附、对齐参考线、多选共用包围盒和各图形的宽高比规则都需要你自行实现。你可以参考[对象吸附](/zh-Hans/docs/sandbox/Objects_Snapping.html)中的方法。 生产环境中的编辑器除了 Transformer,还需要文本编辑、模板和导出功能。如果你不想自行实现这些功能,可以使用 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=transformer-basic)。它是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK,已提供这些功能。 --- # Node.js 配置 > 使用 canvas 或 skia-canvas 后端,在 Node.js 中配置 Konva 以进行服务器端渲染、图像生成和批处理。 Source: https://konvajs.org/zh-Hans/docs/nodejs/nodejs-setup/index.html Konva 可以在 Node.js 环境中用于服务器端渲染、图像处理和 Canvas 操作。本指南帮助你在 Node.js 项目中配置 Konva。 ## Konva 版本 10+ Konva v10+ 不再默认支持 Node.js 环境。现在需要显式导入 Canvas 后端。 ### 安装 Konva v10+ 为 Node.js 提供两种后端选项: **node-canvas 后端**: ```bash npm install konva canvas ``` **Skia 后端(性能更好)**: ```bash npm install konva skia-canvas ``` ### 使用方法 导入 Konva 和所选后端: **Canvas 后端**: ```js import Konva from 'konva'; import 'konva/canvas-backend'; ``` **Skia 后端**: ```js import Konva from 'konva'; import 'konva/skia-backend'; ``` **完整示例**: ```js import Konva from 'konva'; import 'konva/canvas-backend'; // or 'konva/skia-backend' // Create a stage const stage = new Konva.Stage({ container: 'container', // This will be ignored in Node.js width: 800, height: 600 }); // ... the rest of your konva code // Export as data URL const dataURL = stage.toDataURL(); ``` ## Konva 版本 ≤ 9(旧版) 旧版 Konva 的配置更简单: ### 安装 ```bash npm install konva ``` ### 配置 ```js const Konva = require('konva'); // Create a stage const stage = new Konva.Stage({ container: 'container', // This will be ignored in Node.js width: 800, height: 600 }); // ... the rest of your konva code // Export as data URL const dataURL = stage.toDataURL(); ``` ## 服务器端渲染注意事项 在 Node.js 中使用 Konva 时,请注意以下事项: 1. **无 DOM**:Konva 不需要 DOM,因此非常适合服务器端渲染 2. **Canvas 导出**:使用 `stage.toDataURL()` 将 Canvas 导出为图像 3. **内存管理**:处理多个 Canvas 时,请注意内存用量 4. **性能**:Konva 在 Node.js 环境中执行批处理操作时性能良好 5. **SSR 框架**:对于 Next.js 和其他 SSR 框架,可以考虑仅在客户端渲染 Canvas 内容 ## 常见使用场景 - **图像生成**:为电子邮件、报告或社交媒体创建动态图像 - **图表生成**:在服务器端生成图表 - **文档处理**:向 PDF 或其他文档添加图形 - **批处理**:并行处理多个图像或图形 --- # Konva.js 开发 AI 工具 > 使用 AI 更快地构建 Konva.js 应用。与通过 Konva 文档训练的 AI 机器人聊天,或将 Konva MCP 服务器连接到 Cursor、Claude Desktop、Windsurf 和其他 AI 编程工具。 Source: https://konvajs.org/zh-Hans/docs/ai_tools.html ## 使用 Konva 和 AI 编程 我们提供多种 AI 工具,帮助你更快地构建 Konva 应用。所有工具均由 [CrawlChat](https://www.crawlchat.com/) 提供支持。 AI 智能体会广泛使用 Konva 文档来回答问题。请注意,它是一个 LLM,与任何现代 LLM 一样,可能给出错误答案。 ## AI 聊天机器人 单击任意页面上的“Ask AI”按钮,即可询问有关 Konva 的问题。 也可以加入 [Konva Discord 社区](https://discord.gg/8FqZwVT),并在那里询问 `@AiBot-CrawlChat`。 ## MCP(模型上下文协议) MCP 是一种将 AI 编程工具连接到外部文档的标准协议。Cursor、Claude Desktop 和 Windsurf 等工具通过 Konva MCP 服务器帮助你编写代码时,可以直接访问 Konva 文档。 ### Cursor 将以下内容添加到 Cursor MCP 设置中: **重要:Cursor 仅在“Agent”模式下使用 MCP。“Ask”和其他模式不会使用 MCP。** ```json "konva-documentation": { "command": "npx", "args": [ "crawl-chat-mcp", "--id=67d221efb4b9de65095a2579", "--name=konva_documentation" ] } ``` ### Claude Desktop 将以下内容添加到 Claude Desktop 配置文件(`claude_desktop_config.json`)中: ```json { "mcpServers": { "konva-documentation": { "command": "npx", "args": [ "crawl-chat-mcp", "--id=67d221efb4b9de65095a2579", "--name=konva_documentation" ] } } } ``` 在 macOS 上,配置文件位于 `~/Library/Application Support/Claude/claude_desktop_config.json`。在 Windows 上,配置文件位于 `%APPDATA%\Claude\claude_desktop_config.json`。 ### Windsurf 将以下内容添加到 Windsurf MCP 配置中: ```json "konva-documentation": { "command": "npx", "args": [ "crawl-chat-mcp", "--id=67d221efb4b9de65095a2579", "--name=konva_documentation" ] } ``` ### 通用 MCP 命令 对于任何兼容 MCP 的工具,请使用: ``` npx crawl-chat-mcp --id=67d221efb4b9de65095a2579 --name=konva_documentation ``` ## LLM 可读文档 Konva 为 AI 工具提供机器可读的文档文件: - [`/llms.txt`](/llms.txt) — Konva 的简要概述,包含主要文档链接(遵循 [llmstxt.org](https://llmstxt.org/) 标准) - [`/llms-full.txt`](pathname:///zh-Hans/llms-full.txt) — 同一索引及所有文档页面的完整内容,适合通过单个文件读取文档的工具 - [`/llms-small.txt`](pathname:///zh-Hans/llms-small.txt) — 不包含沙盒示例的精简版本,便于放入单个模型的上下文 - [`.md`](pathname:///zh-Hans/docs/overview.md) —— 每个文档页面都在相同路径下以 `.md` 扩展名提供纯 Markdown 版本 这些文件帮助 AI 助手准确回答有关 Konva 的问题。 如果你使用 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=ai-tools) 构建设计编辑器,可以参阅其[使用 AI 构建](https://polotno.com/docs/build-with-ai?utm_source=konvajs&utm_medium=docs&utm_content=ai-tools)页面,获取对应的文档文件和 MCP 配置。Polotno 是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK。 ## 结合 Konva 使用 AI 的提示 向 AI 工具询问 Konva 时,可以通过以下方式获得更好的结果: - 在提示词中明确提及“Konva”或“react-konva” - 引用具体的 Konva 功能,例如“Transformer”“Layer”或“toDataURL” - 每次询问一项任务,不要组合多个问题 - 对照 [Konva 文档](/zh-Hans/docs/overview.html)和 [API 参考](/zh-Hans/api/Konva.html)检查 AI 生成的代码 --- # 需要 Konva 库的相关帮助? > 通过 StackOverflow、GitHub Issues、Discord 聊天、咨询和其他社区资源获取 Konva 帮助。 Source: https://konvajs.org/zh-Hans/docs/support.html ## 正在查找 Konva 框架的相关帮助? 可以使用以下方式: 1. 先在网上查找解决方案。搜索你的问题。可以使用 Google 或页面顶部的内置搜索功能。 2. [StackOverflow](https://stackoverflow.com/questions/tagged/konvajs) 是提问的最佳场所。提供在线示例、代码样本和正确标签等内容,创建高质量问题,更有可能获得好的答案。 3. 如果发现错误或想请求功能,请访问 [Issues 页面](https://github.com/konvajs/konva/issues)。 4. 如果只想讨论 Konva,可以加入 [Discord 聊天](https://discord.gg/8FqZwVT)。 5. 如果有值得分享的内容,请在 Twitter 上使用 `#konvajs` 标签。 6. 访问[更新日志](https://github.com/konvajs/konva/blob/master/CHANGELOG.md),了解最新变化。 7. 需要咨询或策略评审?请访问[咨询页面](https://lavrton.com/consulting/)。 8. 如果你需要完整的设计编辑器,而不是自定义画布,可以使用 [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=support)。它是由 Konva 维护者基于 Konva 开发的商业设计编辑器 SDK,并提供独立的文档和支持。 --- # Angular Konva 缓存教程 > 学习如何在 Angular 中使用 ng2-konva 缓存 Konva 图形,以提高 Canvas 渲染性能。 Source: https://konvajs.org/zh-Hans/docs/angular/Cache.html 缓存会将节点栅格化为内部图像。这可以提高性能,也是滤镜等某些效果的必要条件。 本示例在视图渲染后访问底层 Konva 节点,并手动调用 `cache()`。 **操作说明**:本示例显示一个带阴影样式的矩形。视图初始化后,该矩形会被缓存。 有关更多详细信息,请参阅 [Node API 参考](/zh-Hans/api/Konva.Node.html) 和 [`cache()` 文档](/zh-Hans/api/Konva.Node.html#cache)。 ## 缓存示例 ```js import { Component, ViewChild, AfterViewInit } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { RectConfig } from 'konva/lib/shapes/Rect'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App implements AfterViewInit { @ViewChild('rect') rect!: any; public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configRect: RectConfig = { x: 50, y: 50, width: 100, height: 100, fill: 'red', shadowBlur: 10, shadowColor: 'black', shadowOffsetX: 5, shadowOffsetY: 5 }; ngAfterViewInit() { if (this.rect) { this.rect.getNode().cache(); } } } ``` --- # Angular Konva 自定义图形教程 > 学习如何在 Angular 中使用 ng2-konva 和 sceneFunc 绘制函数,在 Canvas 上绘制自定义图形。 Source: https://konvajs.org/zh-Hans/docs/angular/Custom_Shape.html 要使用 `ng2-konva` 创建自定义图形,请使用 `ko-shape` 组件并提供 `sceneFunc` 绘制函数。 在 `sceneFunc` 中,可以结合使用原生 Canvas 上下文和 `context.fillStrokeShape(shape)` 等 Konva 辅助方法,正确应用填充、描边和阴影样式。 **操作说明**:本示例使用 Canvas 路径命令绘制一个类似三角形的自定义图形。 有关更多详细信息,请参阅 [Shape API 参考](/zh-Hans/api/Konva.Shape.html)。 ## 自定义图形示例 ```js import { Component } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { ShapeConfig } from 'konva/lib/shapes/Shape'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App { public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configShape: ShapeConfig = { x: 100, y: 100, fill: 'red', stroke: 'black', strokeWidth: 2, sceneFunc: (context: any, shape: any) => { context.beginPath(); context.moveTo(0, 0); context.lineTo(100, 0); context.lineTo(50, 100); context.closePath(); context.fillStrokeShape(shape); } }; } ``` --- # Angular Konva 拖放教程 > 学习如何在 Angular 中使用 ng2-konva 和事件处理程序,为 Canvas 图形实现拖放。 Source: https://konvajs.org/zh-Hans/docs/angular/Drag_And_Drop.html 要使图形可拖动,请在其配置中设置 `draggable: true`,并监听节点上的拖动事件。 开始拖动时,本示例会将被拖动的圆形移到其图层顶部。 **操作说明**:在舞台上拖动圆形。开始拖动时,观察它如何移到顶部。 有关更多详细信息,请参阅 [Node API 参考](/zh-Hans/api/Konva.Node.html) 和 [Circle API 参考](/zh-Hans/api/Konva.Circle.html)。 ## 拖放示例 ```js import { Component } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { CircleConfig } from 'konva/lib/shapes/Circle'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App { public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configCircle: CircleConfig = { x: 100, y: 100, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, draggable: true, }; public handleDragstart(event: any): void { event.target.moveToTop(); } } ``` --- # Angular Konva 事件教程 > 学习如何在 Angular 中使用 ng2-konva 事件绑定,处理 Canvas 图形上的鼠标、触摸和指针事件。 Source: https://konvajs.org/zh-Hans/docs/angular/Events.html 要在 Angular 中处理指针事件,请将 `(mousemove)` 和 `(mouseout)` 等监听器直接附加到 `ng2-konva` 组件。 当鼠标移到三角形上时,本示例会根据舞台指针坐标更新文本标签。 **操作说明**:将鼠标移到三角形上以更新坐标标签。然后将鼠标移出,以重置文本。 有关更多详细信息,请参阅 [Node API 参考](/zh-Hans/api/Konva.Node.html)和 [Stage API 参考](/zh-Hans/api/Konva.Stage.html)。 ## 事件示例 ```js import { Component } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { RegularPolygonConfig } from 'konva/lib/shapes/RegularPolygon'; import { TextConfig } from 'konva/lib/shapes/Text'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App { public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configTriangle: RegularPolygonConfig = { x: 80, y: 120, sides: 3, radius: 80, fill: '#00D2FF', stroke: 'black', strokeWidth: 4 }; public configText: TextConfig = { x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: 'hello', fill: 'black' }; public handleMouseMove(event: any): void { const mousePos = event.target.getStage().getPointerPosition(); const x = mousePos.x - 190; const y = mousePos.y - 40; this.configText = { ...this.configText, text: 'x: ' + x + ', y: ' + y }; } public handleMouseOut(): void { this.configText = { ...this.configText, text: 'Mouseout triangle' }; } } ``` --- # Angular Konva 滤镜教程 > 学习如何在 Angular 中配合缓存使用 ng2-konva,对 Canvas 图形应用模糊等视觉滤镜。 Source: https://konvajs.org/zh-Hans/docs/angular/Filters.html 要在 Konva 中应用滤镜,请设置 `filters` 属性,并在节点挂载后缓存该节点。 本示例对圆形使用模糊滤镜,并在底层 Konva 节点可用后调用 `cache()`。 **操作说明**:本示例渲染一个模糊的红色圆形。在编辑器中移除 `cache()`,以了解滤镜为何需要缓存。 有关更多详细信息,请参阅 [Filters API 参考](/zh-Hans/api/Konva.Filters.html) 和 [`cache()` 文档](/zh-Hans/api/Konva.Node.html#cache)。 ## 滤镜示例 ```js import { Component, ViewChild } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { CircleConfig } from 'konva/lib/shapes/Circle'; import Konva from 'konva'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App { @ViewChild('circle') circle!: any; public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configCircle: CircleConfig = { x: 150, y: 150, radius: 50, fill: 'red', filters: [Konva.Filters.Blur], blurRadius: 15 }; ngAfterViewInit() { this.circle.getNode().cache({ offset: 10 }); } } ``` --- # Angular Konva 图像教程 > 学习如何在 Angular 中使用 ng2-konva Image 组件,在 HTML5 Canvas 上加载并显示图像。 Source: https://konvajs.org/zh-Hans/docs/angular/Images.html 要在 Angular 中使用 `Konva` 显示图像,请使用 `ko-image`,并将已加载的 `HTMLImageElement` 分配给 `image` 属性。 Angular 默认使用 zoneless 模式,因此 signals 是从 `Image.onload` 等异步回调更新图像配置的最简单方法。 **操作说明**:本示例加载外部图像。浏览器完成加载后,示例会渲染该图像。 有关更多详细信息,请参阅 [Image API 参考](/zh-Hans/api/Konva.Image.html)。 ## 图像示例 ```js import { Component, OnInit, signal } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { ImageConfig } from 'konva/lib/shapes/Image'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App implements OnInit { public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configImage = signal({ x: 50, y: 50, image: null, width: 100, height: 100 }); ngOnInit() { const imageObj = new Image(); imageObj.onload = () => { this.configImage.update((config) => ({ ...config, image: imageObj })); }; imageObj.src = 'https://konvajs.org/assets/yoda.jpg'; } } ``` --- # Angular Konva 图形教程 > 学习如何在 Angular 中使用 ng2-konva 图形组件,在 Canvas 上绘制矩形、圆形、线条和文本。 Source: https://konvajs.org/zh-Hans/docs/angular/Shapes.html 所有 `ng2-konva` 图形组件都通过 `ko-` 前缀映射到 Konva 图形,因此可以通过 `config` 对象传递任何常规 Konva 图形配置。 本示例展示几个使用不同样式选项的常见图形,例如阴影、渐变和线条张力。 **操作说明**:本示例渲染文本、矩形、圆形,以及使用渐变填充的闭合线条。 有关更多详细信息,请参阅 [Konva API 参考](/zh-Hans/api/Konva.html)和 [Line API 参考](/zh-Hans/api/Konva.Line.html)。 ## 图形示例 ```js import { Component } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { TextConfig } from 'konva/lib/shapes/Text'; import { RectConfig } from 'konva/lib/shapes/Rect'; import { CircleConfig } from 'konva/lib/shapes/Circle'; import { LineConfig } from 'konva/lib/shapes/Line'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App { public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configText: TextConfig = { text: 'Some text on canvas', fontSize: 15 }; public configRect: RectConfig = { x: 20, y: 50, width: 100, height: 100, fill: 'red', shadowBlur: 10 }; public configCircle: CircleConfig = { x: 200, y: 100, radius: 50, fill: 'green' }; public configLine: LineConfig = { x: 20, y: 200, points: [0, 0, 100, 0, 100, 100], tension: 0.5, closed: true, stroke: 'black', fillLinearGradientStartPoint: { x: -50, y: -50 }, fillLinearGradientEndPoint: { x: 50, y: 50 }, fillLinearGradientColorStops: [0, 'red', 1, 'yellow'] }; } ``` --- # Angular Konva 简单动画教程 > 学习如何在 Angular 中使用 ng2-konva 和 Konva.Animation 类创建简单的 Canvas 动画。 Source: https://konvajs.org/zh-Hans/docs/angular/Simple_Animations.html Konva 提供两种主要动画工具:`node.to()` 用于简单过渡,`Konva.Animation` 用于逐帧更新。 视图初始化后,本示例直接使用 `Konva.Animation`,使圆形沿正弦波移动。 **操作说明**:本示例让红色圆形持续左右移动。 有关更多详细信息,请参阅 [动画文档](/zh-Hans/docs/animations/Rotation.html)和 [Node API 参考](/zh-Hans/api/Konva.Node.html)。 ## 简单动画示例 ```js import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { CircleConfig } from 'konva/lib/shapes/Circle'; import Konva from 'konva'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App implements OnInit, OnDestroy { @ViewChild('circle') circle!: any; private animation: any = null; public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configCircle: CircleConfig = { x: 100, y: 100, radius: 50, fill: 'red', stroke: 'black', strokeWidth: 4 }; ngAfterViewInit() { const circle = this.circle.getNode(); this.animation = new Konva.Animation((frame: any) => { const time = frame.time; const x = 100 + Math.sin(time / 1000) * 100; circle.x(x); }, circle.getLayer()); this.animation.start(); } ngOnDestroy() { if (this.animation) { this.animation.stop(); } } } ``` --- # Angular Konva Transformer 教程 > 学习如何在 Angular 中使用 ng2-konva Transformer 进行交互式选择,以及调整 Canvas 图形尺寸并旋转图形。 Source: https://konvajs.org/zh-Hans/docs/angular/Transformer.html Transformer 工具以命令式方式附加到节点。在 Angular 中,这意味着创建 `ko-transformer`,并在视图就绪后将它连接到所选图形。 **操作说明**:单击矩形以选择它,拖动矩形以移动它,然后单击空白舞台以清除选择。 有关更多详细信息,请参阅 [Transformer API 参考](/zh-Hans/api/Konva.Transformer.html)。 ## Transformer 示例 ```js import { Component, ViewChild, AfterViewInit } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { RectConfig } from 'konva/lib/shapes/Rect'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` `, imports: [StageComponent, CoreShapeComponent], }) export default class App implements AfterViewInit { @ViewChild('rect') rect!: any; @ViewChild('transformer') transformer!: any; public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configRect: RectConfig = { x: 100, y: 100, width: 100, height: 100, fill: 'red', stroke: 'black', strokeWidth: 4, draggable: true }; ngAfterViewInit() { this.transformer.getNode().nodes([this.rect.getNode()]); } public handleStageClick(event: any): void { if (event?.target === event.target.getStage()) { this.transformer.getNode().nodes([]); } } public handleShapeClick(): void { this.transformer.getNode().nodes([this.rect.getNode()]); } } ``` --- # Angular Konva 撤销/重做教程 > 学习如何在 Angular 中使用 ng2-konva 跟踪 state 历史记录,为 Canvas 交互实现撤销和重做。 Source: https://konvajs.org/zh-Hans/docs/angular/Undo-Redo.html 将 Canvas state 存储在普通数据对象中,而不是在每次渲染时从 Canvas 读回值,可使撤销和重做达到最佳效果。 本示例将矩形位置存储在小型历史记录栈中。单击撤销或重做时,示例会恢复之前的快照。 **操作说明**:拖动矩形以创建历史记录条目,然后单击撤销和重做以浏览已保存的 state。 有关更多详细信息,请参阅 [Rect API 参考](/zh-Hans/api/Konva.Rect.html)。 ## 撤销/重做示例 ```js import { Component } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { RectConfig } from 'konva/lib/shapes/Rect'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: `
`, imports: [StageComponent, CoreShapeComponent], }) export default class App { private history: RectConfig[] = []; private currentIndex: number = -1; public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public configRect: RectConfig = { x: 100, y: 100, width: 100, height: 100, fill: 'red', draggable: true }; constructor() { this.saveState(); } private saveState(): void { // Remove any states after current index this.history = this.history.slice(0, this.currentIndex + 1); // Add current state this.history.push({ ...this.configRect }); this.currentIndex++; } public handleDragEnd(event: any): void { this.configRect = { ...this.configRect, x: event.target.x(), y: event.target.y() }; this.saveState(); } public undo(): void { if (this.canUndo()) { this.currentIndex--; this.configRect = { ...this.history[this.currentIndex] }; } } public redo(): void { if (this.canRedo()) { this.currentIndex++; this.configRect = { ...this.history[this.currentIndex] }; } } public canUndo(): boolean { return this.currentIndex > 0; } public canRedo(): boolean { return this.currentIndex < this.history.length - 1; } } ``` ## 手动构建历史记录的局限 上述历史记录每一步只记录一个值。生产级编辑器必须记录 成组操作,使多选拖动可以作为一个步骤撤销,还必须记录 变换,以及操作完成后才加载完的图像。该状态机 通常比绘图代码更庞大,因此请围绕文档操作而不是原始节点状态来设计历史记录。 --- # Angular Konva 层叠顺序教程 > 学习如何在 Angular 中使用 ng2-konva,通过管理数据数组控制图形层叠顺序和 z-index。 Source: https://konvajs.org/zh-Hans/docs/angular/zIndex.html 要在 Angular 中控制图形层叠顺序,请更新用于渲染图形的数据数组顺序。 本示例展示以下操作: 1. 创建一个包含随机位置和颜色的圆形图形数组 2. 处理拖动事件以更新图形的视觉顺序 3. 通过操作数组顺序保持正确的层叠顺序 4. 通过 Angular state 驱动渲染,而不是以命令式方式调用 `zIndex()` **操作说明**:尝试拖动圆形。开始拖动时,它会自动移到层叠顶部。此示例通过操作数据中的圆形数组实现该效果,而不是手动更改 zIndex。 ```js import { Component, OnInit } from '@angular/core'; import { StageConfig } from 'konva/lib/Stage'; import { CircleConfig } from 'konva/lib/shapes/Circle'; import { CoreShapeComponent, StageComponent, } from 'ng2-konva'; @Component({ selector: 'app-root', standalone: true, template: ` @for (item of items; track trackById($index, item)) { } `, imports: [StageComponent, CoreShapeComponent], }) export default class App implements OnInit { public configStage: StageConfig = { width: window.innerWidth, height: window.innerHeight, }; public items: CircleConfig[] = []; private dragItemId: string | null = null; ngOnInit() { this.generateItems(); } private generateItems(): void { const newItems: CircleConfig[] = []; for (let i = 0; i < 10; i++) { newItems.push({ x: Math.random() * this.configStage.width!, y: Math.random() * this.configStage.height!, radius: 50, id: "node-" + i, fill: this.getRandomColor(), draggable: true }); } this.items = newItems; } private getRandomColor(): string { const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9']; return colors[Math.floor(Math.random() * colors.length)]; } public handleDragstart(event: any): void { this.dragItemId = event.target.id(); const item = this.items.find(i => i.id === this.dragItemId); if (item) { this.items = [ ...this.items.filter((i) => i.id !== this.dragItemId), item ]; } } public handleDragend(): void { this.dragItemId = null; } public trackById(index: number, item: CircleConfig): string { return item.id as string; } } ``` --- # 位置动画教程 > 学习如何使用 Konva.js 为 HTML5 Canvas 上的图形位置设置动画。使用 Konva.Animation 逐帧更新,使图形平滑移动。 Source: https://konvajs.org/zh-Hans/docs/animations/Moving.html 如需使用 Konva 为图形的位置设置动画,可以通过 `Konva.Animation` 创建新动画。 此动画会在每一帧修改图形的位置。 如需查看完整的属性和方法列表,请参阅 [Konva.Animation 文档](/zh-Hans/api/Konva.Animation.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: 50, y: window.innerHeight / 2, radius: 30, fill: 'red', stroke: 'black', strokeWidth: 4, }); layer.add(circle); const amplitude = 100; const period = 2000; // in milliseconds const anim = new Konva.Animation(function(frame) { circle.x( amplitude * Math.sin((frame.time * 2 * Math.PI) / period) + window.innerWidth / 2 ); }, layer); anim.start(); ```` ```js import { Stage, Layer, Circle } from 'react-konva'; import { useEffect, useRef } from 'react'; const App = () => { const circleRef = useRef(null); useEffect(() => { const amplitude = 100; const period = 2000; // in milliseconds const anim = new Konva.Animation((frame) => { circleRef.current.x( amplitude * Math.sin((frame.time * 2 * Math.PI) / period) + window.innerWidth / 2 ); }, circleRef.current.getLayer()); anim.start(); return () => { anim.stop(); }; }, []); return ( ); }; export default App; ```` ```js ``` --- # 旋转动画教程 > 学习如何使用 Konva.js 为 HTML5 Canvas 上的图形旋转设置动画。使用 Konva.Animation 创建平滑的旋转动画。 Source: https://konvajs.org/zh-Hans/docs/animations/Rotation.html 如需使用 Konva 为图形的旋转设置动画,可以通过 `Konva.Animation` 创建新动画,并定义一个在每一帧修改图形旋转角度的函数。 本教程将使蓝色矩形绕左上角旋转, 黄色矩形绕中心旋转,并使红色矩形绕图形外部的一点旋转。 如需查看完整的属性和方法列表,请参阅 [Konva.Animation 文档](/zh-Hans/api/Konva.Animation.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); // blue rectangle - rotate around top-left corner const blueRect = new Konva.Rect({ x: 50, y: 50, width: 100, height: 50, fill: '#00D2FF', stroke: 'black', strokeWidth: 4, offset: { x: 0, y: 0, }, }); // yellow rectangle - rotate around center const yellowRect = new Konva.Rect({ x: 200, y: 50, width: 100, height: 50, fill: 'yellow', stroke: 'black', strokeWidth: 4, offset: { x: 50, y: 25, }, }); // red rectangle - rotate around point outside shape const redRect = new Konva.Rect({ x: 350, y: 50, width: 100, height: 50, fill: 'red', stroke: 'black', strokeWidth: 4, offset: { x: -50, y: 25, }, }); layer.add(blueRect); layer.add(yellowRect); layer.add(redRect); const angularSpeed = 90; const anim = new Konva.Animation(function(frame) { const angleDiff = (frame.timeDiff * angularSpeed) / 1000; blueRect.rotate(angleDiff); yellowRect.rotate(angleDiff); redRect.rotate(angleDiff); }, layer); anim.start(); ```` ```js import { Stage, Layer, Rect } from 'react-konva'; import { useEffect, useRef } from 'react'; const App = () => { const blueRectRef = useRef(null); const yellowRectRef = useRef(null); const redRectRef = useRef(null); useEffect(() => { const angularSpeed = 90; const anim = new Konva.Animation((frame) => { const angleDiff = (frame.timeDiff * angularSpeed) / 1000; blueRectRef.current.rotate(angleDiff); yellowRectRef.current.rotate(angleDiff); redRectRef.current.rotate(angleDiff); }, blueRectRef.current.getLayer()); anim.start(); return () => { anim.stop(); }; }, []); return ( ); }; export default App; ```` ```js ``` --- # HTML5 Canvas Konva 缩放动画教程 > 学习如何使用 Konva.Animation 以及 scaleX 和 scaleY 属性,为 HTML5 Canvas 上的图形缩放设置动画。 Source: https://konvajs.org/zh-Hans/docs/animations/Scaling.html 如需使用 Konva 为图形的缩放设置动画,可以通过 `Konva.Animation` 创建新动画,并定义一个在每一帧修改图形缩放比例的函数。 本教程将缩放蓝色六边形的 x 和 y 分量、黄色六边形的 y 分量, 以及红色六边形的 x 分量。红色六边形围绕位于图形右侧的轴缩放。 **操作说明:** 在六边形播放动画时拖放它们。 如需查看完整的属性和方法列表,请参阅 [Konva.Animation 文档](/zh-Hans/api/Konva.Animation.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); // blue hexagon - scale x and y const blueHex = new Konva.RegularPolygon({ x: 50, y: 50, sides: 6, radius: 20, fill: '#00D2FF', stroke: 'black', strokeWidth: 4, draggable: true }); // yellow hexagon - scale y only const yellowHex = new Konva.RegularPolygon({ x: 150, y: 50, sides: 6, radius: 20, fill: 'yellow', stroke: 'black', strokeWidth: 4, draggable: true }); // red hexagon - scale x only const redHex = new Konva.RegularPolygon({ x: 250, y: 50, sides: 6, radius: 20, fill: 'red', stroke: 'black', strokeWidth: 4, draggable: true }); layer.add(blueHex); layer.add(yellowHex); layer.add(redHex); const period = 2000; const anim = new Konva.Animation(function(frame) { const scale = Math.sin(frame.time * 2 * Math.PI / period) + 2; // blue hex - scale x and y blueHex.scale({ x: scale, y: scale }); // yellow hex - scale y only yellowHex.scaleY(scale); // red hex - scale x only redHex.scaleX(scale); }, layer); anim.start(); ```` ```js import { Stage, Layer, RegularPolygon } from 'react-konva'; import { useEffect, useRef, useState } from 'react'; const App = () => { const blueHexRef = useRef(null); const yellowHexRef = useRef(null); const redHexRef = useRef(null); const [positions, setPositions] = useState({ blue: { x: 50, y: 50 }, yellow: { x: 150, y: 50 }, red: { x: 250, y: 50 } }); useEffect(() => { const period = 2000; const anim = new Konva.Animation((frame) => { const scale = Math.sin(frame.time * 2 * Math.PI / period) + 2; // blue hex - scale x and y blueHexRef.current.scale({ x: scale, y: scale }); // yellow hex - scale y only yellowHexRef.current.scaleY(scale); // red hex - scale x only redHexRef.current.scaleX(scale); }, blueHexRef.current.getLayer()); anim.start(); return () => { anim.stop(); }; }, []); const handleDragEnd = (e, color) => { setPositions(prev => ({ ...prev, [color]: { x: e.target.x(), y: e.target.y() } })); }; return ( handleDragEnd(e, 'blue')} /> handleDragEnd(e, 'yellow')} /> handleDragEnd(e, 'red')} /> ); }; export default App; ```` ```js ``` --- # HTML5 Canvas Konva 停止动画教程 > 学习如何使用 Konva.Animation 的 start() 和 stop() 方法,启动和停止 Konva 中的 Canvas 动画。 Source: https://konvajs.org/zh-Hans/docs/animations/Stop_Animation.html 如需使用 Konva 停止动画,可以使用 `stop()` 方法。 如需重新启动动画,可以再次调用 `start()`。 **操作说明:** 单击“Start”启动动画,单击“Stop”停止动画。 如需查看完整的属性和方法列表,请参阅 [Konva.Animation 文档](/zh-Hans/api/Konva.Animation.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 30, fill: 'red', stroke: 'black', strokeWidth: 4, }); layer.add(circle); // add buttons const container = document.createElement('div'); document.body.appendChild(container); container.style.position = 'absolute'; container.style.top = '0px'; container.style.left = '0px'; const startBtn = document.createElement('button'); startBtn.textContent = 'Start Animation'; container.appendChild(startBtn); const stopBtn = document.createElement('button'); stopBtn.textContent = 'Stop Animation'; container.appendChild(stopBtn); const anim = new Konva.Animation(function(frame) { circle.x( amplitude * Math.sin((frame.time * 2 * Math.PI) / period) + stage.width() / 2 ); }, layer); const amplitude = 100; const period = 2000; startBtn.addEventListener('click', () => anim.start()); stopBtn.addEventListener('click', () => anim.stop()); ```` ```js import { Stage, Layer, Circle } from 'react-konva'; import { useEffect, useRef, useState } from 'react'; const App = () => { const circleRef = useRef(null); const [isAnimating, setIsAnimating] = useState(false); const animRef = useRef(null); useEffect(() => { const amplitude = 100; const period = 2000; animRef.current = new Konva.Animation((frame) => { circleRef.current.x( amplitude * Math.sin((frame.time * 2 * Math.PI) / period) + window.innerWidth / 2 ); }, circleRef.current.getLayer()); return () => { if (animRef.current) { animRef.current.stop(); } }; }, []); const handleStart = () => { animRef.current.start(); setIsAnimating(true); }; const handleStop = () => { animRef.current.stop(); setIsAnimating(false); }; return (
); }; export default App; ```` ```js ``` --- # 文本动画教程 > 学习如何在 Konva 中使用 charRenderFunc 属性创建逐字符文本动画,为每个字符应用渲染效果。 Source: https://konvajs.org/zh-Hans/docs/animations/Text_Animations.html **注意:此功能仅在 Konva v10.0.0 及更高版本中可用。** Konva 通过 `charRenderFunc` 属性提供文本动画功能。此函数让你可以自定义每个字符的渲染方式,从而创建逐字符动画和效果。 ```js var text = new Konva.Text({ x: 10, y: 10, text: 'AB', fontSize: 20, charRenderFunc: function ({ context, index }) { if (index === 1) { // shift only the second character context.translate(0, 10); } }, }); ``` `charRenderFunc` 接收一个上下文对象,其中包含以下参数: - **`char`** - 实际渲染的字符字符串 - **`index`** - 字符在完整文本中的从零开始的索引 - **`x`** - 渲染字符的 X 坐标 - **`y`** - 渲染字符的 Y 坐标 - **`lineIndex`** - 包含此字符的行的从零开始的索引 - **`column`** - 当前行中从零开始的列位置 - **`isLastInLine`** - 指示此字符是否为所在行最后一个字符的布尔值 - **`width`** - 字符宽度 - **`context`** - Canvas 2D 渲染上下文,用于应用变换、不透明度和颜色等效果 你可以根据各个字符的位置和属性,为它们应用变换、不透明度变化或其他效果。 如果你的效果会将字符移出文本框,请使用 `selfRectFunc`(需要 Konva 10.7.0 或更高版本)描述字符可能到达的区域。这样,缓存、[隔离组](/zh-Hans/docs/groups_and_layers/Isolated_Groups.html)和 Transformer 都会包含这些字符: ```javascript const text = new Konva.Text({ text: 'Bouncing text', // characters jump up to 20px above the text box selfRectFunc: (shape) => ({ x: 0, y: -20, width: shape.width(), height: shape.height() + 20, }), charRenderFunc: ({ context, index }) => { context.translate(0, -Math.abs(Math.sin(index)) * 20); }, }); ``` ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); // we will store the opacity of each character in an array const charOpacities = []; const textNode = new Konva.Text({ x: window.innerWidth / 2 - 100, y: window.innerHeight / 2 - 20, text: 'ANIMATION', fontSize: 40, fontFamily: 'Arial', fill: '#333', charRenderFunc: function ({ context, index }) { context.globalAlpha = charOpacities[index]; }, }); layer.add(textNode); const anim = new Konva.Animation(function(frame) { const time = frame.time; const cycleDuration = 4000; // 4 seconds total cycle const fadeInDuration = 1500; // 1.5 seconds to fade in all const holdDuration = 1000; // 1 second hold const fadeOutDuration = 1500; // 1.5 seconds to fade out all const cycleTime = time % cycleDuration; for (let i = 0; i < textNode.text().length; i++) { const charDelay = i * 150; // 150ms delay between characters if (cycleTime < fadeInDuration) { // Fade in phase const charStartTime = charDelay; const charFadeTime = Math.max(0, cycleTime - charStartTime); charOpacities[i] = Math.min(1, charFadeTime / 300); } else if (cycleTime < fadeInDuration + holdDuration) { // Hold phase - all characters visible charOpacities[i] = 1; } else { // Fade out phase const fadeOutStart = fadeInDuration + holdDuration; const charFadeOutDelay = i * 150; // Same order as fade in const charFadeOutTime = Math.max(0, cycleTime - fadeOutStart - charFadeOutDelay); charOpacities[i] = Math.max(0, 1 - charFadeOutTime / 300); } } }, layer); anim.start(); ``` ```js import { Stage, Layer, Text } from 'react-konva'; import { useEffect, useRef } from 'react'; const App = () => { const textRef = useRef(null); const layerRef = useRef(null); const charOpacitiesRef = useRef([]); useEffect(() => { const anim = new Konva.Animation((frame) => { const time = frame.time; const cycleDuration = 4000; // 4 seconds total cycle const fadeInDuration = 1500; // 1.5 seconds to fade in all const holdDuration = 1000; // 1 second hold const fadeOutDuration = 1500; // 1.5 seconds to fade out all const cycleTime = time % cycleDuration; for (let i = 0; i < textRef.current.text().length; i++) { const charDelay = i * 150; // 150ms delay between characters if (cycleTime < fadeInDuration) { // Fade in phase const charStartTime = charDelay; const charFadeTime = Math.max(0, cycleTime - charStartTime); charOpacitiesRef.current[i] = Math.min(1, charFadeTime / 300); } else if (cycleTime < fadeInDuration + holdDuration) { // Hold phase - all characters visible charOpacitiesRef.current[i] = 1; } else { // Fade out phase const fadeOutStart = fadeInDuration + holdDuration; const charFadeOutDelay = i * 150; // Same order as fade in const charFadeOutTime = Math.max(0, cycleTime - fadeOutStart - charFadeOutDelay); charOpacitiesRef.current[i] = Math.max(0, 1 - charFadeOutTime / 300); } } }, layerRef.current); anim.start(); return () => { anim.stop(); }; }, []); return ( { context.globalAlpha = charOpacitiesRef.current[index] || 0; }} /> ); }; export default App; ``` ```js ``` --- # 裁剪函数教程 > 学习如何在 Konva 中使用 clipFunc 自定义裁剪函数,为组和图层创建复杂的裁剪区域。 Source: https://konvajs.org/zh-Hans/docs/clipping/Clipping_Function.html ## 如何裁剪图层中的节点? 要使用 Konva 在复杂的裁剪区域内绘制内容,可以设置组或图层的 `clipFunc` 属性。 在本教程中,我们将在应用到组的双圆裁剪区域内绘制多个圆形。 ```js import Konva from 'konva'; // First we need to create stage const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); // Then create layer const layer = new Konva.Layer(); const group = new Konva.Group({ clipFunc: function (ctx) { ctx.beginPath(); ctx.arc(200, 120, 50, 0, Math.PI * 2, false); ctx.arc(280, 120, 50, 0, Math.PI * 2, false); }, }); for (let i = 0; i < 20; i++) { const blob = new Konva.Circle({ x: Math.random() * stage.width(), y: Math.random() * stage.height(), radius: Math.random() * 50, fill: 'green', opacity: 0.8, }); group.add(blob); } // add the shape to the layer layer.add(group); // add the layer to the stage stage.add(layer); ``` ```js import { Stage, Layer, Group, Circle } from 'react-konva'; const App = () => { const blobs = Array.from({ length: 20 }, (_, i) => ({ x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, radius: Math.random() * 50, })); return ( { ctx.beginPath(); ctx.arc(200, 120, 50, 0, Math.PI * 2, false); ctx.arc(280, 120, 50, 0, Math.PI * 2, false); }} > {blobs.map((blob, i) => ( ))} ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 简单裁剪教程 > 学习如何在 Konva 中使用 clip 属性,为组和图层应用简单的矩形裁剪区域。 Source: https://konvajs.org/zh-Hans/docs/clipping/Clipping_Regions.html 要使用 Konva 在裁剪区域内绘制内容,可以设置组或图层的 `clip` 属性。 裁剪区域由 `x`、`y`、`width` 和 `height` 定义。在本教程中, 我们将在应用到组的矩形裁剪区域内绘制多个圆形。 对于更复杂的情况,请参阅裁剪函数。[裁剪函数](/zh-Hans/docs/clipping/Clipping_Function.html) ```js import Konva from 'konva'; // First we need to create stage const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); // Then create layer const layer = new Konva.Layer(); const group = new Konva.Group({ clip: { x: 100, y: 20, width: 200, height: 200, }, }); for (let i = 0; i < 20; i++) { const blob = new Konva.Circle({ x: Math.random() * stage.width(), y: Math.random() * stage.height(), radius: Math.random() * 50, fill: 'green', opacity: 0.8, }); group.add(blob); } // add the shape to the layer layer.add(group); // add the layer to the stage stage.add(layer); ``` ```js import { Stage, Layer, Group, Circle } from 'react-konva'; const App = () => { const blobs = Array.from({ length: 20 }, (_, i) => ({ x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, radius: Math.random() * 50, })); return ( {blobs.map((blob, i) => ( ))} ); }; export default App; ``` ```js ``` --- # 保存和加载 HTML5 Canvas 舞台的最佳实践 > 使用 Konva.js 保存和加载 HTML5 Canvas 状态的最佳实践,包括序列化、数据管理和状态持久化技巧。 Source: https://konvajs.org/zh-Hans/docs/data_and_serialization/Best_Practices.html ## 保存和加载完整舞台内容并实现撤销和重做的最佳方式是什么? 如果要保存或加载简单的 Canvas 内容,可以使用内置的 `Konva` 方法:`node.toJSON()` 和 `Node.create(json)`。 请参阅[简单加载](/zh-Hans/docs/data_and_serialization/Simple_Load.html)和[复杂加载](/zh-Hans/docs/data_and_serialization/Complex_Load.html)示例。 但这些方法只适用于很小的应用。在较大的应用中,这些方法很难使用。因为大型应用的树结构通常很复杂,其中可能包含大量事件监听器、图像和滤镜等内容。这些数据无法序列化为 JSON,或很难完成序列化。 树中的节点通常还包含大量与应用状态没有直接关系的信息。这些信息仅用于描述应用的视觉外观。 例如,假设一个游戏在 Canvas 中绘制了多个球。每个球不仅是一个圆形,还是一个复杂的视觉对象组,其中包含阴影和文本(例如“中国制造”)。现在,假设你要序列化应用状态并在其他位置使用,例如发送到另一台计算机或实现撤销和重做。几乎所有视觉信息(阴影、文本和尺寸)都不是关键信息,可能不需要保存,因为所有球都使用相同的阴影和尺寸等设置。真正关键的信息只有球的数量及其坐标。你只需要保存和加载这些信息。它们只是一个简单的数组: ```javascript var state = [{x: 10, y: 10}, { x: 160, y: 1041}] ``` 获得这些信息后,你需要一个可以创建完整 Canvas 结构的函数。 如果要更新 Canvas,例如创建一个新球,不需要直接创建新的 Canvas 节点(例如创建 `Konva.Circle` 的新实例)。只需向状态中添加一个新对象,然后更新或重新创建 Canvas。 这样,在保存和加载阶段就不需要处理图像加载、滤镜和事件监听器等内容。因为这些操作都在 `create` 或 `update` 函数中完成。 如果你了解 `React`、`Vue` 和 `Angular` 等现代框架的工作方式,就能更好地理解这种方法。 还可以查看以下示例以进一步了解这种方法: 1. [使用 React 实现撤销和重做](/zh-Hans/docs/react/Undo-Redo.html) 1. [使用 Vue 实现保存和加载](/zh-Hans/docs/vue/Save-Load.html) 如何实现 `create` 和 `update` 函数取决于具体情况。使用 [react-konva](/zh-Hans/docs/react/index.html) 等能处理这些工作的框架会更容易。 如果不想使用此类框架,需要根据自己的应用来设计。下面的小型示例介绍一种实现方式。 最简单的方法是只实现一个 `create(state)` 函数,由它完成所有复杂的加载工作。 应用发生更改时,只需销毁 Canvas 并创建一个新 Canvas。但这种方法可能导致性能下降。 更合理的实现是创建 `create(state)` 和 `update(state)` 两个函数。`create` 创建所有必需对象的实例、绑定事件并加载图像。`update` 更新节点的属性。如果对象数量发生变化,则销毁所有对象并从头创建。如果只有部分属性发生变化,则调用 `update`。 **操作说明:**此示例包含多个带滤镜的图像。你可以添加和移动图像,单击图像以应用新滤镜,并使用撤销和重做功能。 ```js import Konva from 'konva'; // Initial state let state = { images: [ { x: 50, y: 50, filter: 'none' }, { x: 150, y: 50, filter: 'blur' } ] }; // History for undo/redo const history = [JSON.stringify(state)]; let historyStep = 0; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); // Create container const container = document.createElement('div'); container.style.position = 'relative'; document.body.appendChild(container); // Create button container const buttonContainer = document.createElement('div'); buttonContainer.style.position = 'absolute'; buttonContainer.style.top = '10px'; buttonContainer.style.left = '10px'; buttonContainer.style.zIndex = '10'; container.appendChild(buttonContainer); // Create UI buttons const addButton = document.createElement('button'); addButton.textContent = 'Add Image'; addButton.style.margin = '0 5px'; buttonContainer.appendChild(addButton); const undoButton = document.createElement('button'); undoButton.textContent = 'Undo'; undoButton.style.margin = '0 5px'; buttonContainer.appendChild(undoButton); const redoButton = document.createElement('button'); redoButton.textContent = 'Redo'; redoButton.style.margin = '0 5px'; buttonContainer.appendChild(redoButton); // Move stage container into our container const stageContainer = document.getElementById('container'); container.appendChild(stageContainer); stageContainer.style.position = 'absolute'; stageContainer.style.top = '0'; stageContainer.style.left = '0'; // Load image const imageObj = new Image(); imageObj.src = 'https://konvajs.org/assets/lion.png'; function createImage(imageConfig) { const image = new Konva.Image({ image: imageObj, x: imageConfig.x, y: imageConfig.y, width: 100, height: 100, draggable: true }); if (imageConfig.filter === 'blur') { image.cache(); image.filters([Konva.Filters.Blur]); image.blurRadius(10); } return image; } function create(state) { layer.destroyChildren(); state.images.forEach(imgConfig => { const image = createImage(imgConfig); image.on('dragend', () => { const pos = image.position(); const index = layer.children.indexOf(image); state.images[index] = { ...state.images[index], x: pos.x, y: pos.y }; saveHistory(); }); image.on('click', () => { const index = layer.children.indexOf(image); state.images[index] = { ...state.images[index], filter: state.images[index].filter === 'none' ? 'blur' : 'none' }; saveHistory(); create(state); }); layer.add(image); }); } function saveHistory() { historyStep++; history.length = historyStep; history.push(JSON.stringify(state)); } // Add event listeners addButton.addEventListener('click', () => { state.images.push({ x: Math.random() * stage.width(), y: Math.random() * stage.height(), filter: 'none' }); saveHistory(); create(state); }); undoButton.addEventListener('click', () => { if (historyStep === 0) return; historyStep--; state = JSON.parse(history[historyStep]); create(state); }); redoButton.addEventListener('click', () => { if (historyStep === history.length - 1) return; historyStep++; state = JSON.parse(history[historyStep]); create(state); }); imageObj.onload = () => { create(state); }; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const FilteredImage = ({ filter, ...props }) => { const imageRef = useRef(); useEffect(() => { imageRef.current?.cache(); }, [props.image]); return ( ); }; const App = () => { const [images, setImages] = useState([ { x: 50, y: 50, filter: 'none' }, { x: 150, y: 50, filter: 'blur' } ]); const [history, setHistory] = useState([]); const [historyStep, setHistoryStep] = useState(0); const [lionImage] = useImage('https://konvajs.org/assets/lion.png', 'anonymous'); useEffect(() => { if (lionImage) { setHistory([JSON.stringify(images)]); } }, [lionImage]); const handleDragEnd = (index, e) => { const newImages = [...images]; newImages[index] = { ...newImages[index], x: e.target.x(), y: e.target.y() }; setImages(newImages); saveHistory(newImages); }; const handleClick = (index) => { const newImages = [...images]; newImages[index] = { ...newImages[index], filter: newImages[index].filter === 'none' ? 'blur' : 'none' }; setImages(newImages); saveHistory(newImages); }; const saveHistory = (newImages) => { const newHistory = history.slice(0, historyStep + 1); newHistory.push(JSON.stringify(newImages)); setHistory(newHistory); setHistoryStep(newHistory.length - 1); }; const handleAdd = () => { const newImages = [...images, { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, filter: 'none' }]; setImages(newImages); saveHistory(newImages); }; const handleUndo = () => { if (historyStep === 0) return; const newStep = historyStep - 1; setHistoryStep(newStep); setImages(JSON.parse(history[newStep])); }; const handleRedo = () => { if (historyStep === history.length - 1) return; const newStep = historyStep + 1; setHistoryStep(newStep); setImages(JSON.parse(history[newStep])); }; return (
{lionImage && images.map((img, i) => ( handleDragEnd(i, e)} onClick={() => handleClick(i)} /> ))}
); }; export default App; ``` ```js ``` --- # 从 JSON 加载 HTML5 Canvas 舞台教程 > 使用 Konva.Node.create 和 find 选择器,从 JSON 加载包含图像和事件绑定的 Konva 舞台。 Source: https://konvajs.org/zh-Hans/docs/data_and_serialization/Complex_Load.html 要使用 Konva 加载原来包含图像和事件绑定的复杂舞台, 需要使用 `Konva.Node.create()` 创建舞台节点,然后配合选择器使用 `find()` 方法 设置图像和事件处理程序。 图像和事件处理程序无法序列化,因此必须手动设置。 **这些方法适用于小型应用。对于更复杂的情况,请参阅[最佳实践](/zh-Hans/docs/data_and_serialization/Best_Practices.html)。** ```js import Konva from 'konva'; // JSON string from a previous save const json = '{"attrs":{"width":578,"height":200},"className":"Stage","children":[{"attrs":{},"className":"Layer","children":[{"attrs":{"x":100,"y":100,"sides":6,"radius":70,"fill":"red","stroke":"black","strokeWidth":4},"className":"RegularPolygon"}]}]}'; // create node using json string const stage = Konva.Node.create(json, 'container'); // get reference to the hexagon const hexagon = stage.findOne('RegularPolygon'); // bind events hexagon.on('click', () => { hexagon.fill(Konva.Util.getRandomColor()); }); ``` **注意:**在 React 中直接使用 `Konva.Node.create()` 是一种反模式。在 React 应用中,应分别管理状态和视图。不要反序列化完整的节点结构,而应加载定义图形的数据,并让 React 组件处理渲染。以下示例演示如何在 React 中将图形数据加载为 state: ```js import { Stage, Layer, RegularPolygon } from 'react-konva'; import { useState, useEffect } from 'react'; import Konva from 'konva'; const App = () => { const [shapeData, setShapeData] = useState(null); useEffect(() => { // Simulating loading JSON data from storage or API const loadData = () => { // This would typically come from localStorage, API, etc. const jsonString = '{"hexagon":{"x":100,"y":100,"sides":6,"radius":70,"fill":"red","stroke":"black","strokeWidth":4}}'; try { // Parse the JSON into a JavaScript object const data = JSON.parse(jsonString); setShapeData(data); } catch (error) { console.error('Error parsing JSON:', error); } }; loadData(); }, []); const handleClick = () => { if (shapeData) { setShapeData({ ...shapeData, hexagon: { ...shapeData.hexagon, fill: Konva.Util.getRandomColor() } }); } }; // Don't render until we have data if (!shapeData) return
Loading...
; return ( ); }; export default App; ``` **注意:**在 Vue 中直接使用 `Konva.Node.create()` 是一种反模式。在 Vue 应用中,应使用响应式数据管理状态,并将状态与视图分开。不要反序列化完整的节点结构,而应加载定义图形的数据,并让 Vue 组件处理渲染。以下示例演示如何在 Vue 中将图形数据加载为响应式状态: ```js ``` --- # HTML5 Canvas 导出高质量图像教程 > 学习如何使用 Konva.js 将 HTML5 Canvas 导出为高质量 PNG 或 JPEG 图像,以及如何配合 pixelRatio 使用 stage.toDataURL() 实现 Retina 质量的导出。 Source: https://konvajs.org/zh-Hans/docs/data_and_serialization/High-Quality-Export.html 如果需要将舞台导出为图像或 base64,可以使用 `stage.toDataURL()` 或 `stage.toImage()` 方法。 默认情况下,`Konva` 将导出图像的 `pixelRatio` 属性设置为 `1`。这意味着,如果导出尺寸为 `500x500` 的舞台,导出图像的尺寸也为 `500x500`。 在某些情况下,你可能要导出更适合较高或较低分辨率的图像。例如,你可能要导出一个图像,然后在 HDPI 设备的 Canvas 上使用它。这类设备具有较高的像素比,例如 Retina 显示屏。另一种情况是,你需要为使用高分辨率显示器的计算机导出用户的绘图。 如果使用默认设置执行此操作,图像会变得模糊。有关全局 `pixelRatio` 属性的更多信息,请参阅 [MDN - devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio)。 对于这两种情况,可以使用: ```javascript stage.toDataURL({ pixelRatio: 2 // or other value you need }) ``` 现在,尺寸为 `500x500` 的舞台会导出为尺寸为 `1000x1000` 的图像。除了位图图像和缓存节点外,`Konva` 中几乎所有节点都存储为矢量数据。因此可以导出高质量图像。 **操作说明:**尝试将舞台保存为图像。你会看到该图像具有较高分辨率。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: 400, height: 400 }); const layer = new Konva.Layer(); stage.add(layer); // create some shapes const circle = new Konva.Circle({ x: 200, y: 200, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4 }); const text = new Konva.Text({ x: 150, y: 190, text: 'High Quality Export', fontSize: 20, fill: 'white' }); layer.add(circle); layer.add(text); // add button const button = document.createElement('button'); button.textContent = 'Save as High Quality Image'; document.body.appendChild(button); button.addEventListener('click', () => { // save stage as a high quality image const dataURL = stage.toDataURL({ pixelRatio: 2 // double resolution }); // create link to download const link = document.createElement('a'); link.download = 'stage.png'; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); ``` ```js import { Stage, Layer, Circle, Text } from 'react-konva'; import { useRef } from 'react'; const App = () => { const stageRef = useRef(null); const handleExport = () => { const dataURL = stageRef.current.toDataURL({ pixelRatio: 2 // double resolution }); const link = document.createElement('a'); link.download = 'stage.png'; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; return (
); }; export default App; ``` ```js ``` ## 浏览器尺寸限制 每个浏览器都会限制 Canvas 的宽度、高度和总面积,移动端的限制更低。 超过限制后,较大的 `pixelRatio` 会静默失败:调用返回空白或截断的图像, 而不会引发错误。大幅面输出必须分块处理,或在浏览器外渲染。 --- # 从 JSON 加载简单 HTML5 Canvas 舞台教程 > 学习如何使用 Konva.js 从 JSON 加载 HTML5 Canvas 舞台,以及如何使用 Konva.Node.create() 恢复已保存的 Canvas 状态。 Source: https://konvajs.org/zh-Hans/docs/data_and_serialization/Simple_Load.html 要使用 Konva 从 JSON 加载简单舞台,可以使用 `Konva.Node.create()` 方法。 `create()` 方法接受 JSON 字符串和容器 id 作为参数。 ```js import Konva from 'konva'; // JSON string from a previous save const json = '{"attrs":{"width":400,"height":400},"className":"Stage","children":[{"attrs":{},"className":"Layer","children":[{"attrs":{"x":100,"y":100,"radius":50,"fill":"red","stroke":"black","strokeWidth":3},"className":"Circle"}]}]}'; // create node using json string const stage = Konva.Node.create(json, 'container'); // you can keep adding events, etc const circle = stage.findOne('Circle'); circle.on('click', () => { circle.fill(Konva.Util.getRandomColor()); }); ``` **注意:**在 React 或 Vue 中直接使用 `Konva.Node.create()` 是一种反模式。在这些框架中,应分别管理状态(数据)和视图(组件)。不要序列化和加载完整的节点结构,而应保存和加载定义图形的数据,然后让框架组件处理渲染。这种方法更符合 React 和 Vue 的声明式、状态驱动模式,也能更好地控制组件生命周期和事件。 ```js import { Stage, Layer, Circle } from 'react-konva'; import { useState, useEffect } from 'react'; import Konva from 'konva'; const App = () => { // In React, we store shape data as state instead of using Konva.Node.create() const [shapeData, setShapeData] = useState(null); useEffect(() => { // Simulating loading JSON data from storage or API const savedShapeData = { circle: { x: 100, y: 100, radius: 50, fill: 'red', stroke: 'black', strokeWidth: 3 }, // We could have more shapes here }; // In a real app, this might be: // fetch('/api/shapes').then(response => response.json()).then(setShapeData) setShapeData(savedShapeData); }, []); const handleCircleClick = () => { setShapeData({ ...shapeData, circle: { ...shapeData.circle, fill: Konva.Util.getRandomColor() } }); }; // Don't render until we have data if (!shapeData) return
Loading...
; return ( ); }; export default App; ``` **注意:**在 React 或 Vue 中直接使用 `Konva.Node.create()` 是一种反模式。在这些框架中,应分别管理状态(数据)和视图(组件)。不要序列化和加载完整的节点结构,而应保存和加载定义图形的数据,然后让框架组件处理渲染。这种方法更符合 React 和 Vue 的声明式、状态驱动模式,也能更好地控制组件生命周期和事件。 ```js ``` --- # Canvas 截图——使用 JavaScript 将 HTML5 Canvas 导出为图像 > 使用 JavaScript 截取 HTML5 Canvas 并导出为 PNG 或 JPEG。使用 Konva.js 的 toDataURL() 将 Canvas 内容捕获为 base64 图像或可下载文件。 Source: https://konvajs.org/zh-Hans/docs/data_and_serialization/Stage_Data_URL.html 要使用 `Konva` 截取 Canvas 并将其导出为图像,请使用 `toDataURL()` 方法。此方法对所有节点类型(包括 `Stage`)都直接返回数据 URL。 你可以传入 image/jpeg 等 MIME 类型和 0 到 1 之间的质量值。 还可以截取特定节点,包括图层、组和图形。 *注意:`toDataURL()` 方法要求 Canvas 上绘制的所有图像 与执行代码托管在同一域名下的 Web 服务器上。 如果不满足此条件,将引发 SECURITY_ERR 异常。* **操作说明:**拖放矩形,然后单击保存按钮以获取合成数据 URL,并在新窗口中打开生成的图像。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: 400, height: 400 }); const layer = new Konva.Layer(); stage.add(layer); // create draggable rectangle const rect = new Konva.Rect({ x: 100, y: 100, width: 100, height: 100, fill: 'red', stroke: 'black', strokeWidth: 4, draggable: true }); layer.add(rect); // add button const button = document.createElement('button'); button.textContent = 'Save as Image'; document.body.appendChild(button); button.addEventListener('click', () => { // get data URL with default settings const dataURL = stage.toDataURL(); // open in new window const win = window.open(); win.document.write(`Stage`); // you can also save with different settings const jpegURL = stage.toDataURL({ mimeType: 'image/jpeg', quality: 0.8 }); console.log('JPEG URL:', jpegURL); }); ``` ```js import { Stage, Layer, Rect } from 'react-konva'; import { useRef, useState } from 'react'; const App = () => { const stageRef = useRef(null); const [position, setPosition] = useState({ x: 100, y: 100 }); const handleExport = () => { // get data URL with default settings const dataURL = stageRef.current.toDataURL(); // open in new window const win = window.open(); win.document.write(`Stage`); // you can also save with different settings const jpegURL = stageRef.current.toDataURL({ mimeType: 'image/jpeg', quality: 0.8 }); console.log('JPEG URL:', jpegURL); }; const handleDragEnd = (e) => { setPosition({ x: e.target.x(), y: e.target.y() }); }; return (
); }; export default App; ``` ```js ``` --- # 如何支持 Konva 项目并捐款? > 通过 Patreon、Open Collective 或 GitHub Sponsors 支持 Konva 开发,帮助维护该框架。 Source: https://konvajs.org/zh-Hans/docs/donate.html 你好,我叫 Anton,是 `Konva` 框架的核心维护者。 **如果你想支持 `Konva` 及其所有生态系统工具(例如 `react-konva` 和 `vue-konva`)的开发,可以使用以下渠道:** - [Patreon](https://www.patreon.com/lavrton) - [Open Collective](https://opencollective.com/konva) - [GitHub 赞助](https://github.com/sponsors/lavrton) 我投入大量时间为 `Konva` 用户提供支持,并开发包含错误修复和新功能的新版本。 如果你通过在项目中使用 `Konva` 获得收入,支持 `Konva` 的开发是合理的。你的支持有助于保持框架的质量和持续维护。 即使项目没有带来收入,如果 `Konva` 为你节省了大量时间,也欢迎你提供支持。 ### 你的公司在使用 Konva 吗? 许多开发者可能难以提供资金捐助。但是,如果你在公司工作中使用 `konva`,可以与管理人员沟通,让公司支持该项目。得到充分支持的项目可以为公司带来价值。 --- # HTML5 Canvas 复杂拖放边界 > 学习如何使用 dragmove 事件,将 Konva 中的拖放操作限制在自定义边界和区域内。 Source: https://konvajs.org/zh-Hans/docs/drag_and_drop/Complex_Drag_and_Drop.html 如需使用 Konva 将正在拖放的节点限制在指定区域内, 可以使用 `dragmove` 事件定义节点无法越过的边界。 _提示:你可以使用 `shape.absolutePosition()` 方法获取或设置节点的绝对位置,而不是使用相对的 `x` 和 `y`。_ **操作说明:** 拖放浅蓝色矩形,并观察它 被限制在 y = 50 的假想边界下方。拖放黄色 矩形,并观察它被限制在假想圆形内。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); const blueGroup = new Konva.Group({ x: 30, y: 70, draggable: true, }); // bound below y=50 blueGroup.on('dragmove', () => { blueGroup.y(Math.max(blueGroup.y(), 50)); }); // bound inside a circle const yellowGroup = new Konva.Group({ x: stage.width() / 2, y: 70, draggable: true, }); yellowGroup.on('dragmove', () => { const x = stage.width() / 2; const y = 70; const radius = 50; const pos = yellowGroup.absolutePosition(); const scale = radius / Math.sqrt(Math.pow(pos.x - x, 2) + Math.pow(pos.y - y, 2)); if (scale < 1) { yellowGroup.x(Math.round((pos.x - x) * scale + x)); yellowGroup.y(Math.round((pos.y - y) * scale + y)); } }); const blueText = new Konva.Text({ fontSize: 26, fontFamily: 'Calibri', text: 'bound below', fill: 'black', padding: 10, width: 150, align: 'center', }); const blueRect = new Konva.Rect({ width: 150, height: 72, fill: '#aaf', stroke: 'black', strokeWidth: 4, }); const yellowText = new Konva.Text({ fontSize: 26, fontFamily: 'Calibri', text: 'bound in circle', fill: 'black', padding: 10, width: 150, align: 'center', }); const yellowRect = new Konva.Rect({ width: 150, height:72, fill: 'yellow', stroke: 'black', strokeWidth: 4, }); blueGroup.add(blueRect).add(blueText); yellowGroup.add(yellowRect).add(yellowText); layer.add(blueGroup); layer.add(yellowGroup); stage.add(layer); ```` ```jsx import { Stage, Layer, Group, Rect, Text } from 'react-konva'; import { useState } from 'react'; const App = () => { const [bluePosition, setBluePosition] = useState({ x: 30, y: 70 }); const [yellowPosition, setYellowPosition] = useState({ x: window.innerWidth / 2, y: 70, }); const handleBlueDragMove = (e) => { // At the boundary, y stays 50 in state. React-Konva skips unchanged props. // Constrain the node directly before saving its position in state. e.target.y(Math.max(e.target.y(), 50)); setBluePosition(e.target.position()); }; const handleYellowDragMove = (e) => { const x = window.innerWidth / 2; const y = 70; const radius = 50; const position = e.target.absolutePosition(); const scale = radius / Math.sqrt( Math.pow(position.x - x, 2) + Math.pow(position.y - y, 2) ); if (scale < 1) { // Repeated drags can produce the same boundary coordinates. // Set the node directly because React-Konva skips unchanged props. e.target.absolutePosition({ x: Math.round((position.x - x) * scale + x), y: Math.round((position.y - y) * scale + y), }); } setYellowPosition(e.target.position()); }; return ( ); }; export default App; ```` ```vue ``` --- # HTML5 Canvas 拖放事件 > 学习如何使用 Konva.js 处理 HTML5 Canvas 上的拖放事件。使用 dragstart、dragmove 和 dragend 事件响应图形拖动。 Source: https://konvajs.org/zh-Hans/docs/drag_and_drop/Drag_Events.html 如需检测 Konva 拖放事件,可以使用 `on()` 方法将 `dragstart`、`dragmove` 或 `dragend` 事件绑定到节点。 `on()` 方法需要事件类型和事件发生时要运行的函数。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const text = new Konva.Text({ x: 40, y: 40, text: 'Draggable Text', fontSize: 20, draggable: true, width: 200, }); layer.add(text); const status = new Konva.Text({ x: 40, y: 100, text: '', fontSize: 16, width: 200, }); layer.add(status); text.on('dragstart', () => { status.text('drag started'); }); text.on('dragend', () => { status.text('drag ended'); }); text.on('dragmove', () => { status.text('dragging'); }); ``` ```jsx import { Stage, Layer, Text } from 'react-konva'; import { useState } from 'react'; const App = () => { const [position, setPosition] = useState({ x: 40, y: 40 }); const [status, setStatus] = useState(''); const handleDrag = (e, nextStatus) => { setPosition({ x: e.target.x(), y: e.target.y() }); setStatus(nextStatus); }; return ( setStatus('drag started')} onDragEnd={(e) => handleDrag(e, 'drag ended')} onDragMove={(e) => handleDrag(e, 'dragging')} /> ); }; export default App; ``` ```vue ``` --- # HTML5 Canvas 拖放组教程 > 学习如何使用 Konva 的 draggable 属性,在 HTML5 Canvas 上一起拖放一组图形。 Source: https://konvajs.org/zh-Hans/docs/drag_and_drop/Drag_a_Group.html 如需使用 Konva 拖放组,可以在创建组时将配置对象的 `draggable` 属性 设置为 `true`,也可以使用 `draggable()` 方法。 注意:拖动组不会更改任何子节点的 `x` 和 `y` 属性。更改的是组本身的属性。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const group = new Konva.Group({ draggable: true, }); layer.add(group); const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']; for (let i = 0; i < 6; i++) { const box = new Konva.Rect({ x: i * 30 + 10, y: i * 18 + 40, width: 100, height: 50, name: colors[i], fill: colors[i], stroke: 'black', strokeWidth: 4, }); group.add(box); } group.on('mouseover', function () { document.body.style.cursor = 'move'; }); group.on('mouseout', function () { document.body.style.cursor = 'default'; }); ``` ```jsx import { Stage, Layer, Group, Rect, Text } from 'react-konva'; const App = () => { const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']; const handleMouseOver = () => { document.body.style.cursor = 'move'; }; const handleMouseOut = () => { document.body.style.cursor = 'default'; }; return ( {colors.map((color, i) => ( ))} ); }; export default App; ``` ```vue ``` --- # HTML5 Canvas 拖放线条 > 学习如何通过将 draggable 属性设置为 true,使用 Konva 在 HTML5 Canvas 上拖放线条。 Source: https://konvajs.org/zh-Hans/docs/drag_and_drop/Drag_a_Line.html 如需使用 Konva 拖放线条,可以在创建线条时将配置对象的 `draggable` 属性 设置为 `true`,也可以使用 `draggable()` 方法。 **注意:拖动线条不会更改 `points` 属性,而会更改线条的 `x` 和 `y` 属性。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); const redLine = new Konva.Line({ x: 50, y: 50, points: [0, 0, 150, 0], stroke: 'red', strokeWidth: 15, lineCap: 'round', lineJoin: 'round', draggable: true, }); // add cursor styling redLine.on('mouseover', function () { document.body.style.cursor = 'pointer'; }); redLine.on('mouseout', function () { document.body.style.cursor = 'default'; }); layer.add(redLine); stage.add(layer); ``` ```jsx import { Stage, Layer, Line } from 'react-konva'; import { useState } from 'react'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); return ( { setPosition({ x: e.target.x(), y: e.target.y(), }); }} onMouseEnter={(e) => { document.body.style.cursor = 'pointer'; }} onMouseLeave={(e) => { document.body.style.cursor = 'default'; }} /> ); }; export default App; ``` ```vue ``` --- # HTML5 Canvas 拖放舞台 > 学习如何使整个 Konva 舞台可以拖动,让用户通过拖动任意区域来平移 Canvas。 Source: https://konvajs.org/zh-Hans/docs/drag_and_drop/Drag_a_Stage.html 如需使用 Konva 拖放舞台,可以在创建组时将配置对象的 `draggable` 属性 设置为 `true`,也可以使用 `draggable()` 方法。 与图形、组和图层等其他节点的拖放不同, 你可以拖动舞台的任意部分来拖动整个舞台。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, draggable: true }); const layer = new Konva.Layer(); stage.add(layer); // create circle const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4 }); // create text const text = new Konva.Text({ x: 10, y: 10, text: 'Drag the stage anywhere', fontSize: 20, fontFamily: 'Calibri', fill: 'black' }); layer.add(circle); layer.add(text); ``` ```jsx import { Stage, Layer, Circle, Text } from 'react-konva'; const App = () => { return ( ); }; export default App; ``` ```vue ``` --- # HTML5 Canvas 拖放图像 > 学习如何通过将 draggable 属性设置为 true,使用 Konva 在 HTML5 Canvas 上拖放图像。 Source: https://konvajs.org/zh-Hans/docs/drag_and_drop/Drag_an_Image.html 如需使用 Konva 拖放图像,可以在创建图形时将 `draggable` 属性 设置为 true,也可以使用 `draggable()` 方法。 `draggable()` 方法会自动为桌面应用和移动应用启用拖放功能。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); const imageObj = new Image(); imageObj.onload = () => { const yoda = new Konva.Image({ x: 50, y: 50, image: imageObj, width: 106, height: 118, draggable: true, }); // add cursor styling yoda.on('mouseover', function () { document.body.style.cursor = 'pointer'; }); yoda.on('mouseout', function () { document.body.style.cursor = 'default'; }); layer.add(yoda); }; imageObj.src = 'https://konvajs.org/assets/yoda.jpg'; stage.add(layer); ``` ```jsx import { Stage, Layer, Image } from 'react-konva'; import { useState } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [yodaImage] = useImage('https://konvajs.org/assets/yoda.jpg'); return ( { setPosition({ x: e.target.x(), y: e.target.y() }); }} onMouseEnter={(e) => { document.body.style.cursor = 'pointer'; }} onMouseLeave={(e) => { document.body.style.cursor = 'default'; }} /> ); }; export default App; ``` ```vue ``` --- # HTML5 Canvas 放置事件 > 学习如何使用 Konva.js 实现 HTML5 Canvas 放置事件。检测拖动的图形何时被放置到目标区域。 Source: https://konvajs.org/zh-Hans/docs/drag_and_drop/Drop_Events.html Konva 不支持放置事件,但你可以自行编写放置事件检测逻辑。 如需检测作为放置目标的图形,必须把正在拖动的对象移到另一个图层。 此示例展示 `drop`、`dragenter`、`dragleave` 和 `dragover` 事件的实现。 **操作说明:** 将一个图形拖到另一个图形上方。也可以将一个图形拖放到另一个图形中。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); const tempLayer = new Konva.Layer(); stage.add(layer); stage.add(tempLayer); const text = new Konva.Text({ fill: 'black', }); layer.add(text); let previousShape; // create multiple stars for (let i = 0; i < 10; i++) { const star = new Konva.Star({ x: stage.width() * Math.random(), y: stage.height() * Math.random(), fill: 'blue', numPoints: 10, innerRadius: 20, outerRadius: 25, draggable: true, name: 'star ' + i, shadowOffsetX: 5, shadowOffsetY: 5, }); star.on('dragstart', () => { star.moveTo(tempLayer); text.text('Moving ' + star.name()); }); star.on('dragmove', (e) => { const pos = stage.getPointerPosition(); const shape = layer.getIntersection(pos); if (previousShape && shape) { if (previousShape !== shape) { // leave from old target previousShape.fire('dragleave', { evt: e.evt }, true); // enter new target shape.fire('dragenter', { evt: e.evt }, true); previousShape = shape; } else { previousShape.fire('dragover', { evt: e.evt }, true); } } else if (!previousShape && shape) { previousShape = shape; shape.fire('dragenter', { evt: e.evt }, true); } else if (previousShape && !shape) { previousShape.fire('dragleave', { evt: e.evt }, true); previousShape = undefined; } }); star.on('dragend', (e) => { const pos = stage.getPointerPosition(); const shape = layer.getIntersection(pos); if (previousShape && previousShape !== shape) { previousShape.fire('dragleave', { evt: e.evt }, true); } if (shape) { shape.fire('drop', { evt: e.evt }, true); } previousShape = undefined; star.moveTo(layer); }); star.on('dragenter', () => { star.fill('green'); text.text('dragenter ' + star.name()); }); star.on('dragleave', () => { star.fill('blue'); text.text('dragleave ' + star.name()); }); star.on('dragover', () => { text.text('dragover ' + star.name()); }); star.on('drop', () => { star.fill('red'); text.text('drop ' + star.name()); }); layer.add(star); } ``` ```jsx import { Stage, Layer, Text, Star } from 'react-konva'; import { useState, useRef } from 'react'; const App = () => { const [stars, setStars] = useState(() => Array.from({ length: 10 }, (_, i) => ({ id: i, x: window.innerWidth * Math.random(), y: window.innerHeight * Math.random(), fill: 'blue', name: `star ${i}`, })) ); const [message, setMessage] = useState(''); const previousShapeRef = useRef(null); const mainLayerRef = useRef(null); const tempLayerRef = useRef(null); const updateStar = (id, attrs) => { setStars((currentStars) => currentStars.map((star) => star.id === id ? { ...star, ...attrs } : star ) ); }; const handleDragStart = (id, e) => { const shape = e.target; updateStar(id, { x: shape.x(), y: shape.y() }); shape.moveTo(tempLayerRef.current); setMessage('Moving ' + shape.name()); }; const handleDragMove = (id, e) => { const stage = e.target.getStage(); const pos = stage.getPointerPosition(); const shape = mainLayerRef.current.getIntersection(pos); updateStar(id, { x: e.target.x(), y: e.target.y() }); if (previousShapeRef.current && shape) { if (previousShapeRef.current !== shape) { // leave from old target previousShapeRef.current.fire('dragleave', { evt: e.evt }, true); // enter new target shape.fire('dragenter', { evt: e.evt }, true); previousShapeRef.current = shape; } else { previousShapeRef.current.fire('dragover', { evt: e.evt }, true); } } else if (!previousShapeRef.current && shape) { previousShapeRef.current = shape; shape.fire('dragenter', { evt: e.evt }, true); } else if (previousShapeRef.current && !shape) { previousShapeRef.current.fire('dragleave', { evt: e.evt }, true); previousShapeRef.current = undefined; } }; const handleDragEnd = (id, e) => { const shape = e.target; const stage = e.target.getStage(); const pos = stage.getPointerPosition(); const dropShape = mainLayerRef.current.getIntersection(pos); if ( previousShapeRef.current && previousShapeRef.current !== dropShape ) { previousShapeRef.current.fire('dragleave', { evt: e.evt }, true); } if (dropShape) { dropShape.fire('drop', { evt: e.evt }, true); } updateStar(id, { x: shape.x(), y: shape.y() }); shape.moveTo(mainLayerRef.current); previousShapeRef.current = undefined; }; const handleDragEnter = (id, e) => { updateStar(id, { fill: 'green' }); setMessage('dragenter ' + e.target.name()); }; const handleDragLeave = (id, e) => { updateStar(id, { fill: 'blue' }); setMessage('dragleave ' + e.target.name()); }; const handleDragOver = (e) => { setMessage('dragover ' + e.target.name()); }; const handleDrop = (id, e) => { updateStar(id, { fill: 'red' }); setMessage('drop ' + e.target.name()); }; return ( {stars.map((star) => ( handleDragStart(star.id, e)} onDragMove={(e) => handleDragMove(star.id, e)} onDragEnd={(e) => handleDragEnd(star.id, e)} onDragEnter={(e) => handleDragEnter(star.id, e)} onDragLeave={(e) => handleDragLeave(star.id, e)} onDragOver={handleDragOver} onDrop={(e) => handleDrop(star.id, e)} /> ))} ); }; export default App; ``` ```vue ``` --- # HTML5 Canvas 简单拖动边界教程 > 学习如何使用 Konva.js 限制 HTML5 Canvas 上的拖动。设置拖动边界,以限制图形可以拖动的区域。 Source: https://konvajs.org/zh-Hans/docs/drag_and_drop/Simple_Drag_Bounds.html 如需限制使用 Konva 拖放的图形的移动范围, 可以使用 `dragmove` 事件,并在事件处理程序中重新设置拖放位置。 可以使用此事件以多种方式限制拖放时的移动。例如,只允许水平、垂直、对角线或径向移动,甚至可以将节点 限制在方框、圆形或其他路径内。 ```js shape.on('dragmove', () => { // lock position of the shape on x axis // keep y position as is shape.x(0); }); ``` _提示:你可以使用 `shape.absolutePosition()` 方法获取或设置节点的绝对位置,而不是使用相对的 `x` 和 `y`。_ **操作说明:** 拖放横向文本,并观察它只能沿水平方向移动。 拖放纵向文本,并观察它只能沿垂直方向移动。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const horizontalText = new Konva.Text({ x: 50, y: 50, text: 'Drag me horizontally', fontSize: 16, draggable: true, fill: 'black', }); horizontalText.on('dragmove', function () { // horizontal only this.y(50); }); const verticalText = new Konva.Text({ x: 200, y: 50, text: 'Drag me vertically', fontSize: 16, draggable: true, fill: 'black', }); verticalText.on('dragmove', function () { // vertical only this.x(200); }); layer.add(horizontalText); layer.add(verticalText); ``` ```jsx import { Stage, Layer, Text } from 'react-konva'; import { useState } from 'react'; const App = () => { const [horizontalPosition, setHorizontalPosition] = useState({ x: 50, y: 50 }); const [verticalPosition, setVerticalPosition] = useState({ x: 200, y: 50 }); const handleHorizontalDragMove = (e) => { // The y prop stays 50, so React-Konva will not reapply it after a drag. // Constrain the node directly, then save its actual position in state. e.target.y(50); setHorizontalPosition(e.target.position()); }; const handleVerticalDragMove = (e) => { // The x prop stays 200, so a state update alone cannot lock this axis. e.target.x(200); setVerticalPosition(e.target.position()); }; return ( ); }; export default App; ``` ```vue ``` --- # 使用 Konva 取消 HTML5 Canvas 事件冒泡传播 > 学习如何将事件对象的 cancelBubble 设置为 true,以取消 Konva 中的事件冒泡传播。 Source: https://konvajs.org/zh-Hans/docs/events/Cancel_Propagation.html 要取消 Konva 中的事件冒泡传播,可以将事件对象的 `cancelBubble` 属性设置为 true。 **操作说明:单击圆形。你会看到仅处理了圆形绑定的事件,因为触发圆形事件时取消了事件传播, 所以事件对象不会向上冒泡。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); circle.on('click', function (evt) { alert('You clicked on the circle'); // stop event bubble evt.cancelBubble = true; }); layer.on('click', function () { alert('You clicked on the layer'); }); layer.add(circle); ``` ```jsx import { Stage, Layer, Circle } from 'react-konva'; const App = () => { const handleCircleClick = (e) => { alert('You clicked on the circle'); // stop event bubble e.cancelBubble = true; }; const handleLayerClick = () => { alert('You clicked on the layer'); }; return ( ); }; export default App; ``` ```html ``` --- # HTML5 Canvas 自定义命中检测函数教程 > 学习如何使用 Konva.js 为 HTML5 Canvas 图形定义自定义命中检测区域,以及如何使用 hitFunc 覆盖默认命中区域以准确检测单击。 Source: https://konvajs.org/zh-Hans/docs/events/Custom_Hit_Region.html 可以使用两种方式更改图形的命中区域:`hitFunc` 和 `hitStrokeWidth` 属性。 ## 1. 什么是 `hitFunc`? 要使用 Konva 为图形创建自定义命中绘制函数,可以设置 `hitFunc` 属性。Konva 使用命中绘制函数绘制用于命中检测的区域。 自定义命中绘制函数有多种用途。例如,你可以扩大命中区域, 让用户更容易与图形交互。你也可以检测图形的某些部分并忽略其他部分, 或简化命中绘制函数以提高渲染性能。 另请参阅编写自定义 `sceneFunc` 的[最佳实践](/zh-Hans/docs/shapes/Custom.html),这些实践也适用于 `hitFunc`。 `hitFunc` 是一个有两个参数的函数:一个 [Konva.Context](/zh-Hans/api/Konva.Context.html) 渲染器和一个图形实例。 ## 2. 什么是 `hitStrokeWidth`? 对于 `Konva.Line` 等图形,覆盖 `hitFunc` 很困难。有时,你只想加粗事件命中区域。此时,最好为 `hitStrokeWidth` 属性设置较大的值。 **操作说明:在星形上触发 mouseover、mouseout、mousedown 和 mouseup。 你会看到命中区域是一个包围图形的大圆。也请对线条执行相同操作。 你还可以切换命中 Canvas 来查看其外观。这对调试很有用。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const text = new Konva.Text({ x: 10, y: 10, text: '', fontSize: 24, }); layer.add(text); const star = new Konva.Star({ x: stage.width() / 4, y: stage.height() / 2, numPoints: 5, innerRadius: 40, outerRadius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); // custom hit function star.hitFunc(function (context) { context.beginPath(); context.arc(0, 0, 70, 0, Math.PI * 2, true); context.closePath(); context.fillStrokeShape(this); }); const line = new Konva.Line({ x: stage.width() * 0.6, y: stage.height() / 2, points: [-50, -50, 50, 50], stroke: 'black', strokeWidth: 2, hitStrokeWidth: 20, }); const button = document.createElement('button'); button.innerHTML = 'Toggle hit canvas'; document.body.appendChild(button); let showHit = false; button.addEventListener('click', () => { showHit = !showHit; if (showHit) { stage.container().style.border = '2px solid black'; stage.container().style.height = stage.height() + 'px'; stage.container().appendChild(layer.hitCanvas._canvas); layer.hitCanvas._canvas.style.position = 'absolute'; layer.hitCanvas._canvas.style.top = 0; layer.hitCanvas._canvas.style.left = 0; } else { layer.hitCanvas._canvas.remove(); } }); function writeMessage(message) { text.text(message); } star.on('mouseover mouseout mousedown mouseup', function (evt) { writeMessage(evt.type + ' star'); }); line.on('mouseover mouseout mousedown mouseup', function (evt) { writeMessage(evt.type + ' line'); }); layer.add(star); layer.add(line); ``` ```jsx import { Stage, Layer, Star, Line, Text } from 'react-konva'; import { useState, useEffect } from 'react'; const App = () => { const [message, setMessage] = useState(''); const [showHit, setShowHit] = useState(false); const handleStarEvent = (evt) => { setMessage(evt.type + ' star'); }; const handleLineEvent = (evt) => { setMessage(evt.type + ' line'); }; useEffect(() => { const stage = document.querySelector('.konvajs-content'); if (showHit) { const hitCanvas = stage.querySelector('canvas:last-child'); stage.style.border = '2px solid black'; hitCanvas.style.position = 'absolute'; hitCanvas.style.top = '0'; hitCanvas.style.left = '0'; } }, [showHit]); return ( <> { context.beginPath(); context.arc(0, 0, 70, 0, Math.PI * 2, true); context.closePath(); context.fillStrokeShape(shape); }} onMouseover={handleStarEvent} onMouseout={handleStarEvent} onMousedown={handleStarEvent} onMouseup={handleStarEvent} /> ); }; export default App; ``` ```html ``` --- # HTML5 Canvas 桌面端和移动端事件支持教程 > 学习如何在 Konva 中使用 mousedown/touchstart 和 mouseup/touchend 等事件对,同时处理桌面端和移动端事件。 Source: https://konvajs.org/zh-Hans/docs/events/Desktop_and_Mobile.html _注意:此示例可能已过时,因为现代浏览器支持指针事件。Konva 也支持指针事件。请参阅[指针事件示例](/zh-Hans/docs/events/Pointer_Events.html)。如果你不想使用指针事件,请继续阅读。_ 要使用 Konva 为图形添加同时适用于桌面端和移动端应用的事件处理程序,可以使用 `on()` 方法并传入事件对。 例如,要在桌面端和移动端应用中触发 `mousedown` 事件,可以使用 `"mousedown touchstart"` 事件对来覆盖这两种环境。 要在桌面端和移动端应用中触发 `mouseup` 事件,可以使用 `"mouseup touchend"` 事件对。 还可以使用 `"dblclick dbltap"` 事件对绑定同时适用于桌面端和移动端设备的双击事件。 **操作说明:在桌面端或移动端设备上,对圆形触发 mousedown、mouseup、touchstart 或 touchend,以观察相同的功能。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); const text = new Konva.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black', }); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); function writeMessage(message) { text.text(message); } // desktop and mobile events circle.on('mousedown touchstart', function () { writeMessage('Mousedown or touchstart'); }); circle.on('mouseup touchend', function () { writeMessage('Mouseup or touchend'); }); layer.add(circle); layer.add(text); stage.add(layer); ``` ```jsx import { Stage, Layer, Circle, Text } from 'react-konva'; import { useState } from 'react'; const App = () => { const [message, setMessage] = useState(''); return ( setMessage('Mousedown or touchstart')} onTouchstart={() => setMessage('Mousedown or touchstart')} onMouseup={() => setMessage('Mouseup or touchend')} onTouchend={() => setMessage('Mouseup or touchend')} /> ); }; export default App; ``` ```html ``` --- # 使用 Konva 实现 HTML5 Canvas 事件委托 > 学习如何使用 Konva.js 在 HTML5 Canvas 上实现事件委托。在图层或组上监听事件,而不是在各个图形上监听,以简化代码。 Source: https://konvajs.org/zh-Hans/docs/events/Event_Delegation.html 要使用 Konva 获取事件目标,可以访问事件对象的 `target` 属性。 这对事件委托特别有用。我们可以将事件处理程序绑定到父节点, 并监听其子节点上发生的事件。 **操作说明:单击星形,观察图层绑定的事件是否正确识别被单击的图形。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const text = new Konva.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black', }); layer.add(text); const star = new Konva.Star({ x: stage.width() / 2, y: stage.height() / 2, numPoints: 5, innerRadius: 40, outerRadius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); layer.add(star); // add event delegation layer.on('click', function (evt) { const shape = evt.target; text.text('click on ' + shape.getClassName()); }); ``` ```jsx import { Stage, Layer, Star, Text } from 'react-konva'; import { useState } from 'react'; const App = () => { const [message, setMessage] = useState(''); const handleLayerClick = (e) => { const shape = e.target; setMessage('click on ' + shape.getClassName()); }; return ( ); }; export default App; ``` ```html ``` --- # 使用 Konva 触发 HTML5 Canvas 事件 > 学习如何使用 fire() 方法以编程方式在 Konva 图形上触发事件,包括自定义事件。 Source: https://konvajs.org/zh-Hans/docs/events/Fire_Events.html 要使用 Konva 触发事件,可以使用 `fire()` 方法。 此方法可以通过编程方式触发 `click`、`mouseover`、`mousemove` 等事件, 也可以触发 foo 和 bar 等自定义事件。 > **注意**:虽然可以使用自定义事件,但通常最好使用 `click`、`mouseover`、`mousemove` 等内置交互事件。自定义事件会增加代码的维护和调试难度。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); // add shape event listener circle.on('customEvent', function (evt) { alert('custom event fired'); }); // add button to trigger custom event const button = document.createElement('button'); button.innerHTML = 'Fire Custom Event'; button.style.position = 'absolute'; button.style.top = '10px'; button.style.left = '10px'; button.style.zIndex = '1'; document.body.appendChild(button); button.addEventListener('click', () => { // fire custom event circle.fire('customEvent', { bubbles: true, }); }); layer.add(circle); ``` ```jsx import { Stage, Layer, Circle } from 'react-konva'; import { useRef } from 'react'; const App = () => { const circleRef = useRef(); const handleCustomEvent = () => { alert('custom event fired'); }; const fireCustomEvent = () => { circleRef.current.fire('customevent', { bubbles: true, }); }; return ( <> ); }; export default App; ``` ```html ``` --- # 使用 Konva 处理 HTML5 Canvas 图像事件 > 学习如何使用 Konva.js 处理 HTML5 Canvas 中的图像事件,以及如何为 Konva.Image 图形绑定 click、mouseover 和触摸事件。 Source: https://konvajs.org/zh-Hans/docs/events/Image_Events.html 要使用 Konva 仅检测图像中非透明像素上的事件,可以使用 `drawHitFromCache()` 方法生成更准确的图像命中区域。 默认情况下,图像内的任何像素都可以触发事件,即使该像素是透明的。`drawHitFromCache()` 方法还接受一个可选的回调方法。每次创建图像命中区域时,都会执行此方法。 *注意:`drawHitFromCache()` 方法要求图像和执行代码托管在同一域名下的 Web 服务器上。* **操作说明:将鼠标移到猴子和狮子上,观察绑定的 mouseover 事件。对于猴子,只要鼠标移到图像的任何部分,事件就会触发,包括透明像素。因为我们为狮子创建了图像命中区域,所以会忽略透明像素,从而实现更准确的事件检测。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const text = new Konva.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black', }); layer.add(text); function writeMessage(message) { text.text(message); } const imageObj1 = new Image(); imageObj1.onload = () => { const monkey = new Konva.Image({ x: 120, y: 50, image: imageObj1, width: 200, height: 200, }); monkey.on('mouseover', function () { writeMessage('mouseover monkey (regular image)'); }); monkey.on('mouseout', function () { writeMessage(''); }); layer.add(monkey); }; imageObj1.crossOrigin = 'Anonymous'; imageObj1.src = 'https://konvajs.org/assets/monkey.png'; const imageObj2 = new Image(); imageObj2.onload = () => { const lion = new Konva.Image({ x: 320, y: 50, image: imageObj2, width: 200, height: 200, }); // override color detection region lion.on('mouseover', function () { writeMessage('mouseover lion (with transparent pixels detection)'); }); lion.on('mouseout', function () { writeMessage(''); }); layer.add(lion); lion.cache(); lion.drawHitFromCache(); }; imageObj2.crossOrigin = 'Anonymous'; imageObj2.src = 'https://konvajs.org/assets/lion.png'; ``` ```jsx import { Stage, Layer, Image, Text } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [message, setMessage] = useState(''); const lionRef = useRef(); const [monkeyImage] = useImage('https://konvajs.org/assets/monkey.png', 'anonymous'); const [lionImage] = useImage('https://konvajs.org/assets/lion.png', 'anonymous'); useEffect(() => { if (lionImage) { lionRef.current.cache(); lionRef.current.drawHitFromCache(); } }, [lionImage]); return ( {monkeyImage && ( setMessage('mouseover monkey (regular image)')} onMouseout={() => setMessage('')} /> )} {lionImage && ( setMessage('mouseover lion (with transparent pixels detection)') } onMouseout={() => setMessage('')} /> )} ); }; export default App; ``` ```html ``` --- # 使用 Konva 处理 HTML5 Canvas 键盘事件 > 学习如何使用 Konva.js 处理 HTML5 Canvas 图形上的键盘事件,以及如何实现键盘快捷键、方向键移动和按键交互。 Source: https://konvajs.org/zh-Hans/docs/events/Keyboard_Events.html Konva 没有 `keydown` 或 `keyup` 等内置键盘事件。 ### 如何监听 Canvas 上的 keydown 或 keyup 事件? 你可以通过两种方式添加这些事件: 1. 监听 `window` 对象上的全局事件 2. 使用 `tabIndex` 属性让舞台容器可获得焦点,然后监听容器上的事件。 **操作说明:单击舞台以使其获得焦点,然后使用方向键移动图形。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 50, fill: 'red', stroke: 'black', strokeWidth: 4, }); layer.add(circle); // make stage container focusable stage.container().tabIndex = 1; // focus it // also stage will be in focus on its click stage.container().focus(); const DELTA = 4; // add keyboard events stage.container().addEventListener('keydown', (e) => { if (e.keyCode === 37) { circle.x(circle.x() - DELTA); } else if (e.keyCode === 38) { circle.y(circle.y() - DELTA); } else if (e.keyCode === 39) { circle.x(circle.x() + DELTA); } else if (e.keyCode === 40) { circle.y(circle.y() + DELTA); } else { return; } e.preventDefault(); }); ``` ```jsx import { Stage, Layer, Circle } from 'react-konva'; import { useRef, useEffect, useState } from 'react'; const App = () => { const stageRef = useRef(); const containerRef = useRef(); const [position, setPosition] = useState({ x: window.innerWidth / 2, y: window.innerHeight / 2, }); useEffect(() => { // focus the div on mount containerRef.current.focus(); }, []); const handleKeyDown = (e) => { const DELTA = 4; switch (e.keyCode) { case 37: // left setPosition(pos => ({ ...pos, x: pos.x - DELTA })); break; case 38: // up setPosition(pos => ({ ...pos, y: pos.y - DELTA })); break; case 39: // right setPosition(pos => ({ ...pos, x: pos.x + DELTA })); break; case 40: // down setPosition(pos => ({ ...pos, y: pos.y + DELTA })); break; default: return; } e.preventDefault(); }; return (
); }; export default App; ``` ```html ``` --- # 使用 Konva 启用或禁用 HTML5 Canvas 事件监听 > 学习如何使用 listening 属性和 setListening() 方法启用或禁用 Konva 图形上的事件监听。 Source: https://konvajs.org/zh-Hans/docs/events/Listen_for_Events.html 要使用 Konva 启用或禁用事件监听,可以在实例化图形时将配置对象的 listening 属性设置为 true 或 false,也可以使用 `setListening()` 方法设置 listening 属性。 为一个或多个节点设置 listening 属性后,还需要使用 `drawHit()` 方法 重新绘制每个受影响图层的命中图。 **操作说明:将鼠标移到椭圆上,观察事件处理程序是否未执行。 单击“Listen”开始监听事件,然后观察事件处理程序是否开始执行。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const text = new Konva.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black', }); layer.add(text); const oval = new Konva.Ellipse({ x: stage.width() / 2, y: stage.height() / 2, radiusX: 100, radiusY: 50, fill: 'yellow', stroke: 'black', strokeWidth: 4, listening: false, }); oval.on('mouseover', function () { writeMessage('Mouseover oval'); }); oval.on('mouseout', function () { writeMessage(''); }); function writeMessage(message) { text.text(message); } layer.add(oval); // add button to toggle listening const button = document.createElement('button'); button.innerHTML = 'Listen'; document.body.appendChild(button); button.addEventListener('click', () => { const listening = !oval.listening(); oval.listening(listening); button.innerHTML = listening ? 'Stop listening' : 'Listen'; layer.drawHit(); }); ``` ```jsx import { Stage, Layer, Ellipse, Text } from 'react-konva'; import { useState } from 'react'; const App = () => { const [message, setMessage] = useState(''); const [listening, setListening] = useState(false); return ( <> setMessage('Mouseover oval')} onMouseout={() => setMessage('')} /> ); }; export default App; ``` ```html ``` --- # HTML5 Canvas 移动端触摸事件教程 > 学习如何使用 Konva.js 在移动设备上处理 HTML5 Canvas 触摸事件,包括 tap、touchstart、touchmove、touchend 和多点触摸手势。 Source: https://konvajs.org/zh-Hans/docs/events/Mobile_Events.html 要使用 Konva 在移动设备上为图形绑定事件处理程序,可以使用 `on()` 方法。 `on()` 方法需要一个事件类型,以及一个在事件发生时执行的函数。 Konva 支持 `touchstart`、`touchmove`、`touchend`、`tap`、`dbltap`、`dragstart`、`dragmove` 和 `dragend` 移动端事件。 有关 `rotate` 等更复杂的手势,请参阅[手势示例](/zh-Hans/docs/sandbox/Gestures.html)。 如果你需要整个舞台的平移和缩放逻辑,请参阅[多点触摸缩放舞台示例](/zh-Hans/docs/sandbox/Multi-touch_Scale_Stage.html)。 *注意:此示例使用触摸事件而不是鼠标事件,因此仅适用于移动设备。* **操作说明:在三角形上移动手指以查看触摸坐标,并在圆形上触发 touchstart 和 touchend。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); const text = new Konva.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black', }); const triangle = new Konva.RegularPolygon({ x: 80, y: 120, sides: 3, radius: 80, fill: '#00D2FF', stroke: 'black', strokeWidth: 4, }); const circle = new Konva.Circle({ x: 230, y: 100, radius: 60, fill: 'red', stroke: 'black', strokeWidth: 4, }); function writeMessage(message) { text.text(message); } triangle.on('touchmove', function () { const touchPos = stage.getPointerPosition(); const x = touchPos.x; const y = touchPos.y; writeMessage('x: ' + x + ', y: ' + y); }); circle.on('touchstart', function () { writeMessage('touchstart circle'); }); circle.on('touchend', function () { writeMessage('touchend circle'); }); layer.add(triangle); layer.add(circle); layer.add(text); stage.add(layer); ``` ```jsx import { Stage, Layer, RegularPolygon, Circle, Text } from 'react-konva'; import { useState, useRef } from 'react'; const App = () => { const [message, setMessage] = useState(''); const stageRef = useRef(); const handleTriangleTouch = () => { const touchPos = stageRef.current.getPointerPosition(); setMessage(`x: ${touchPos.x}, y: ${touchPos.y}`); }; return ( setMessage('touchstart circle')} onTouchend={() => setMessage('touchend circle')} /> ); }; export default App; ``` ```html ``` --- # 使用 Konva 控制 HTML5 Canvas 移动端滚动和原生事件 > 学习如何使用图形的 preventDefault 属性控制 Konva 舞台上的移动端滚动行为。 Source: https://konvajs.org/zh-Hans/docs/events/Mobile_Scrolling.html 默认情况下,`Konva` 会阻止与舞台进行的所有指针交互的默认行为。 这会防止你在移动设备上拖放图形时意外滚动页面。 但在某些情况下,你可能要保留浏览器事件的默认行为。此时,可以将图形的 `preventDefault` 属性设置为 `false`。 **操作说明:如果你使用移动设备,请尝试通过每个矩形滚动页面。 绿色矩形会阻止默认行为(页面不滚动)。 红色矩形会保留默认行为(页面可以滚动)。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); // green rectangle - will prevent scrolling const greenRect = new Konva.Rect({ x: 50, y: 50, width: 100, height: 600, fill: 'green', stroke: 'black', strokeWidth: 4, }); layer.add(greenRect); // red rectangle - will NOT prevent scrolling const redRect = new Konva.Rect({ x: 200, y: 50, width: 100, height: 600, fill: 'red', stroke: 'black', strokeWidth: 4, preventDefault: false, }); layer.add(redRect); ``` ```jsx import { Stage, Layer, Rect } from 'react-konva'; const App = () => { return ( ); }; export default App; ``` ```html ``` --- # 为什么处理函数在移动端触发两次,以及拖动之后的 click > 在 Konva 中一次点按可能触发 tap、pointerclick 和 click,因此绑定到 'click tap' 的处理函数在触摸设备上会执行两次。本文说明如何只绑定一个事件族,以及为什么拖动之后 click 本来就不会触发。 Source: https://konvajs.org/zh-Hans/docs/events/Mobile_Tap_And_Click.html 关于 Canvas 上的指针输入,有两件事常让人意外。一件是你必须处理的真实问题;另一件已经被 处理好了,知道这一点可以让你省掉一个并不需要的变通方案。 ## 一次点按可能触发三个事件 Konva 会监听全部三个 DOM 输入事件族——鼠标、触摸和指针——这样处理函数无需你事先选择就能 在各处工作。但它不会把它们合并。 在触摸屏上,浏览器会发送一组 `touchstart`/`touchend`、一组 `pointerdown`/`pointerup`, 然后为了兼容触摸出现之前编写的页面,再**合成**一组 `mousedown`/`mouseup`。Konva 会把它们 各自转换成自己的事件。一次点按会产生: | Konva 事件 | 来源 | | --- | --- | | `tap` | 触摸事件对 | | `pointerclick` | 指针事件对 | | `click` | 合成的鼠标事件对 | 因此下面这段代码在手机上会让处理函数执行**两次**,在桌面端执行一次: ```js // Fires for `click` and again for `tap` on a touch device. shape.on('click tap', handleSelect); ``` ### 只绑定一个事件族 最简单的办法是不再成对绑定事件,改用指针事件族——它已经覆盖了鼠标、触摸和触控笔: ```js shape.on('pointerclick', handleSelect); // once, on every device ``` `pointerdown`、`pointermove` 和 `pointerup` 的用法相同。指针事件默认开启 (`Konva.pointerEventsEnabled`),更多内容参见 [指针事件](/zh-Hans/docs/events/Pointer_Events.html)。 如果你需要支持不带指针事件的浏览器,或者正在维护已经成对绑定 `click tap` 的代码, 那就为处理函数加一道防护: ```js let lastHandled = 0; function handleSelect(e) { // A synthesised mouse event follows its touch within about 300ms. const now = Date.now(); if (now - lastHandled < 400) return; lastHandled = now; // ... } ``` 请优先只绑定一个事件族。计时器只是变通方案,它会吞掉手速快的用户真正的第二次点按。 ## 拖动之后 click 本来就不会触发 这一点值得了解,因为常见的变通方案其实没有必要。 当拖动真正开始时,Konva 会清除允许派发 click 的标志,因此 `dragend` 之后**不会** 再触发 `click`: ```js shape.on('dragend', () => console.log('dragged')); shape.on('click', () => console.log('clicked')); // Drag the shape: you get 'dragged' only. // Press and release without moving: you get 'clicked' only. ``` 如果你仍然看到拖动之后出现 click,通常是下面几种原因之一。 **你监听的是 `mouseup` 而不是 `click`。** `mouseup` 是原始指针事件,无论是否拖动都会触发。 Konva 抑制的是 `click`。 **指针的移动距离不足以算作拖动。** `Konva.dragDistance` 默认为 `3` 像素。按下时偏移 一两个像素——这在触摸屏上很正常——会被当作一次点击,`dragstart` 也不会触发。这是预期行为。 如果你的用户需要在很小的目标上操作且手不够稳,可以调高它: ```js Konva.dragDistance = 8; ``` **你把 `dragDistance` 设成了 `0`。** 那么每次按下都是拖动,于是你想要的 click 被抑制了。 ## 阻止浏览器自身的手势 与 Konva 无关的是,在 Canvas 上触摸可能会滚动页面或触发双击缩放。那是浏览器的行为, 需要用 CSS 阻止: ```css #container { touch-action: none; } ``` 如果页面仍应可以垂直滚动、同时由 Canvas 处理水平拖动,请改用 `touch-action: pan-y`。 完整说明参见[移动端滚动](/zh-Hans/docs/events/Mobile_Scrolling.html)。 ## 相关内容 - [指针事件](/zh-Hans/docs/events/Pointer_Events.html)——适用于所有设备的单一事件族 - [移动端事件](/zh-Hans/docs/events/Mobile_Events.html)——触摸事件列表 - [桌面端与移动端](/zh-Hans/docs/events/Desktop_and_Mobile.html)——较早的成对绑定做法 --- # HTML5 Canvas 多事件绑定教程 > 学习如何在 Konva 中使用 on() 方法和空格分隔的字符串,将多个事件绑定到一个处理程序。 Source: https://konvajs.org/zh-Hans/docs/events/Multi_Event.html 要使用 Konva 将多个事件绑定到一个处理程序,可以使用 `on()` 方法,并传入包含多个事件类型且以空格分隔的字符串。 ```js shape.on('mouseover mousedown mouseup', function (e) { console.log('events: ' + e.type); }); ``` **操作说明:在圆形上触发 mouseover、mousedown 和 mouseup,观察绑定到圆形的函数是否为每个事件执行。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const text = new Konva.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black', }); layer.add(text); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); function writeMessage(message) { text.text(message); } circle.on('mouseover mousedown mouseup', function (evt) { writeMessage('event: ' + evt.type); }); layer.add(circle); ``` ```jsx import { Stage, Layer, Circle, Text } from 'react-konva'; import { useState } from 'react'; const App = () => { const [message, setMessage] = useState(''); const handleMultiEvent = (e) => { setMessage('event: ' + e.type); }; return ( ); }; export default App; ``` ```html ``` --- # HTML5 Canvas 指针事件教程 > 学习如何在 Konva 中使用指针事件,通过一个事件处理程序同时处理鼠标和触摸输入。 Source: https://konvajs.org/zh-Hans/docs/events/Pointer_Events.html 指针事件可用于通过一个处理程序同时处理移动端和桌面端事件。 要使用 Konva 为图形绑定指针事件处理程序,可以使用 `on()` 方法。 `on()` 方法需要一个事件类型,以及一个在事件发生时执行的函数。 Konva 支持 `pointerdown`、`pointermove`、`pointerup`、`pointercancel`、`pointerover`、`pointerenter`、`pointerout`、`pointerleave`、`pointerclick`、`pointerdblclick` 事件。 _注意:此示例同时适用于移动端和桌面端设备。_ **操作说明:在三角形上移动鼠标或手指,以查看指针坐标。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); const text = new Konva.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black', }); const triangle = new Konva.RegularPolygon({ x: stage.width() / 2, y: stage.height() / 2, sides: 3, radius: 80, fill: '#00D2FF', stroke: 'black', strokeWidth: 4, }); function writeMessage(message) { text.text(message); } triangle.on('pointermove', function () { const pos = stage.getPointerPosition(); writeMessage('x: ' + pos.x + ', y: ' + pos.y); }); triangle.on('pointerout', function () { writeMessage(''); }); layer.add(triangle); layer.add(text); stage.add(layer); ``` ```jsx import { Stage, Layer, RegularPolygon, Text } from 'react-konva'; import { useState, useRef } from 'react'; const App = () => { const [message, setMessage] = useState(''); const stageRef = useRef(); const handlePointerMove = () => { const pos = stageRef.current.getPointerPosition(); setMessage(`x: ${pos.x}, y: ${pos.y}`); }; return ( setMessage('')} /> ); }; export default App; ``` ```html ``` --- # 使用 Konva 移除 HTML5 Canvas 事件监听器 > 学习如何使用 off() 方法从 Konva 图形中移除事件监听器。 Source: https://konvajs.org/zh-Hans/docs/events/Remove_Event.html 要使用 Konva 移除事件监听器,可以使用图形对象的 `off()` 方法。 此方法需要 click 或 mousedown 等事件类型。 **操作说明:单击圆形,查看 onclick 事件绑定触发的警告框。 单击按钮以移除事件监听器,然后再次单击圆形,观察事件绑定是否已移除。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); // add click listener circle.on('click', function () { alert('you clicked the circle'); }); layer.add(circle); // add button to remove listener const button = document.createElement('button'); button.style.position = 'absolute'; button.style.top = '10px'; button.style.left = '10px'; button.innerHTML = 'Remove click listener'; document.body.appendChild(button); button.addEventListener('click', () => { // remove click listener circle.off('click'); }); ``` ```jsx import { Stage, Layer, Circle } from 'react-konva'; import { useState } from 'react'; const App = () => { const [hasListener, setHasListener] = useState(true); return ( <> alert('you clicked the circle') : null} /> ); }; export default App; ``` ```html ``` --- # 使用 Konva 按名称移除 HTML5 Canvas 事件监听器 > 学习如何在 Konva 中配合命名事件使用 on() 和 off() 方法,按命名空间移除特定的事件监听器。 Source: https://konvajs.org/zh-Hans/docs/events/Remove_by_Name.html Konva 事件命名空间用于在命令式代码中标识相关的监听器。将命名空间添加到事件类型后, 例如 `click.menu`。然后将相同名称传给 `off()`,以移除该监听器。 **操作说明:**选择圆形以运行两个监听器。分别使用两个按钮移除一个监听器。 然后再次选择圆形。 `react-konva` 不通过 React 事件 props 公开 Konva 事件命名空间。 React 为每种事件类型提供一个 prop,例如 `onClick`。在 React 中存储启用状态, 并根据条件向该 prop 传入一个分派函数。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); // add click listeners circle.on('click.event1', function () { alert('first click listener'); }); circle.on('click.event2', function () { alert('second click listener'); }); layer.add(circle); // add buttons to remove listeners const button1 = document.createElement('button'); button1.innerHTML = 'Remove first listener'; button1.style.position = 'absolute'; button1.style.top = '0'; button1.style.left = '0'; button1.onclick = function() { circle.off('click.event1'); }; document.getElementById('container').appendChild(button1); const button2 = document.createElement('button'); button2.innerHTML = 'Remove second listener'; button2.style.position = 'absolute'; button2.style.top = '30px'; button2.style.left = '0'; button2.onclick = function() { circle.off('click.event2'); }; document.getElementById('container').appendChild(button2); ``` ```jsx import { useState } from 'react'; import { Stage, Layer, Circle, Text } from 'react-konva'; const App = () => { const [firstEnabled, setFirstEnabled] = useState(true); const [secondEnabled, setSecondEnabled] = useState(true); const [message, setMessage] = useState('Select the circle'); const handleClick = firstEnabled || secondEnabled ? () => { const messages = []; if (firstEnabled) { messages.push('first listener'); } if (secondEnabled) { messages.push('second listener'); } setMessage(messages.join(' + ')); } : undefined; return ( <> ); }; export default App; ``` --- # Konva 的 HTML5 Canvas 特殊舞台事件 > 了解 Konva.js 中特殊的舞台级事件,包括 contentClick、contentMousemove 和其他在 Stage 容器上触发的事件。 Source: https://konvajs.org/zh-Hans/docs/events/Stage_Events.html 所有事件都从图形开始。因此,如果单击 Canvas 内的空白区域,`click` 事件不会在 `Layer` 上触发,而会在 `Stage` 对象上触发。 **操作说明:单击空白区域和图形,查看不同的事件行为。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const text = new Konva.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black', }); layer.add(text); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); function writeMessage(message) { text.text(message); } // handle stage click stage.on('click', function (e) { if (e.target === stage) { writeMessage('clicked on stage'); return; } writeMessage('clicked on ' + e.target.name()); }); // add shape circle.name('circle'); layer.add(circle); ``` ```jsx import { Stage, Layer, Circle, Text } from 'react-konva'; import { useState } from 'react'; const App = () => { const [message, setMessage] = useState(''); const handleStageClick = (e) => { if (e.target === e.target.getStage()) { setMessage('clicked on stage'); return; } setMessage('clicked on ' + e.target.name()); }; return ( ); }; export default App; ``` ```html ``` --- # HTML5 Canvas 图像增亮滤镜教程 > 已弃用的 Konva.js Brighten 滤镜教程。请改用较新的 Brightness 滤镜调整图像亮度。 Source: https://konvajs.org/zh-Hans/docs/filters/Brighten.html **注意**:此滤镜已弃用,将来会移除。请改用 `Brightness` 滤镜。 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 增亮或调暗图像,可以使用 `Konva.Filters.Brighten` 滤镜,并通过 `brightness` 属性设置亮度。 `brightness` 属性可以设置为 -1 到 1 之间的任意数值。 负值会调暗图像,正值会增亮图像。 **操作说明**:滑动控件以调整亮度。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Brighten]); image.brightness(0.3); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '-1'; slider.max = '1'; slider.step = '0.1'; slider.value = image.brightness(); slider.style.position = 'absolute'; slider.style.top = '20px'; slider.style.left = '20px'; slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); image.brightness(value); }); document.body.appendChild(slider); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [brightness, setBrightness] = useState(0.3); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Brighten]} brightness={brightness} /> setBrightness(parseFloat(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像亮度滤镜教程 > 学习如何使用 Konva.js Brightness 滤镜调整 HTML5 Canvas 图像的亮度,其效果与 CSS 的 brightness 滤镜类似。 Source: https://konvajs.org/zh-Hans/docs/filters/Brightness.html **注意**:Konva 10.0.0 引入了此滤镜,用它替代 `Brighten` 滤镜。为保持向后兼容,`Brighten` 滤镜仍然可用,但它已弃用,将来会移除。 新滤镜的渲染结果更接近 CSS `filter: brightness(0.5);`。 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 增亮或调暗图像,可以使用 `Konva.Filters.Brightness` 滤镜,并通过 `brightness` 属性设置亮度。 `brightness` 属性可以设置为 0 到 2 之间的任意数字,其中: - 0 生成全黑图像 - 1 表示原始图像(无变化) - 大于 1 的值会增亮图像 - 2 生成非常明亮的图像 **操作说明**:滑动控件以调整亮度。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Brightness]); image.brightness(1.5); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0'; slider.max = '2'; slider.step = '0.1'; slider.value = image.brightness(); slider.style.position = 'absolute'; slider.style.top = '20px'; slider.style.left = '20px'; slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); image.brightness(value); }); document.body.appendChild(slider); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [brightness, setBrightness] = useState(1.5); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Brightness]} brightness={brightness} /> setBrightness(parseFloat(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像对比度滤镜教程 > 学习如何使用 Konva.js Contrast 滤镜和交互式滑块示例,调整 HTML5 Canvas 图像的对比度。 Source: https://konvajs.org/zh-Hans/docs/filters/Contrast.html 要对 `Konva.Node` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 更改图像对比度,可以使用 `Konva.Filters.Contrast`。 **操作说明**:滑动控件以更改对比度值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Contrast]); image.contrast(30); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '-100'; slider.max = '100'; slider.value = image.contrast(); slider.style.position = 'absolute'; slider.style.top = '20px'; slider.style.left = '20px'; slider.addEventListener('input', (e) => { const value = parseInt(e.target.value); image.contrast(value); }); document.body.appendChild(slider); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [contrast, setContrast] = useState(30); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Contrast]} contrast={contrast} /> setContrast(parseInt(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 自定义滤镜教程 > 学习如何通过直接处理 Canvas ImageData 像素,在 Konva.js 中创建并应用自定义图像滤镜。 Source: https://konvajs.org/zh-Hans/docs/filters/Custom_Filter.html ## 如何为 Konva 节点应用自定义滤镜? 本示例展示如何在 `Konva` 框架中使用自定义滤镜。 `Filter` 是一个以 Canvas ImageData 为输入的函数,它必须修改该数据。 ```javascript function Filter(imageData) { // do something with image data imageData.data[0] = 0; } ``` 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 有关自定义滤镜示例,另请参阅 [图像边框示例](/zh-Hans/docs/sandbox/Image_Border.html)。 **本示例将移除图像的全部透明度。** ```js import Konva from 'konva'; // create our custom filter Konva.Filters.RemoveAlpha = function (imageData) { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { data[i + 3] = 255; // set alpha to 1 } }; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.RemoveAlpha]); }; imageObj.src = 'https://konvajs.org/assets/lion.png'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; // create our custom filter Konva.Filters.RemoveAlpha = function (imageData) { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { data[i + 3] = 255; // set alpha to 1 } }; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [image] = useImage('https://konvajs.org/assets/lion.png', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image) { imageRef.current.cache(); } }, [image]); return ( { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.RemoveAlpha]} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像浮雕滤镜教程 > 学习如何使用 Konva.js 对 HTML5 Canvas 图像应用浮雕滤镜,并调整强度、白色级别和混合参数。 Source: https://konvajs.org/zh-Hans/docs/filters/Emboss.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 **操作说明**:滑动控件以更改浮雕参数值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Emboss]); image.embossStrength(0.5); image.embossWhiteLevel(0.5); image.embossDirection('top-left'); image.embossBlend(0.5); // create sliders const strengthSlider = document.createElement('input'); strengthSlider.type = 'range'; strengthSlider.min = '0'; strengthSlider.max = '1'; strengthSlider.step = '0.1'; strengthSlider.value = image.embossStrength(); strengthSlider.style.position = 'absolute'; strengthSlider.style.top = '20px'; strengthSlider.style.left = '20px'; const whiteLevelSlider = document.createElement('input'); whiteLevelSlider.type = 'range'; whiteLevelSlider.min = '0'; whiteLevelSlider.max = '1'; whiteLevelSlider.step = '0.1'; whiteLevelSlider.value = image.embossWhiteLevel(); whiteLevelSlider.style.position = 'absolute'; whiteLevelSlider.style.top = '45px'; whiteLevelSlider.style.left = '20px'; const blendSlider = document.createElement('input'); blendSlider.type = 'range'; blendSlider.min = '0'; blendSlider.max = '1'; blendSlider.step = '0.1'; blendSlider.value = image.embossBlend(); blendSlider.style.position = 'absolute'; blendSlider.style.top = '70px'; blendSlider.style.left = '20px'; // add labels const strengthLabel = document.createElement('div'); strengthLabel.textContent = 'Strength'; strengthLabel.style.position = 'absolute'; strengthLabel.style.top = '20px'; strengthLabel.style.left = '200px'; const whiteLevelLabel = document.createElement('div'); whiteLevelLabel.textContent = 'White Level'; whiteLevelLabel.style.position = 'absolute'; whiteLevelLabel.style.top = '45px'; whiteLevelLabel.style.left = '200px'; const blendLabel = document.createElement('div'); blendLabel.textContent = 'Blend'; blendLabel.style.position = 'absolute'; blendLabel.style.top = '70px'; blendLabel.style.left = '200px'; // add event listeners strengthSlider.addEventListener('input', (e) => { image.embossStrength(parseFloat(e.target.value)); }); whiteLevelSlider.addEventListener('input', (e) => { image.embossWhiteLevel(parseFloat(e.target.value)); }); blendSlider.addEventListener('input', (e) => { image.embossBlend(parseFloat(e.target.value)); }); // add elements to the page document.body.appendChild(strengthSlider); document.body.appendChild(whiteLevelSlider); document.body.appendChild(blendSlider); document.body.appendChild(strengthLabel); document.body.appendChild(whiteLevelLabel); document.body.appendChild(blendLabel); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [strength, setStrength] = useState(0.5); const [whiteLevel, setWhiteLevel] = useState(0.5); const [blend, setBlend] = useState(0.5); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Emboss]} embossStrength={strength} embossWhiteLevel={whiteLevel} embossDirection="top-left" embossBlend={blend} />
setStrength(parseFloat(e.target.value))} />
setWhiteLevel(parseFloat(e.target.value))} />
setBlend(parseFloat(e.target.value))} />
); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像增强滤镜教程 > 学习如何使用 Konva.js Enhance 滤镜增强 HTML5 Canvas 图像,并调整增强程度。 Source: https://konvajs.org/zh-Hans/docs/filters/Enhance.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 增强图像,可以使用 `Konva.Filters.Enhance` 滤镜 并通过 `enhance` 属性设置增强程度。 **操作说明**:滑动控件以调整增强值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Enhance]); image.enhance(0.4); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '-1'; slider.max = '1'; slider.step = '0.1'; slider.value = image.enhance(); slider.style.position = 'absolute'; slider.style.top = '20px'; slider.style.left = '20px'; slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); image.enhance(value); }); document.body.appendChild(slider); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [enhance, setEnhance] = useState(0.4); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Enhance]} enhance={enhance} /> setEnhance(parseFloat(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像灰度滤镜教程 > 学习如何使用 Konva.js Grayscale 滤镜,将 HTML5 Canvas 图像转换为灰度图像。 Source: https://konvajs.org/zh-Hans/docs/filters/Grayscale.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 反转图像颜色,可以使用 `Konva.Filters.Grayscale` 滤镜。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Grayscale]); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image) { imageRef.current.cache(); } }, [image]); return ( {image && ( { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Grayscale]} /> )} ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像色相、饱和度和亮度滤镜教程 > 学习如何使用 Konva.js HSL 滤镜调整 HTML5 Canvas 图像的色相、饱和度和亮度。 Source: https://konvajs.org/zh-Hans/docs/filters/HSL.html 要对 `Konva.Node` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 更改图像的色相、饱和度和亮度分量,可以使用 `Konva.Filters.HSL`。 **操作说明**:滑动控件以更改 HSL 值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.HSL]); // create sliders const createSlider = (label, min, max, defaultValue, property) => { const container = document.createElement('div'); container.style.position = 'absolute'; container.style.left = '20px'; const text = document.createElement('span'); text.textContent = `${label}: `; container.appendChild(text); const slider = document.createElement('input'); slider.type = 'range'; slider.min = min; slider.max = max; slider.step = '0.1'; slider.value = defaultValue; slider.style.width = '200px'; slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); image[property](value); }); container.appendChild(slider); return container; }; const hueSlider = createSlider('Hue', -180, 180, 0, 'hue'); hueSlider.style.top = '20px'; document.body.appendChild(hueSlider); const saturationSlider = createSlider('Saturation', -2, 10, 0, 'saturation'); saturationSlider.style.top = '45px'; document.body.appendChild(saturationSlider); const luminanceSlider = createSlider('Luminance', -2, 2, 0, 'luminance'); luminanceSlider.style.top = '70px'; document.body.appendChild(luminanceSlider); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [hue, setHue] = useState(0); const [saturation, setSaturation] = useState(0); const [luminance, setLuminance] = useState(0); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.HSL]} hue={hue} saturation={saturation} luminance={luminance} />
Hue setHue(parseInt(e.target.value))} />
Saturation setSaturation(parseFloat(e.target.value))} />
Luminance setLuminance(parseFloat(e.target.value))} />
); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像色相、饱和度和明度滤镜教程 > 学习如何使用 Konva.js HSV 滤镜调整 HTML5 Canvas 图像的色相、饱和度和明度。 Source: https://konvajs.org/zh-Hans/docs/filters/HSV.html 要对 `Konva.Node` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 更改图像的色相、饱和度和明度分量,可以使用 `Konva.Filters.HSV`。 **操作说明**:滑动控件以更改 HSV 值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.HSV]); // create sliders const createSlider = (label, min, max, defaultValue, property) => { const container = document.createElement('div'); container.style.position = 'absolute'; container.style.left = '20px'; const text = document.createElement('span'); text.textContent = `${label}: `; container.appendChild(text); const slider = document.createElement('input'); slider.type = 'range'; slider.min = min; slider.max = max; slider.step = '0.1'; slider.value = defaultValue; slider.style.width = '200px'; slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); image[property](value); }); container.appendChild(slider); return container; }; const hueSlider = createSlider('Hue', -180, 180, 0, 'hue'); hueSlider.style.top = '20px'; document.body.appendChild(hueSlider); const saturationSlider = createSlider('Saturation', -2, 10, 0, 'saturation'); saturationSlider.style.top = '45px'; document.body.appendChild(saturationSlider); const value = createSlider('Value', -2, 2, 0, 'value'); value.style.top = '70px'; document.body.appendChild(value); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [hue, setHue] = useState(0); const [saturation, setSaturation] = useState(0); const [value, setValue] = useState(0); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.HSV]} hue={hue} saturation={saturation} value={value} />
Hue setHue(parseInt(e.target.value))} />
Saturation setSaturation(parseFloat(e.target.value))} />
Value setValue(parseFloat(e.target.value))} />
); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像反色滤镜教程 > 学习如何使用 Konva.js Invert 滤镜反转 HTML5 Canvas 图像的颜色,以产生照片负片效果。 Source: https://konvajs.org/zh-Hans/docs/filters/Invert.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 反转图像颜色,可以使用 `Konva.Filters.Invert` 滤镜。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Invert]); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useRef, useEffect } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( {image && ( { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Invert]} /> )} ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像万花筒滤镜教程 > 学习如何使用 Konva.js Kaleidoscope 滤镜为图像创建万花筒效果,并调整强度和角度。 Source: https://konvajs.org/zh-Hans/docs/filters/Kaleidoscope.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 创建万花筒效果,可以使用 `Konva.Filters.Kaleidoscope` 滤镜,并设置 `kaleidoscopePower` 和 `kaleidoscopeAngle` 属性。 **操作说明**:滑动控件以调整万花筒的强度和角度。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Kaleidoscope]); image.kaleidoscopePower(3); image.kaleidoscopeAngle(0); // create sliders const createSlider = (label, min, max, defaultValue, property) => { const container = document.createElement('div'); container.style.position = 'absolute'; container.style.left = '20px'; const text = document.createElement('span'); text.textContent = `${label}: `; container.appendChild(text); const slider = document.createElement('input'); slider.type = 'range'; slider.min = min; slider.max = max; slider.step = property === 'kaleidoscopePower' ? '1' : '0.1'; slider.value = defaultValue; slider.style.width = '200px'; slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); image[property](value); }); container.appendChild(slider); return container; }; const powerSlider = createSlider('Power', 2, 8, 3, 'kaleidoscopePower'); powerSlider.style.top = '20px'; document.body.appendChild(powerSlider); const angleSlider = createSlider('Angle', 0, 360, 0, 'kaleidoscopeAngle'); angleSlider.style.top = '45px'; document.body.appendChild(angleSlider); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [angle, setAngle] = useState(0); const [power, setPower] = useState(3); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Kaleidoscope]} kaleidoscopePower={power} kaleidoscopeAngle={angle} />
Power setPower(parseInt(e.target.value))} style={{ width: '200px' }} />
Angle setAngle(parseFloat(e.target.value))} style={{ width: '200px' }} />
); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像遮罩滤镜教程 > 学习如何使用 Konva.js Mask 滤镜移除 HTML5 Canvas 图像背景,并调整阈值。 Source: https://konvajs.org/zh-Hans/docs/filters/Mask.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 对图像颜色应用遮罩,可以使用 `Konva.Filters.Mask` 滤镜。 `Konva.Filters.Mask` 滤镜会尝试移除图像背景。工作过程如下: 1. 对图像四个角的颜色进行采样。 2. 如果角落颜色相似(差异在 `threshold` 范围内),则假定此颜色代表背景。 3. 然后创建遮罩。与已识别背景颜色相似的像素变为透明,其他像素保持不透明。 4. 使用图像处理技术(例如腐蚀和膨胀)优化遮罩,以去除噪点并平滑边缘。 5. 最后,将优化后的遮罩应用于图像的 Alpha 通道。 `threshold` 属性的范围是 0 到 255。它控制像素颜色必须多接近背景颜色才会被遮罩。较低的阈值只移除非常接近背景的颜色,较高的阈值会移除更大范围的颜色。 **操作说明**:滑动控件以调整遮罩阈值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Mask]); image.threshold(10); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0'; slider.max = '255'; slider.value = image.threshold(); slider.style.position = 'absolute'; slider.style.top = '20px'; slider.style.left = '20px'; slider.addEventListener('input', (e) => { const value = parseInt(e.target.value); image.threshold(value); }); document.body.appendChild(slider); }; imageObj.src = 'https://konvajs.org/assets/space.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [threshold, setThreshold] = useState(10); const [image] = useImage('https://konvajs.org/assets/space.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Mask]} threshold={threshold} /> setThreshold(parseInt(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 多个滤镜教程 > 学习如何使用 Konva.js 同时对图像应用模糊、亮度和对比度等多个滤镜。 Source: https://konvajs.org/zh-Hans/docs/filters/Multiple_Filters.html 要对 `Konva.Image` 应用多个滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 **操作说明**:使用复选框切换不同滤镜的启用状态,并使用滑块调整滤镜值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); // Create controls container const container = document.createElement('div'); container.style.position = 'absolute'; container.style.top = '20px'; container.style.left = '20px'; document.body.appendChild(container); // Filter states const filterStates = { blur: false, brightness: false, contrast: false, }; const filterValues = { blur: 10, brightness: 1.3, contrast: 50, }; // Create filter controls const createFilterControl = (name, min, max, step, defaultValue) => { const div = document.createElement('div'); div.style.marginBottom = '10px'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = name; checkbox.checked = filterStates[name]; const label = document.createElement('label'); label.htmlFor = name; label.textContent = ` ${name.charAt(0).toUpperCase() + name.slice(1)}: `; const slider = document.createElement('input'); slider.type = 'range'; slider.min = min; slider.max = max; slider.step = step; slider.value = defaultValue; slider.style.width = '200px'; slider.disabled = !filterStates[name]; div.appendChild(checkbox); div.appendChild(label); div.appendChild(slider); checkbox.addEventListener('change', (e) => { filterStates[name] = e.target.checked; slider.disabled = !e.target.checked; updateFilters(); }); slider.addEventListener('input', (e) => { filterValues[name] = parseFloat(e.target.value); updateFilters(); }); return div; }; // Add controls container.appendChild(createFilterControl('blur', 0, 40, 1, filterValues.blur)); container.appendChild(createFilterControl('brightness', 0, 2, 0.1, filterValues.brightness)); container.appendChild(createFilterControl('contrast', -100, 100, 1, filterValues.contrast)); function updateFilters() { const activeFilters = []; if (filterStates.blur) { activeFilters.push(Konva.Filters.Blur); image.blurRadius(filterValues.blur); } if (filterStates.brightness) { activeFilters.push(Konva.Filters.Brightness); image.brightness(filterValues.brightness); } if (filterStates.contrast) { activeFilters.push(Konva.Filters.Contrast); image.contrast(filterValues.contrast); } image.filters(activeFilters); } }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const FilterControl = ({ name, min, max, step, filters, setFilters }) => { const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1); return (
{ setFilters({ ...filters, [name]: { ...filters[name], active: e.target.checked }, }); }} /> { setFilters({ ...filters, [name]: { ...filters[name], value: parseFloat(e.target.value) }, }); }} style={{ width: '200px' }} />
); }; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [filters, setFilters] = useState({ blur: { active: false, value: 10 }, brightness: { active: false, value: 1.3 }, contrast: { active: false, value: 50 }, }); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); const activeFilters = []; if (filters.blur.active) activeFilters.push(Konva.Filters.Blur); if (filters.brightness.active) activeFilters.push(Konva.Filters.Brightness); if (filters.contrast.active) activeFilters.push(Konva.Filters.Contrast); return ( <> {image && ( { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={activeFilters} blurRadius={filters.blur.value} brightness={filters.brightness.value} contrast={filters.contrast.value} /> )}
); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像噪点滤镜教程 > 学习如何使用 Konva.js Noise 滤镜为 HTML5 Canvas 图像添加噪点效果,并调整噪点强度。 Source: https://konvajs.org/zh-Hans/docs/filters/Noise.html 要对 `Konva.Node` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 更改图像噪点,可以使用 `Konva.Filters.Noise`。 **操作说明**:滑动控件以更改噪点值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Noise]); image.noise(0.3); // create slider const container = document.createElement('div'); container.style.position = 'absolute'; container.style.top = '20px'; container.style.left = '20px'; const text = document.createElement('span'); text.textContent = 'Noise: '; container.appendChild(text); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0'; slider.max = '1'; slider.step = '0.1'; slider.value = image.noise(); slider.style.width = '200px'; slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); image.noise(value); }); container.appendChild(slider); document.body.appendChild(container); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [noise, setNoise] = useState(0.3); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Noise]} noise={noise} /> setNoise(parseFloat(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像像素化滤镜教程 > 学习如何使用 Konva.js Pixelate 滤镜对 HTML5 Canvas 图像进行像素化处理,并调整像素大小。 Source: https://konvajs.org/zh-Hans/docs/filters/Pixelate.html 要对 `Konva.Node` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 更改图像的像素化效果,可以使用 `Konva.Filters.Pixelate`。 **操作说明**:滑动控件以更改像素大小。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Pixelate]); image.pixelSize(8); // create slider const container = document.createElement('div'); container.style.position = 'absolute'; container.style.top = '20px'; container.style.left = '20px'; const text = document.createElement('span'); text.textContent = 'Pixel Size: '; container.appendChild(text); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '2'; slider.max = '32'; slider.step = '1'; slider.value = image.pixelSize(); slider.style.width = '200px'; slider.addEventListener('input', (e) => { const value = parseInt(e.target.value); image.pixelSize(value); }); container.appendChild(slider); document.body.appendChild(container); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [pixelSize, setPixelSize] = useState(8); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Pixelate]} pixelSize={pixelSize} /> setPixelSize(parseInt(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像 RGB 滤镜教程 > 学习如何使用 Konva.js RGB 滤镜调整 HTML5 Canvas 图像的红色、绿色和蓝色通道。 Source: https://konvajs.org/zh-Hans/docs/filters/RGB.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 更改图像的 RGB 分量,可以使用 `Konva.Filters.RGB`。 **操作说明**:滑动控件以更改 RGB 值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.RGB]); image.red(100); image.green(100); image.blue(100); // create sliders const createSlider = (label, property) => { const container = document.createElement('div'); container.style.position = 'absolute'; container.style.left = '20px'; const text = document.createElement('span'); text.textContent = `${label}: `; container.appendChild(text); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0'; slider.max = '255'; slider.step = '1'; slider.value = image[property](); slider.style.width = '200px'; slider.addEventListener('input', (e) => { const value = parseInt(e.target.value); image[property](value); }); container.appendChild(slider); return container; }; const redSlider = createSlider('Red', 'red'); redSlider.style.top = '20px'; document.body.appendChild(redSlider); const greenSlider = createSlider('Green', 'green'); greenSlider.style.top = '45px'; document.body.appendChild(greenSlider); const blueSlider = createSlider('Blue', 'blue'); blueSlider.style.top = '70px'; document.body.appendChild(blueSlider); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [red, setRed] = useState(100); const [green, setGreen] = useState(100); const [blue, setBlue] = useState(100); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.RGB]} red={red} green={green} blue={blue} />
Red setRed(parseInt(e.target.value))} />
Green setGreen(parseInt(e.target.value))} />
Blue setBlue(parseInt(e.target.value))} />
); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像棕褐色滤镜教程 > 学习如何使用 Konva.js Sepia 滤镜为 HTML5 Canvas 图像应用棕褐色调效果。 Source: https://konvajs.org/zh-Hans/docs/filters/Sepia.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 为图像应用棕褐色效果,可以使用 `Konva.Filters.Sepia`。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); // Apply Sepia filter image.cache(); image.filters([Konva.Filters.Sepia]); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useRef, useEffect } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( {image && ( { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Sepia]} /> )} ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像曝光过度滤镜教程 > 学习如何使用 Konva.js Solarize 滤镜为 HTML5 Canvas 图像应用曝光过度效果,并调整阈值。 Source: https://konvajs.org/zh-Hans/docs/filters/Solarize.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 为图像应用曝光过度效果,可以使用 `Konva.Filters.Solarize`。 **操作说明**:滑动控件以更改阈值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Solarize]); image.threshold(0.5); // create slider const container = document.createElement('div'); container.style.position = 'absolute'; container.style.top = '20px'; container.style.left = '20px'; const text = document.createElement('span'); text.textContent = 'Threshold: '; container.appendChild(text); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0'; slider.max = '1'; slider.step = '0.1'; slider.value = image.threshold(); slider.style.width = '200px'; slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); image.threshold(value); }); container.appendChild(slider); document.body.appendChild(container); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [threshold, setThreshold] = useState(0.5); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Solarize]} threshold={threshold} /> setThreshold(parseFloat(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # HTML5 Canvas 图像阈值滤镜教程 > 学习如何使用 Konva.js Threshold 滤镜将图像转换为黑白图像,并调整阈值。 Source: https://konvajs.org/zh-Hans/docs/filters/Threshold.html 要对 `Konva.Image` 应用滤镜,必须先使用 `cache()` 函数缓存它。然后使用 `filters()` 函数应用滤镜。 要使用 Konva 为图像应用阈值效果,可以使用 `Konva.Filters.Threshold`。 阈值滤镜将图像转换为黑白图像。高于阈值的所有像素会变为白色,低于阈值的所有像素会变为黑色。 **操作说明**:滑动控件以更改阈值。 有关全部可用滤镜,请参阅 [滤镜文档](/zh-Hans/api/Konva.Filters.html)。 ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); const imageObj = new Image(); imageObj.onload = () => { const image = new Konva.Image({ x: 50, y: 50, image: imageObj, draggable: true, }); layer.add(image); image.cache(); image.filters([Konva.Filters.Threshold]); image.threshold(0.5); // create slider const container = document.createElement('div'); container.style.position = 'absolute'; container.style.top = '20px'; container.style.left = '20px'; const text = document.createElement('span'); text.textContent = 'Threshold: '; container.appendChild(text); const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0'; slider.max = '1'; slider.step = '0.1'; slider.value = image.threshold(); slider.style.width = '200px'; slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); image.threshold(value); }); container.appendChild(slider); document.body.appendChild(container); }; imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg'; imageObj.crossOrigin = 'anonymous'; ``` ```js import Konva from 'konva'; import { Stage, Layer, Image } from 'react-konva'; import { useState, useEffect, useRef } from 'react'; import useImage from 'use-image'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); const [threshold, setThreshold] = useState(0.5); const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const imageRef = useRef(null); useEffect(() => { if (image && imageRef.current) { imageRef.current.cache(); } }, [image]); return ( <> { setPosition({ x: e.target.x(), y: e.target.y() }); }} filters={[Konva.Filters.Threshold]} threshold={threshold} /> setThreshold(parseFloat(e.target.value))} style={{ position: 'absolute', top: '20px', left: '20px' }} /> ); }; export default App; ``` ```js ``` --- # 将图形移动到其他容器 > 学习如何在 Konva 中使用 moveTo() 方法,在组、图层和容器之间移动图形。 Source: https://konvajs.org/zh-Hans/docs/groups_and_layers/Change_Containers.html 要使用 Konva 将图形从一个容器移动到另一个容器,可以使用 `moveTo()` 方法。此方法需要一个容器作为参数。 容器可以是另一个舞台、图层或组。你也可以将组移动到其他组或图层中,或将组中的图形直接移动到其他图层中。 **操作说明:拖放这些组,观察红色矩形绑定到黄色组还是蓝色组。使用左侧按钮将矩形从一个组移动到另一个组。** ```js import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); // yellow group const group1 = new Konva.Group({ x: 50, y: 50, draggable: true, }); const yellow = new Konva.Rect({ width: 100, height: 100, fill: 'yellow', stroke: 'black', strokeWidth: 4, }); group1.add(yellow); // blue group const group2 = new Konva.Group({ x: 200, y: 50, draggable: true, }); const blue = new Konva.Rect({ width: 100, height: 100, fill: 'blue', stroke: 'black', strokeWidth: 4, }); group2.add(blue); // red box const redBox = new Konva.Rect({ x: 10, y: 10, width: 30, height: 30, fill: 'red', }); group1.add(redBox); layer.add(group1); layer.add(group2); stage.add(layer); // create buttons const moveToGroup1Btn = document.createElement('button'); moveToGroup1Btn.textContent = 'Move to yellow group'; moveToGroup1Btn.addEventListener('click', () => { redBox.moveTo(group1); }); const moveToGroup2Btn = document.createElement('button'); moveToGroup2Btn.textContent = 'Move to blue group'; moveToGroup2Btn.addEventListener('click', () => { redBox.moveTo(group2); }); document.body.appendChild(moveToGroup1Btn); document.body.appendChild(moveToGroup2Btn); ```` ```js import { Stage, Layer, Rect, Group } from 'react-konva'; import { useState } from 'react'; const App = () => { const [redBoxGroup, setRedBoxGroup] = useState('yellow'); const [groupPositions, setGroupPositions] = useState({ yellow: { x: 50, y: 50 }, blue: { x: 200, y: 50 }, }); const handleDragEnd = (group, e) => { setGroupPositions((positions) => ({ ...positions, [group]: { x: e.target.x(), y: e.target.y() }, })); }; return ( <> handleDragEnd('yellow', e)} > {redBoxGroup === 'yellow' && ( )} handleDragEnd('blue', e)} > {redBoxGroup === 'blue' && ( )} ); }; export default App; ```` ```js ``` --- # 图形组 > 学习如何使用 Konva.Group 将多个图形组合在一起,并将它们作为一个整体移动、旋转和缩放。 Source: https://konvajs.org/zh-Hans/docs/groups_and_layers/Groups.html 要使用 Konva 将多个图形组合在一起,可以实例化一个 `Konva.Group()` 对象,然后使用 `add()` 方法向其中添加图形。 如果要同时移动、旋转或缩放多个图形,将这些图形组合在一起非常方便。 还可以将组添加到其他组中,以创建更复杂的节点树。 有关属性和方法的完整列表,请参阅 [Konva.Group 文档](/zh-Hans/api/Konva.Group.html)。如果要将不透明度或混合模式作为一个整体应用于组,请参阅[隔离组](/zh-Hans/docs/groups_and_layers/Isolated_Groups.html)。 **操作说明:尝试拖动组。观察所有图形如何一起移动。** ```js import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); const group = new Konva.Group({ x: 50, y: 50, draggable: true, }); const circle = new Konva.Circle({ x: 40, y: 40, radius: 30, fill: 'red', stroke: 'black', strokeWidth: 4, }); const rect = new Konva.Rect({ x: 80, y: 20, width: 100, height: 50, fill: 'green', stroke: 'black', strokeWidth: 4, }); group.add(circle); group.add(rect); layer.add(group); stage.add(layer); ```` ```js import { Stage, Layer, Group, Circle, Rect } from 'react-konva'; import { useState } from 'react'; const App = () => { const [position, setPosition] = useState({ x: 50, y: 50 }); return ( { setPosition({ x: e.target.x(), y: e.target.y() }); }} > ); }; export default App; ```` ```js ``` --- # HTML5 Canvas 隔离组:组不透明度与混合模式 > 使用 Konva.Group 的 isolated 属性,将不透明度和混合模式一次性应用于整组图形,并将子节点的混合模式和擦除效果限制在组内。 Source: https://konvajs.org/zh-Hans/docs/groups_and_layers/Isolated_Groups.html 默认情况下,`Konva.Group` 只负责组织其子节点。绘制组时,每个子节点都会直接绘制到图层 Canvas 上: - 组的 `opacity` 会乘到每个子节点上,因此你可以看到子节点的重叠区域。 - 设置了 `globalCompositeOperation`(例如 `multiply` 或 `destination-out`)的子节点,会与图层上已绘制的所有内容混合,而不仅仅是与其兄弟节点混合。 设置 `isolated: true` 可以将组作为一个图像绘制。隔离组会先将子节点绘制到一个单独的透明 Canvas 上,然后一步将结果放到图层上,并只应用一次自身的 `opacity` 和 `globalCompositeOperation`。其效果类似于 CSS 中的 `isolation: isolate` 以及 SVG `` 上的不透明度。 ```javascript const group = new Konva.Group({ isolated: true, opacity: 0.6, }); ``` 启用隔离后: - **组的不透明度应用于整个图像。** 重叠的子节点不会互相透出。 - **组的混合模式应用于整个图像。** `group.globalCompositeOperation('multiply')` 会将合成后的组与其下方的内容混合。 - **子节点的混合模式限制在组内。** 设置了 `destination-out` 的子节点只会擦除组自身的内容,而不会擦除图层的其余部分。 隔离功能需要 Konva 10.7.0 或更高版本。有关完整的属性列表,请参阅 [Konva.Group 文档](/zh-Hans/api/Konva.Group.html)。 **操作说明:将两个组拖到条纹背景上。左侧是普通组:其图形会互相透出,设置了 `destination-out` 的黑色圆形还会部分擦除条纹。右侧是隔离组:它作为一个整体图像变透明,孔洞中显示的是其后面的条纹。** ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); // stripes, to see what the groups let through for (let x = 0; x < stage.width(); x += 40) { layer.add( new Konva.Rect({ x: x, width: 20, height: stage.height(), fill: '#8ecae6', }) ); } function createGroup(x, isolated) { const group = new Konva.Group({ x: x, y: 40, opacity: 0.6, isolated: isolated, draggable: true, }); group.add( new Konva.Rect({ width: 120, height: 120, fill: '#e63946', }) ); group.add( new Konva.Circle({ x: 120, y: 120, radius: 70, fill: '#1d3557', }) ); // erases everything under it: the whole layer, or only the isolated group group.add( new Konva.Circle({ x: 60, y: 60, radius: 30, fill: 'black', globalCompositeOperation: 'destination-out', }) ); group.add( new Konva.Text({ y: 205, text: isolated ? 'isolated: true' : 'default group', fontSize: 18, }) ); layer.add(group); } createGroup(40, false); createGroup(300, true); ``` ```js import { Stage, Layer, Group, Rect, Circle, Text } from 'react-konva'; import { useState } from 'react'; const width = window.innerWidth; const height = window.innerHeight; const stripes = Array.from({ length: Math.ceil(width / 40) }, (_, i) => i * 40); const App = () => { const [groups, setGroups] = useState([ { id: 'default', x: 40, y: 40, isolated: false }, { id: 'isolated', x: 300, y: 40, isolated: true }, ]); const handleDragEnd = (id, e) => { const { x, y } = e.target.position(); setGroups((current) => current.map((group) => (group.id === id ? { ...group, x, y } : group)) ); }; return ( {/* stripes, to see what the groups let through */} {stripes.map((x) => ( ))} {groups.map((group) => ( handleDragEnd(group.id, e)} > {/* erases everything under it: the whole layer, or only the isolated group */} ))} ); }; export default App; ``` ```js ``` ## 隔离还是缓存? [`group.cache()`](/zh-Hans/docs/performance/Shape_Caching.html) 也会将组作为一个图像绘制,但它生成的是快照。更改子节点后,必须再次调用 `cache()`。隔离组则保持实时更新:更改、动画化或拖动任意子节点,下一次绘制就会显示结果。 代价是每次绘制图层时都要经过一个缓冲 Canvas。Konva 会让该缓冲区只与组的可见部分一样大,在多个隔离组之间复用它,并在不再使用时将其释放。对于会变化的内容,请使用隔离。对于很少变化的复杂组,或者需要使用[滤镜](/zh-Hans/docs/filters/Blur.html)时,请使用 `cache()`。 ## 注意事项 - Konva 根据组的边界确定缓冲区的大小。如果[自定义图形](/zh-Hans/docs/shapes/Custom.html)在其 `width` 和 `height` 之外绘制内容,请使用 `selfRectFunc` 描述绘制区域,否则这部分内容可能会被裁剪。 - 隔离只改变组的外观。命中检测保持不变:被兄弟节点擦除的图形仍然会接收事件。 - 缓存组会保留其快照。如果对隔离组调用 `cache()`,在调用 `clearCache()` 之前,将一直绘制该快照。 --- # 图形层叠 > 学习如何在 Konva 中使用 moveToTop、moveToBottom、moveUp 和 moveDown 方法调整 Canvas 上图形的顺序。 Source: https://konvajs.org/zh-Hans/docs/groups_and_layers/Layering.html 要使用 Konva 设置图形的层叠顺序,可以使用以下任一层叠方法: `moveToTop()`、`moveToBottom()`、`moveUp()`、`moveDown()` 或 `zIndex()`。 你也可以设置组和图层的层叠顺序。 **操作说明:拖放矩形以移动它们,然后使用左侧按钮调整黄色矩形的顺序。** ```js import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); const yellowBox = new Konva.Rect({ x: 50, y: 50, width: 100, height: 100, fill: 'yellow', stroke: 'black', strokeWidth: 4, draggable: true, }); const redBox = new Konva.Rect({ x: 100, y: 100, width: 100, height: 100, fill: 'red', stroke: 'black', strokeWidth: 4, draggable: true, }); layer.add(yellowBox); layer.add(redBox); stage.add(layer); // create buttons const toTopBtn = document.createElement('button'); toTopBtn.textContent = 'Move yellow box to top'; toTopBtn.addEventListener('click', () => { yellowBox.moveToTop(); }); const toBottomBtn = document.createElement('button'); toBottomBtn.textContent = 'Move yellow box to bottom'; toBottomBtn.addEventListener('click', () => { yellowBox.moveToBottom(); }); document.body.prepend(toTopBtn); document.body.prepend(toBottomBtn); ```` ```js import { Stage, Layer, Rect } from 'react-konva'; import { useRef, useState } from 'react'; const App = () => { const yellowRef = useRef(); const [positions, setPositions] = useState({ yellow: { x: 50, y: 50 }, red: { x: 100, y: 100 }, }); const handleDragEnd = (color, e) => { setPositions((currentPositions) => ({ ...currentPositions, [color]: { x: e.target.x(), y: e.target.y() }, })); }; return ( <> handleDragEnd('yellow', e)} /> handleDragEnd('red', e)} /> ); }; export default App; ```` ```js ``` --- # 理解节点的 zIndex > 了解 zIndex 在 Konva 中如何表示节点在其父节点 children 数组中的索引,以及它与 CSS z-index 的区别。 Source: https://konvajs.org/zh-Hans/docs/groups_and_layers/zIndex.html ## 节点的 zIndex 是什么? 可以通过以下方式获取或设置节点的 zIndex: ```javascript // get const zIndex = shape.zIndex(); // set shape.zIndex(1); ``` zIndex 只是节点在其父节点 children 数组中的索引。不要混淆 Konva 中的 `zIndex` 和 CSS 中的 `z-index`。 ```javascript const group = new Konva.Group(); const circle = new Konva.Circle({}); group.add(circle); // it will log 0 console.log(circle.zIndex()); // the next line will not work because the group has only one child circle.zIndex(1); // still logs 0 console.log(circle.zIndex()); // for any node this equation will be true: console.log(circle.zIndex() === circle.getParent().children.indexOf(circle)); ``` 不能像在 CSS 中那样使用 `zIndex` 设置节点的绝对位置。 Konva 严格按照节点在节点树中的定义顺序绘制节点。 **操作说明:尝试使用按钮更改图形的 zIndex。观察图形顺序如何变化。** ```js import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); // first group const group1 = new Konva.Group(); layer.add(group1); const blackRect = new Konva.Rect({ x: 10, y: 10, width: 100, height: 100, fill: 'black', }); group1.add(blackRect); const redCircle = new Konva.Circle({ x: 80, y: 80, radius: 40, fill: 'red', }); group1.add(redCircle); // second group const group2 = new Konva.Group(); layer.add(group2); const greenRect = new Konva.Rect({ x: 50, y: 50, width: 100, height: 100, fill: 'green', }); group2.add(greenRect); stage.add(layer); // create buttons const btn1 = document.createElement('button'); btn1.textContent = 'Move red circle to group2'; btn1.addEventListener('click', () => { redCircle.moveTo(group2); }); const btn2 = document.createElement('button'); btn2.textContent = 'Move red circle to group1'; btn2.addEventListener('click', () => { redCircle.moveTo(group1); }); document.body.appendChild(btn1); document.body.appendChild(btn2); ```` ```js import { Stage, Layer, Group, Rect, Circle } from 'react-konva'; import { useState } from 'react'; const App = () => { const [redCircleGroup, setRedCircleGroup] = useState('group1'); return ( <> {redCircleGroup === 'group1' && ( )} {redCircleGroup === 'group2' && ( )} ); }; export default App; ```` ```js ``` --- # HTML5 Canvas 避免内存泄漏技巧 > 使用 Konva.js 防止 HTML5 Canvas 应用发生内存泄漏。学习如何正确销毁图形、移除事件并管理 Canvas 资源。 Source: https://konvajs.org/zh-Hans/docs/performance/Avoid_Memory_Leaks.html ### 删除图形 `remove()` 和 `destroy()` 是两个非常相似的方法。如果需要彻底删除节点,请使用 `destroy()`。`destroy()` 方法从 KonvaJS 引擎中删除对该节点的所有引用。如果要重复使用节点,请使用 `remove()`,之后可以再次将该节点添加到任何容器。 ### 补间 使用 `Konva.Tween` 实例时,必须在使用后将其销毁。 以下示例展示了正确的内存管理: ```js import Konva from 'konva'; const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); // Create circle const circle = new Konva.Circle({ x: 100, y: 100, radius: 30, fill: 'red' }); layer.add(circle); // Add buttons const addButton = document.createElement('button'); addButton.textContent = 'Add Circle'; document.body.appendChild(addButton); const removeButton = document.createElement('button'); removeButton.textContent = 'Remove Circle'; document.body.appendChild(removeButton); const animateButton = document.createElement('button'); animateButton.textContent = 'Animate'; document.body.appendChild(animateButton); // Handle adding/removing addButton.addEventListener('click', () => { layer.add(circle); }); removeButton.addEventListener('click', () => { // Just remove from layer, can be added back circle.remove(); }); animateButton.addEventListener('click', () => { // Using to() method which auto-destroys the tween circle.to({ x: Math.random() * stage.width(), y: Math.random() * stage.height(), duration: 1 }); // If using Tween directly, make sure to destroy it const tween = new Konva.Tween({ node: circle, rotation: 360, duration: 1, onFinish: function() { // Clean up the tween tween.destroy(); } }).play(); }); ``` ```js import { Stage, Layer, Circle } from 'react-konva'; import { useState } from 'react'; const App = () => { const [isVisible, setIsVisible] = useState(true); const [position, setPosition] = useState({ x: 100, y: 100 }); const [rotation, setRotation] = useState(0); const handleAdd = () => { setIsVisible(true); }; const handleRemove = () => { setIsVisible(false); }; const handleAnimate = () => { // Update position setPosition({ x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight }); // Update rotation setRotation(rotation + 360); }; return (
{isVisible && ( )}
); }; export default App; ``` ```js ``` --- # 自动重绘——你需要调用 draw() 或 batchDraw() 吗? > 自 Konva 8 起重绘是自动的,因此在修改图形之后不再需要 layer.draw() 和 layer.batchDraw()。本文说明何时仍需手动重绘,以及如何关闭批量绘制。 Source: https://konvajs.org/zh-Hans/docs/performance/Batch_Draw.html **简短回答:不需要。自 Konva 8 起,修改图形之后不需要调用 `draw()` 或 `batchDraw()`。** ```js // This is enough. Konva schedules the redraw itself. rect.fill('red'); rect.x(120); // Both of these are redundant. layer.draw(); layer.batchDraw(); ``` 设置任何属性都会把图层标记为需要重绘,Konva 会在下一个动画帧中重绘一次。连续修改十个属性 仍然只会产生一次重绘,因此你自己调用 `batchDraw()` 不会带来任何收益——批量处理已经发生了。 你仍然会在较旧的教程、2021 年之前写的回答,以及 AI 助手生成的代码中看到 `layer.draw()`, 因为语料库里到处都是它。它是无害的,只是没有必要。删掉这些调用不会改变屏幕上的显示结果。 ## 什么时候仍然需要手动重绘 自动重绘响应的是 *Konva* 的变化。如果变化发生在 Konva 的感知之外,就没有任何东西会把图层 标记为需要重绘,你必须自己说明。 常见情况是 `Konva.Image` 背后的源会自行变化——`