# Konva.js 中文文档 · 全文 本文件收录 49 个核心页面的完整正文。页面清单与其余页面见 https://konva-doc-cn.front-end-js.top/llms.txt。 --- ## 事件绑定 来源:https://konva-doc-cn.front-end-js.top/docs/events/binding-events 事件绑定 事件是 Konva 交互的起点。它的事件系统模仿 DOM,但有几处关键差别值得先弄清。 用法​ 使用Konva检测形状事件,我们可以使用on()方法将事件处理程序绑定到节点. on()方法需要事件类型和回调函数。 Konva支持mouseover,mouseout,mouseenter,mouseleave,mousemove,mousedown,mouseup,wheel,click,dblclick,dragstart,dragmove和dragend桌面事件。 说明:鼠标悬停和鼠标移出三角形,鼠标悬停,鼠标悬停,鼠标悬停和鼠标悬停在圆圈上 Konva Shape Events Demo
常见问题​ e.target 和 e.currentTarget 有什么区别?​ e.target 是实际被点中的图形,e.currentTarget 是绑定处理器的那个节点。 直接绑在图形上时两者相同,所以容易忽略这个区别。一旦把处理器绑到图层或分组上做 事件委托,e.currentTarget 就是那个容器, 而 e.target 才是用户真正点的东西。写委托逻辑时用错会导致所有点击都被当成同一个目标。 mouseenter 和 mouseover 该用哪个?​ 区别在于冒泡:mouseover / mouseout 会冒泡,mouseenter / mouseleave 不会。 这个差别在分组上最明显。给一个分组绑 mouseover,鼠标在它的子图形之间移动时会 反复触发进出;绑 mouseenter 则只在真正进入和离开整个分组时各触发一次。 做悬停高亮通常想要后者,否则高亮会在子图形边界处闪烁。 可以一次绑定多个事件吗?​ 可以,事件名之间用空格分隔: shape.on('click tap', handler); 这在同时支持桌面与触屏时很常用。详见多重事件与 桌面/移动端事件支持。 与其他方案的取舍​ 把处理器直接绑在每个图形上,写法最直观,适合图形数量有限、行为各不相同的场景。 图形很多且行为一致时(例如一百个可点选的节点),改用事件委托——把一个处理器绑在 图层上,通过 e.target 区分。监听器数量从一百降到一,节点销毁时也不必逐个解绑。 判断依据不是「哪种更好」,而是图形数量与行为是否同质。十个按钮各有各的逻辑, 直接绑更清楚;一百个同类节点,委托更省。 --- ## 取消事件冒泡 来源:https://konva-doc-cn.front-end-js.top/docs/events/cancel-propagation 取消事件冒泡 Konva 的事件会从图形向上冒泡到分组、图层、舞台。需要时可以中断这个过程。 用法​ 要使用Konva阻止冒泡,我们可以设置cancelBubble对象的属性设置为true。 说明:点击圆形可以观察到只有圆形事件绑定被触发, 因为事件传播在触发循环事件时被取消,防止事件对象向上冒泡。 Konva Cancel Event Bubble Propagation Demo
常见问题​ cancelBubble 是属性还是方法?​ 是属性赋值,不是方法调用: shape.on('click', (e) => { e.cancelBubble = true; }); 写成 e.cancelBubble() 会抛错。这个命名沿袭自早期 IE 的事件模型, 和 DOM 标准的 stopPropagation() 不是一回事。 阻止了冒泡,为什么外层的 DOM 事件还是触发了?​ cancelBubble 只管 Konva 内部的节点冒泡链——图形到分组到图层到舞台。 它不影响原生 DOM 事件继续往 外面传。 要拦住原生事件,操作真实事件对象: e.evt.stopPropagation(); 两者经常需要一起用:前者防止舞台的点击处理器把选中态清空, 后者防止页面上的其他监听器响应。 舞台的 content 事件能被 cancelBubble 阻止吗?​ 不能。contentClick、contentMousemove 这类事件由舞台自己派发, 不在图形的冒泡链上,图形里设 cancelBubble 对它们没有任何影响。 如果你同时用了这两套机制,要意识到它们是并行的——点一个图形, 图形的 click 和舞台的 contentClick 都会触发。详见 Stage 事件。 与其他方案的取舍​ 除了阻止冒泡,还有一种常见做法是在外层处理器里判断 e.target: layer.on('click', (e) => { if (e.target !== background) return; // 只处理点在背景上的情况 }); 两者的区别在于控制权在哪一侧。cancelBubble 是内层决定「这个事件到我为止」, 外层无从知晓;判断 e.target 是外层自己筛选,内层什么都不用做。 组件化的场景更适合前者——子组件封装自己的行为,不需要外层配合。 而外层逻辑集中的场景用后者更清楚,因为所有分支都写在一处,不必到处找谁阻止了冒泡。 --- ## 自定义事件监听范围 来源:https://konva-doc-cn.front-end-js.top/docs/events/custom-hit-region 自定义事件监听范围 命中检测走的是一张看不见的画布。自定义它,可以让点击范围与视觉形状解耦。 用法​ 要为使用Konva的形状创建自定义命中空间函数,我们可以设置 drawHitFunc属性。 命中空间函数是Konva的内置函数 将使用在用于命中检测的区域。 使用自定义命中 函数可以有几个好处,如使命中区域更大 使得用户更容易与形状进行交互,从而形成一些部分 的图形可检测区域和其大小不一致,且简化命中绘制功能 以提高渲染性能。 说明:鼠标经过,离开,按下和弹起在星图和 观察到击中区域,是包围该形状的更大尺寸的圆。 Konva Custom Hit Function Demo
常见问题​ hitFunc 里也要调用 fillStrokeShape 吗?​ 要。和 sceneFunc 一样,画完路径后必须调用: hitFunc: (context, shape) => { context.beginPath(); // ...画路径... context.closePath(); context.fillStrokeShape(shape); }, 漏掉这一行,命中图上什么都没画,结果是该图形完全点不中——而视觉上一切正常,很难联想到是命中函数的问题。 命中图里用什么颜色有影响吗?​ 没有。命中图是一张独立的隐藏画布,Konva 会给每个图形分配一个内部标识色来做识别, 你在 hitFunc 里设的颜色会被忽略。 所以不用纠结填充色,专注把形状画对就行。想调试命中区域长什么样, 用 layer.toggleHitCanvas() 把这张画布显示出来。 命中区域可以和视觉形状完全不同吗?​ 可以,而且这正是它的价值。两个典型用法: 一是放大可点区域。一条 1 像素的细线几乎点不中,给它一个宽 10 像素的矩形命中区, 手感立刻变好。线条类图形还有个更简单的办法:设 hitStrokeWidth。 二是简化复杂图形。一个有几百个顶点的路径,命中检测要逐点判断; 用一个矩形近似,精度损失可以接受,开销却小得多。 性能提示​ 命中图和场景图是两张画布,每次重绘都要各画一遍。这意味着图形的绘制成本实际上是双份的。 优化命中图有两个方向。一是简化形状——复杂路径用包围盒或凸多边形近似, 视觉上没有任何变化,命中绘制却快得多。二是干脆不画——纯装饰的图形设 listening(false),它就完全不进命中图。 一个容易忽略的点:hitFunc 里的计算和 sceneFunc 一样每帧都会跑。 把顶点计算提到外面,不要在回调里现算。 --- ## 桌面/移动端事件支持 来源:https://konva-doc-cn.front-end-js.top/docs/events/desktop-and-mobile 桌面/移动端事件支持 同一份交互逻辑要同时服务鼠标和手指,Konva 的做法是把两套事件名写在一起。 用法​ 要向使用Konva为PC端和移动端工作的形状添加事件处理程序,我们可以使用on()方法并传递配对的事件。 例如,为了在桌面和移动应用程序上触发mousedown事件,我们可以使用“mousedown touchstart”事件对来覆盖这两种媒体。 为了在桌面和移动应用程序上触发mouseup事件,我们可以使用“mouseup touchend”事件对。 我们还可以使用“dblclick dbltap”事件对来绑定适用于桌面设备和移动设备的双击事件。 说明:在桌面设备或移动设备上进行鼠标移动,鼠标向上,触摸启动或触摸圈子,以观察相同的功能。 Konva Desktop and Mobile Events Support Demo
常见问题​ click 和 tap 都绑了,触屏上会执行两次吗?​ 会。触屏浏览器在手指抬起后既派发触摸事件,也会补发一次兼容性的鼠标事件, 于是 tap 和 click 先后各触发一次,处理器执行两遍。 正确写法是把它们绑在同一个 on() 调用里:shape.on('click tap', handler)。 Konva 会做去重,同一次交互只执行一次。分两次 on() 绑定则不会去重。 为什么有时候触屏上要等一下才响应?​ 历史原因是移动浏览器的 300 毫秒点击延迟——浏览器要等一下,确认用户不是在双击缩放。 现代浏览器在页面声明了 之后 会取消这个延迟。如果你的页面没有这行 meta,或者允许用户缩放,延迟就还在。 追求极致响应可以改用 touchstart,但要接受「手指按下即触发」的语义—— 用户滑动取消的机会就没有了。 事件对都有哪些?​ 常用的四组:mousedown touchstart、mouseup touchend、mousemove touchmove、 click tap。双击是 dblclick dbltap。 注意没有触摸版的 mouseover / mouseout——手指没有「悬停」这个状态。 依赖悬停的交互在触屏上必须有替代方案,这一点见鼠标指针样式。 国内环境注意事项​ 微信内置浏览器是国内最主要的移动端运行环境,它对手势的处理比标准浏览器更激进。 两个具体影响:左右边缘的横向滑动会被识别为返回上一页,画布上靠近屏幕边缘的 水平拖拽因此经常失效;双指操作可能被判定为页面缩放而不传给画布。 应对办法是给画布留出边距,不要让可拖拽内容贴着屏幕边缘; 需要双指手势时在容器上设置 touch-action: none 把手势控制权拿回来。 另外,部分国产浏览器的「无痕」或「极速」模式会改变事件派发顺序, 如果收到用户反馈说某些机型点不动,值得让他们试试切换浏览器内核。 --- ## 事件委托 来源:https://konva-doc-cn.front-end-js.top/docs/events/event-delegation 事件委托 把一个处理器绑在容器上,靠 e.target 区分实际目标——这就是事件委托。 用法​ 使用Konva获取事件目标,我们可以访问target属性 的Event对象。 这在使用事件委托时特别有用, 其中我们可以将事件处理程序绑定到父节点,并监听 发生在它的孩子上的事件。 说明:点击星形并观察层的事件绑定能正确标识所点击的形状。 Konva Event Delegation Demo
常见问题​ 委托和直接绑定该怎么选?​ 看图形数量和行为是否同质。 十个按钮各有各的逻辑,直接绑在每个上面最清楚。一百个同类节点共用一套行为, 委托能把监听器从一百个降到一个,节点增删时也不用管绑定和解绑。 委托还有一个隐性好处:动态新增的图形自动就能响应,不需要为新节点补绑定。 怎么区分点到的是哪一类图形?​ 用 name 做分类标记,不要用 id——id 语义上应当唯一,而分类天然是多对一的: layer.on('click', (e) => { if (e.target.hasName('node')) { /* ... */ } else if (e.target.hasName('edge')) { /* ... */ } }); 一个节点可以有多个 name(空格分隔),所以还能做「既是 node 又是 selected」这类组合判断。 点击空白处时委托收不到事件?​ 收不到。空白处没有图形,事件的 target 是舞台本身,不会冒泡到图层。 要处理空白点击,得在舞台上单独监听并判断 e.target === stage, 或者铺一个透明矩形当背景。两种做法的差别见 Stage 事件。 性能提示​ 委托的收益主要在两处。 一是监听器数量。每个监听器都是一个闭包,持有对节点和外部变量的引用。 上千个节点各绑一个处理器,这些闭包会实打实占内存,节点销毁时若忘记解绑还会泄漏。 二是增删成本。动态场景下每新增一个图形就要绑一次、删除时解绑一次, 委托把这部分工作完全省掉。 注意委托不会减少命中检测的开销——那取决于有多少图形在监听,与处理器绑在哪无关。 要省命中检测,用 listening(false)。 --- ## 触发事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/fire-events 触发事件 fire() 让你以编程方式触发事件,既可以是内置事件,也可以是自定义的。 用法​ 使用Konva触发事件,我们可以用fire()方法. 这使我们能够以编程方式触发事件,如点击,鼠标经过, 鼠标移动等,也可以是自定义事件。 Konva Fire Event Demo
常见问题​ fire 触发的事件会冒泡吗?​ 默认不会。需要冒泡要传第三个参数: shape.fire('click', null, true); 这个默认值经常让人困惑——用 fire('click') 模拟点击,绑在图层上的 委托处理器收不到,看起来就像 fire 没生效。 处理器里读 e.evt 拿到的是什么?​ 是 fire() 第二个参数传进去的东西,默认为 null。 这意味着依赖原生事件对象的处理器会出问题——读 e.evt.clientX、e.evt.shiftKey 都会抛错。写处理器时如果预期它可能被 fire() 调用,要对 e.evt 做判空。 反过来,测试时可以自己构造:shape.fire('click', { evt: { shiftKey: true } })。 自定义事件名有什么限制?​ 基本没有,任意字符串都行: node.on('selected', () => { /* ... */ }); node.fire('selected'); 这是做组件间解耦通信的轻量办法——发出方不需要知道谁在监听。 建议给自定义事件名加个前缀(如 app:selected),避免与 Konva 未来新增的内置事件撞名。 与其他方案的取舍​ fire() 与直接调用函数的区别,在于谁知道谁。 直接调用要求发出方持有接收方的引用,两者耦合。fire() 只是发出一个信号, 有没有人听、有几个人听,发出方都不关心。 代价是调试变难——看到一个事件被触发,要找出所有监听它的地方并不容易, 尤其在事件名是动态拼接的时候。 经验做法是:同一个模块内部直接调函数;跨模块、或者「可能有多个响应方」时用事件。 不要为了解耦而把所有调用都改成事件,那会让代码流向变得无法追踪。 --- ## 图片事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/image-events 图片事件 图片默认按整个矩形响应事件,透明像素也算在内。想让抠图后的形状精确响应,需要额外一步。 用法​ ##使用Konva的HTML5 Canvas 图片事件 为了仅使用Konva检测图像中非透明像素的事件,我们可以使用drawHitFromCache()方法生成更精确的图像命中区域。 默认情况下,即使图片内部的像素透明,也可以触发事件。 drawHitFromCache()方法还接受一个可选的回调函数,以便在创建图像匹配区域时执行。 注意:drawHitFromCache()方法要求图像托管在与执行它的代码相同域的Web服务器上。 说明:将鼠标放在猴子和狮子上,观察mouseover事件绑定。 请注意,如果您将鼠标悬停在图片的任何部分(包括透明像素)上,则会为猴子触发事件。 因为我们为狮子创建了图像命中区域,所以忽略透明像素,这使得能够进行更精确的事件检测。 Konva Image Events Demo
常见问题​ 为什么点图片的透明区域也会触发事件?​ 因为命中图上画的是图片的矩形边界,Konva 并不知道哪些像素是透明的。 对一张抠好的 PNG 来说,这意味着角落的空白处同样可点。多张图片叠放时问题更明显—— 上层图片的透明区域会挡住下层图片的点击。 drawHitFromCache 怎么用?​ 它读取图片的像素数据,按 Alpha 通道重建命中图: image.cache(); image.drawHitFromCache(); 必须先 cache()——它要从缓存的位图上读像素。可以传一个 0 到 255 的阈值参数, 决定多透明才算「不可点」,默认是 0(只有完全透明才排除)。 图片换了之后要重新执行这两步,否则命中图还是旧形状。 调用之后报 SecurityError?​ 画布被跨域图片污染了。drawHitFromCache 需要读取像素, 而被污染的画布禁止 getImageData。 解决办法和导出图片一样: 图片服务端要返回 Access-Control-Allow-Origin,客户端要在赋 src 前设 crossOrigin = 'anonymous',两个条件缺一不可。 国内环境注意事项​ 这一页的功能对跨域特别敏感,而国内的图片托管普遍不满足条件。 主流对象存储与图床默认不返回 CORS 响应头,需要到控制台单独配置跨域规则。 没配的话,drawHitFromCache 会在调用时抛 SecurityError, 而普通的图片显示却一切正常——问题只在你用到像素读取时才暴露。 更麻烦的是 crossOrigin = 'anonymous' 的副作用:服务端不支持 CORS 时, 加上这行会让图片彻底加载失败,从「能显示但点击不精确」退化成「什么都看不见」。 所以不能盲目加,必须先确认服务端配置。 最稳妥的做法是把需要精确命中的图片与站点同域托管,或者经自己的服务端代理一层。 --- ## 键盘事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/keyboard-events 键盘事件 键盘事件​ 我们没有内建像 keydown 或者 keyup 这些键盘事件 怎么样在 canvas 上监听键盘事件呢?​ 你可以通过两种很简单的方法监听它们: 全局监听 window 对象的事件 给 stage container 设置 tabIndex 属性使它可以被 focus, 然后监听它上面的事件。 说明:点击 stage 使它被 focus, 使用键盘方向键移动图形 Canvas Keyboard events Demo
常见问题​ 为什么给 Konva 节点绑 keydown 没反应?​ Konva 节点不是 DOM 元素,收不到键盘事件。键盘事件只会派发给 当前拥有焦点的 DOM 元素,而 默认不可获得焦点。 所以要给舞台容器加 tabIndex 让它可聚焦,再把监听绑在容器或 window 上: const container = stage.container(); container.tabIndex = 1; container.focus(); container.addEventListener('keydown', handler); 绑在 container 上和绑在 window 上有什么区别?​ 绑在容器上只有画布获得焦点时才响应,用户点了页面别处就不再触发。 这是符合直觉的行为——编辑器里按 Delete 应该删画布上选中的图形, 而不是在用户正在填写表单时误删。 绑在 window 上则全局生效,适合应用型页面(整页就是一个画布)。 但要自己判断焦点是否在输入框里,否则用户打字会触发快捷键。 怎么实现「选中图形后按 Delete 删除」?​ Konva 没有内置选中态,需要自己维护。通常是一个变量记住当前选中的节点, 点击时更新它,键盘事件里读它: let selected = null; shape.on('click', () => { selected = shape; }); container.addEventListener('keydown', (e) => { if (e.key === 'Delete' && selected) { selected.destroy(); selected = null; } }); 如果用了 Transformer, 选中态已经在 tr.nodes() 里,直接读它即可,不必另建变量。 与其他方案的取舍​ 绑在容器上语义最准确:键盘操作作用于「当前正在操作的画布」。 多画布页面、或者画布只是页面的一部分时,这是唯一正确的选择。 代价是要处理焦点——用户必须先点一下画布,快捷键才生效, 这一点最好在界面上有所提示。 绑在 window 上省掉了焦点管理,全屏画布应用里更省事。 但必须自己排除输入场景: const tag = document.activeElement?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA') return; 漏掉这个判断,用户在输入框里按 Delete 会连画布上的图形一起删掉, 这类 bug 在测试中很容易被漏过,因为开发时很少同时用到两者。 --- ## 控制对象是否被监听事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/listen-for-events 控制对象是否被监听事件 listening 控制一个节点是否参与命中检测。关掉它,节点仍然可见,但对指针完全透明。 用法​ 使用Konva设置监听事件,我们可以设置是否监听 当对象被实例化时,配置对象的属性为true或false, 或者我们可以使用setListening()方法设置listen属性。 一旦我们为一个或多个节点设置了侦听属性,我们也需要 使用drawHit()方法重绘每个受影响图层的命中图。 说明:鼠标经过椭圆形以观察事件监听未执行。 单击“Listen”开始监听事件,并观察事件处理程序现在是否被执行。 Konva Listen or Don’t Listen to Events Demo
常见问题​ 关掉之后子节点还能响应吗?​ 不能。在容器(图层或分组)上设 listening(false),整棵子树都不再响应, 子节点自己设了 listening(true) 也没用。 这个特性很实用——把整个装饰图层一次性关掉,比逐个设置省事得多。 改了之后没有立刻生效?​ 命中图是在图层绘制时生成的,改动 listening 之后需要重绘该图层才会反映出来。 Konva 10 默认开启自动重绘,通常会在下一帧自动处理。如果你关掉了 Konva.autoDrawEnabled,需要手动调 layer.draw()——注意不是 shape.draw(), 单个图形的重绘不会重建图层的命中画布。 和 visible(false) 有什么区别?​ visible(false) 是既不画也不响应,节点完全不参与渲染。 listening(false) 是照常画,但不响应。用于那些需要看见、但不该被点到的内容: 背景图、网格线、水印、提示文字。 两者都不会把节点从场景树里移除,遍历时仍然会遇到它们。 性能提示​ 命中检测是逐图形进行的,图形越多开销越大。而一个典型场景里, 真正需要交互的图形往往只占少数——背景、网格、标注文字都不需要响应点击。 把这些装饰性内容关掉监听,收益有两层:命中检测时跳过它们, 以及它们完全不必被画进命中图,绘制成本直接减半。 Konva 10.3.2 还做了一项相关优化:设了 listening: false 的图层会释放掉 与舞台同样大小的命中画布,重新开启时再创建。对整层都是静态内容的场景, 这能省下可观的内存。 注意从 false 改回 true 后,命中图要到下一次图层绘制才会重建。 --- ## 移动端事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/mobile-events 移动端事件 Konva 原生支持触摸事件,但触摸与鼠标在语义上有几处不对等,需要区别对待。 用法​ 要使用Konva将事件处理程序绑定到移动设备上的形状,我们可以使用on()方法. 使用on()方法需要事件类型和回调函数 . Konva 支持 touchstart,touchmove,touchend,tap,dbltap,dragstart,dragmove和dragend移动事件。 注意:此示例仅适用于iOS和Android移动设备,因为它使用触摸事件而不是鼠标事件。 说明:将手指移动到三角形上以查看触摸坐标,然后开始触摸、结束触摸圆形。 Konva Mobile Touch Events Demo
常见问题​ 怎么拿到多个触摸点?​ 通过原生事件对象:e.evt.touches 是当前屏幕上的所有触点, e.evt.changedTouches 是本次事件涉及的触点。 做双指缩放这类手势时,判断 e.evt.touches.length === 2, 再从两个触点算出距离变化。Konva 本身不提供手势识别,这部分要自己写。 触摸点的坐标该怎么取?​ 用 stage.getPointerPosition(),不要直接读 touch.clientX。 前者已经把舞台的位置、缩放、容器滚动都换算进去了,返回的是舞台坐标系里的点; 后者是视口坐标,在舞台有缩放或页面有滚动时完全对不上。 多点触控时用 stage.getPointersPositions() 取全部触点的舞台坐标。 tap 和 touchstart 该用哪个?​ tap 是完整的一次点按——手指按下再抬起,且中间没有明显移动。语义上对应 click。 touchstart 在手指刚接触时就触发,响应更快,但用户滑走取消的机会没有了。 按钮类交互用 tap,误触代价低。需要即时反馈的(例如画笔起笔)用 touchstart。 国内环境注意事项​ 国产浏览器与各类 App 内置 WebView 对触摸事件的处理差异不小,几个常见问题: 手势冲突。微信、UC、QQ 浏览器都有自己的边缘返回、下拉刷新手势, 它们会优先拦截,画布收不到完整的触摸序列。表现为靠近屏幕边缘的拖拽做到一半就断了。 应对办法是在容器上设 touch-action: none,并给画布留出安全边距。 事件顺序。少数内核会在 touchend 之后补发鼠标事件,导致同一次操作触发两遍。 把 tap 和 click 写在同一个 on() 里可以让 Konva 帮你去重。 长按菜单。长按图片会弹出系统的保存菜单,打断交互。 在容器上设 -webkit-touch-callout: none 并对 contextmenu 事件 preventDefault 可以抑制。 --- ## 移动端的滚动和原生事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/mobile-scrolling 移动端的滚动和原生事件 Konva 默认阻止指针的浏览器默认行为,代价是页面在画布上滑不动。这个取舍可以调整。 用法​ 默认情况下,Konva将阻止所有指针与舞台交互的默认行为。 当您尝试在移动设备上拖放形状时,这将防止意外滚动页面。 但在某些情况下,您可能希望保留浏览器事件的默认行为。 在这种情况下,您可以将形状的preventDefault属性设置为false。 说明:如果您使用移动设备,请尝试按每个矩形滚动页面。 绿色 - 应防止默认行为(无页面滚动)。 红色 - 将保持默认行为(滚动工作)。 Konva Mobile Scrolling and Native Events Demo
常见问题​ 为什么在画布上滑动,页面不跟着滚?​ 这是 Konva 的有意设计。移动端如果不阻止默认行为,用户想拖动图形时页面会跟着滚, 两种意图混在一起,拖拽几乎没法用。 所以 Konva 默认对指针交互调用 preventDefault。代价就是画布区域「吞掉」了页面滚动。 怎么让页面能正常滚动?​ 把图形的 preventDefault 设为 false: shape.preventDefault(false); 这样触摸该图形时浏览器的默认行为(滚动)照常发生。 注意这是按节点设置的,不是全局开关。舞台上没有图形的空白区域行为由容器的 CSS touch-action 决定,与这个属性无关。 放行之后拖拽图形时页面也在滚,怎么办?​ 这正是两者的根本矛盾:同一个手势,系统无法判断用户想拖图形还是想滚页面。 常见折中是按图形区分——可拖拽的图形保持 preventDefault: true, 纯展示的图形设为 false。用户在展示区滑动能滚页面,在可操作区拖拽不会误滚。 另一种做法是限制方向:容器设 touch-action: pan-y,允许垂直滚动, 水平手势留给画布。适合横向拖拽的场景。 国内环境注意事项​ 国内的 App 内置浏览器大多加了自己的下拉刷新与边缘返回手势,它们的优先级高于页面逻辑。 下拉刷新最容易出问题:画布位于页面顶部时,向下拖拽图形会先触发刷新。 preventDefault 在部分内核下拦不住它,因为手势识别发生在更底层。 实践中的办法是让画布不要紧贴页面顶部,上方留一块内容区。 边缘返回同理,从屏幕左右边缘开始的横向拖拽会被判为返回手势。 把可拖拽内容与屏幕边缘保持一定距离,或者在容器上设 touch-action: none 争夺控制权。 这些行为在不同版本、不同机型上表现不一致,只能靠实机验证。 如果产品主要跑在微信里,至少要在 iOS 与 Android 各测一台。 --- ## 移动端 tap 与 click 来源:https://konva-doc-cn.front-end-js.top/docs/events/mobile-tap-and-click 移动端 tap 与 click 移动端最经典的坑是"一次点击执行了两遍"。 这一页讲清楚 Konva 在这件事上帮你挡掉了什么、什么情况下会漏过来。 click 和 tap 不会同时触发​ 先说结论,因为这和很多老文章讲的不一样: Konva 把 pointerclick 按输入类型映射成 click(鼠标) 或 tap(触摸),只会得到其中一个。 实测一次鼠标点击与一次手指轻点收到的事件序列: 鼠标: pointerdown → mousedown → pointerup → pointerclick → mouseup → click 触摸: pointerdown → touchstart → pointerup → pointerclick → touchend → tap 末尾一个是 click,另一个是 tap,没有任何一次交互同时产生两者。 所以下面这种写法是安全的,桌面和移动端都只会执行一次: shape.on('click tap', handler); // 空格分隔,注册两个事件名 Konva 默认替你调用了 preventDefault​ 浏览器有一套"兼容性鼠标事件"机制:手指轻点之后, 浏览器会补发一组合成的 mousedown / mouseup / click, 好让只写了鼠标逻辑的老页面在手机上也能用。 这正是"双重触发"的历史来源。 Konva 默认把它挡掉了。源码里在处理按下事件时: // only call preventDefault if the shape is listening for events const isTouch = evt.type.indexOf('touch') >= 0; if (shape.preventDefault() && evt.cancelable && isTouch) { evt.preventDefault(); } 节点的 preventDefault 属性默认是 true, 所以只要图形在监听事件,触摸事件就会被 preventDefault(), 浏览器的合成点击不再产生。 这意味着你把 preventDefault 关掉之后,双重触发会回来: shape.preventDefault(false); // 合成 click 不再被抑制 而关掉它是有正当理由的——preventDefault() 会一并阻止页面滚动, 如果你的画布是页面的一部分、用户需要在画布上滑动来滚动页面, 就必须关掉它。这时候就得小心事件重复,见 移动端滚动。 dragDistance 影响轻触的判定​ Konva.dragDistance 默认 3(像素)。手指按下后移动超过这个距离, Konva 判定为拖拽,tap 就不会触发了。 手指远不如鼠标稳,按下的瞬间偏移三五个像素很正常。 默认值对触摸设备偏小,表现为"明明点了一下,却没有反应"。 Konva.dragDistance = 8; // 全局 shape.dragDistance(8); // 或者只改某个节点 调大的副作用是拖拽启动会有一点"迟钝感", 需要在"点不中"和"拖不动"之间权衡。移动端优先的应用一般设 6–10。 常见问题​ 还有 300ms 点击延迟吗?​ 现代浏览器基本没有了。这个延迟原本是为了等待判断是不是双击缩放, 当页面声明了 (即已适配移动端、不可缩放或由浏览器管理缩放)之后, Chrome 和 Safari 都已移除。 所以第一件事是确认你的页面有这个 meta 标签。 仍可能有延迟的场景是老旧的 WebView—— 一些 App 内置浏览器的内核版本很旧。如果确实遇到, 用 touchstart 代替 tap 能规避,但要自己处理"按下后滑走不应算点击"的逻辑。 为什么我在手机上点不中小按钮?​ 两个原因通常同时存在: 命中区域太小。 手指的接触面积远大于鼠标指针。 细线要设 hitStrokeWidth,小图标要用 hitFunc 把命中区扩大到 至少 44×44 的 CSS 像素(这是 iOS 人机界面指南的建议值), 见自定义命中区域。 dragDistance 太小。 见上面一节。 双击在移动端怎么写?​ dbltap,对应桌面的 dblclick: shape.on('dblclick dbltap', handler); 映射关系和单击一致——pointerdblclick 按输入类型映射成 dblclick 或 dbltap 之一。 长按怎么实现?​ Konva 没有内置长按事件,自己用定时器实现: let timer = null; shape.on('pointerdown', () => { timer = setTimeout(() => { timer = null; onLongPress(); }, 500); }); shape.on('pointerup pointercancel pointerout', () => { clearTimeout(timer); timer = null; }); shape.on('click tap', () => { if (timer === null) return; // 已经当作长按处理了,别再当点击 }); pointercancel 必须清理定时器,否则来电或系统手势打断之后, 长按回调仍然会在半秒后触发。 国内环境注意事项​ 微信内置浏览器是必测环境。 安卓端微信用的是 X5 内核(基于较旧的 Chromium), 在触摸事件、字体回退、preventDefault 的生效范围上和标准 Chrome 都有差异。 iOS 端微信用的是系统 WKWebView,行为更接近 Safari。 桌面浏览器的移动端模拟器复现不出这些差异,必须真机测。 调试手段上,微信里看不到控制台,实用的办法是引入 vConsole: 排查"执行了两次"这类问题时,直接在处理函数里打印 e.type 和 e.evt.pointerType,一眼就能看出是哪个事件重复了。 各家 App 的 WebView 内核版本差异很大。 如果你的画布要嵌进 第三方 App(小程序的 web-view、各类资讯 App 的内置浏览器), 不要假设它们支持指针事件。保险的写法是用 click tap 这类传统事件名, Konva 内部会把指针事件映射过来,两边都能工作。 --- ## 多重事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/multi-event 多重事件 一个处理器同时响应多种事件,只要把事件名写在同一个字符串里。 用法​ 要使用Konva将多个事件绑定到单个处理程序,我们可以使用on()方法,并传递一个包含多个事件类型用空格开的字符串。 说明:鼠标经过,鼠标按下和鼠标弹起在圆上,观察 每个事件都执行绑定到圆上的函数。 Konva Multi-Event Binding Demo
常见问题​ 多个事件共用处理器,怎么知道是哪个触发的?​ 读 e.type: shape.on('mouseover mouseout', (e) => { const entering = e.type === 'mouseover'; }); 如果每种事件的逻辑差别很大,与其在一个处理器里分支,不如分开绑定—— 合并的意义在于逻辑相同,不在于少写几行。 能和命名空间一起用吗?​ 可以,每个事件名各带各的命名空间: node.on('click.menu dblclick.menu', handler); node.off('.menu'); 一次 off('.menu') 就能把这一组全部移除,不用逐个列举事件名。 组件卸载时这个写法很省事,详见通过名称移除事件。 绑定多个事件会影响性能吗?​ 基本不会。监听器本身极轻,真正的开销在命中检测——而那取决于有多少图形在监听, 与每个图形监听了几种事件无关。 换句话说,一个图形绑十种事件,和绑一种,命中检测的成本是一样的。 想省这部分开销要用 listening(false)。 与其他方案的取舍​ 合并绑定适合逻辑完全相同的场景,最典型的是桌面与触屏的事件对: click tap、mousedown touchstart。这类情况下合并不只是省事, Konva 还会做去重,避免触屏上同一次操作执行两遍。 如果只是「恰好都要改同一个状态」,分开绑定往往更清楚—— 后续某一种事件的逻辑要单独调整时,不必先把合并的处理器拆开。 判断标准:这些事件在语义上是不是同一件事的不同表达形式。是,就合并; 只是碰巧做了类似的事,就分开。 --- ## Pointer 指针事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/pointer-events Pointer 指针事件 指针事件(Pointer Events)是浏览器用来统一鼠标、触摸、手写笔的一套抽象。 Konva 把它接了进来,Konva.pointerEventsEnabled 默认就是 true。 shape.on('pointerdown', (e) => { console.log(e.evt.pointerType); // 'mouse' | 'touch' | 'pen' }); 事件列表​ 事件说明 pointerdown按下(鼠标键、手指触屏、笔尖接触) pointermove移动 pointerup抬起 pointercancel系统中断了这次交互(来电、手势接管) pointerclick一次完整的点击 pointerdblclick双击 pointerover / pointerout进入 / 离开(冒泡) pointerenter / pointerleave进入 / 离开(不冒泡) 一次交互会派发两套事件​ 这是本页最重要的一点,也是"点一下执行了两遍"最常见的原因。 Konva 内部按 pointerType 把指针事件同时映射成对应的鼠标或触摸事件, 两套都会派发。下面是在真实浏览器里对同一个矩形监听九个事件、 各操作一次测出来的实际顺序: 鼠标点击一次: pointerdown → mousedown → pointerup → pointerclick → mouseup → click 手指轻点一次: pointerdown → touchstart → pointerup → pointerclick → touchend → tap 两点结论: 同时监听 pointerdown 和 mousedown 会收到两次。 它们不是"二选一"的关系,而是同一个物理动作的两种表述。 pointerup / mouseup、pointerclick / click 同理。 但 click 和 tap 不会同时触发。 注意上面两行—— 鼠标那行末尾是 click,触摸那行末尾是 tap, pointerclick 根据 pointerType 只映射成其中一个。 所以 on('click tap') 这种写法是安全的,见 移动端 tap 与 click。 选一套用,不要混。 新代码用指针事件(一套代码覆盖三种输入), 或者用 click tap 这类传统写法,但不要两者都注册。 常见问题​ 该用 pointer 还是 mouse + touch?​ 新代码用 pointer。一套监听覆盖鼠标、触摸、手写笔, 不需要为移动端和桌面端写两份逻辑,也不用担心两者的事件顺序差异。 需要下探到具体设备的场景只有几种: 读取手写笔压感(e.evt.pressure)、 处理多指手势(需要跟踪多个 pointerId)、 或者要精确控制触摸时的滚动行为。 怎么知道是鼠标还是触摸?​ 原生事件对象上的 pointerType: shape.on('pointerdown', (e) => { if (e.evt.pointerType === 'touch') { // 手指:命中区域要更大,不要依赖 hover } else if (e.evt.pointerType === 'pen') { console.log('压感', e.evt.pressure); // 0 ~ 1 } }); 这比判断 'ontouchstart' in window 可靠得多—— 后者只能判断设备支持触摸,判断不了用户这一次用的是什么。 现在很多笔记本既有触摸屏又有鼠标。 多指触摸怎么区分是哪根手指?​ 用 pointerId。每根手指按下时分配一个 id,直到抬起为止保持不变: const active = new Map(); shape.on('pointerdown', (e) => { active.set(e.evt.pointerId, { x: e.evt.clientX, y: e.evt.clientY }); }); shape.on('pointerup pointercancel', (e) => { active.delete(e.evt.pointerId); }); pointercancel 一定要处理。 系统中断(来电、通知、浏览器接管手势)时 只会派发 pointercancel,不会派发 pointerup。 只监听 pointerup 的话,那根手指会永远留在你的 Map 里, 后续的手势判断全部错乱。 Konva 10.5.0 修过一个相关问题:touchcancel 此前会让 Transformer 保持激活状态,现在取消时会正确派发 pointercancel / touchcancel。 pointerclick 和 click 有区别吗?​ 对鼠标操作来说没有——pointerclick 会被映射成 click,两者都会触发。 所以不要两个都监听,否则一次点击处理两遍。 区别在触摸时:pointerclick 映射成 tap 而不是 click。 所以 pointerclick 是"跨设备的点击",click 是"鼠标的点击"。 性能提示​ pointermove 的触发频率很高——高刷新率屏幕上可以到每秒 120 次以上, 而每一次 Konva 都要做一遍命中检测(读取命中图上的一个像素)。 在 pointermove 里做重活是卡顿最常见的来源。 几条实践: 别在 pointermove 里调 getClientRect() 或 find()。 前者要递归遍历子树,后者要遍历整棵场景图。 把结果在 pointerdown 时算好存起来。 拖拽过程中关掉命中检测。 Konva.hitOnDragEnabled 默认就是 false, 拖拽时不做命中检测——这是个好默认值,不要为了"拖的时候也要 hover 效果" 随手打开它,代价比看起来大。 用 Stage.eventBatchFunc() 批量处理。 Konva 10.5.0 新增, 原本是给框架集成层用的,可以把原生输入处理合并到一次批量更新里: stage.eventBatchFunc((fn) => requestAnimationFrame(fn)); 注意它只作用于原生输入事件,直接的 fire() 调用和程序化的属性变更不走批处理。 调大 Konva.dragDistance。 默认 3px,意味着手指抖动 3px 就进入拖拽。 移动端调到 6–10 能减少误触发的拖拽,也就减少了拖拽期间的持续计算。 --- ## 通过名称移除事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/remove-by-name 通过名称移除事件 命名空间让你精确移除某一组监听器,而不影响同一事件上的其他处理器。 用法​ ##使用Konva通过名称删除事件监听 我们可以使用on()方法添加命名空间的事件类型,以便我们以后 使用off()方法通过相同的命名空间删除事件监听器。 说明:点击圆形可查看两个不同的onclick绑定的弹窗。 使用按钮删除事件监听器再次单击圆形观察新的oncli Konva Remove Event Listener by Name Demo
常见问题​ 命名空间的语法是什么?​ 在事件名后面加点和名字:click.myNamespace。 node.on('click.tooltip', showTooltip); node.on('click.select', selectNode); node.off('click.tooltip'); // 只移除 tooltip 那个 不带事件名只写命名空间也行:node.off('.tooltip') 会移除该命名空间下的所有事件类型。 为什么不能直接 off 掉某个函数?​ Konva 的 off() 按事件类型和命名空间匹配,不支持传入函数引用来精确移除。 这和 DOM 的 removeEventListener(type, fn) 不同。所以如果你在同一个事件上绑了 多个匿名函数,off('click') 会把它们全部移除,没有办法只去掉其中一个。 需要精确控制就用命名空间,这是 Konva 提供的唯一办法。 命名空间能嵌套吗?​ 不能。click.a.b 这样的写法不会建立层级关系,Konva 只认第一个点之后的部分。 需要分组管理时用扁平的命名约定,例如 click.panel-tooltip、click.panel-select, 靠前缀自己组织,而不是指望框架支持层级。 性能提示​ 忘记解绑的监听器是内存泄漏的常见来源。每个处理器都是一个闭包, 持有对节点、对组件实例、对外部数据的引用——只要监听器还在,这些都回收不掉。 在 React、Vue 这类框架里尤其要注意:组件重新渲染时若重复绑定而不解绑, 监听器会一次次累积,同一个点击执行十几遍。 可靠的做法是给组件内绑定的所有事件加统一命名空间,卸载时一行清理: // 挂载 node.on('click.myComp dragmove.myComp', handler); // 卸载 node.off('.myComp'); 如果节点本身也要销毁,destroy() 会自动清理它的全部监听器,就不必单独 off 了。 --- ## 移除事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/remove-event 移除事件 off() 按事件类型移除监听器。它的粒度比 DOM 的 removeEventListener 粗,用法要相应调整。 用法​ 要使用Konva删除事件监听,我们可以使用的off()方法 需要事件类型(例如点击或鼠标)和形状对象。 说明:点击圈子可查看onclick触发的提醒事件绑定。 通过单击按钮并再次删除事件侦听器 单击该圆圈以观察事件绑定已被删除。 Konva Remove Event Listener Demo
常见问题​ off 能只移除其中一个处理器吗?​ 不能——除非用命名空间。off('click') 会移除该节点上所有的 click 处理器, 不管是谁绑的。 在多人协作或组件化的代码里这很危险:你以为只清理了自己的监听, 实际上把别的模块绑的也一并干掉了。养成给自己的监听加 命名空间的习惯,off('click.myModule') 才是安全的。 不带参数调用 off 会怎样?​ node.off() 移除该节点上的全部监听器,所有事件类型、所有命名空间。 这个写法在「彻底重置一个节点」时有用,但同样要小心误伤。 如果节点马上就要销毁,直接 destroy() 更合适——它会连同子节点的监听一起清理。 destroy 之后还需要手动 off 吗?​ 不需要。destroy() 会解除该节点及其所有子节点的监听器, 并把节点从场景树中移除。 要注意的是 remove() 不会——它只是把节点从父容器里摘出来,监听器都还在, 节点本身也还被你的变量引用着。打算复用就用 remove(), 不再需要就用 destroy(),两者的区别见防止内存溢出。 与其他方案的取舍​ 三种清理监听的手段,适用场景各不相同: off('事件名'):粒度最粗,会误伤他人绑定。只在你确定这个节点上只有自己的监听时用。 off('.命名空间'):精确到模块,是组件化代码里的标准做法。代价是绑定时要记得加命名空间。 destroy():节点连同监听一起销毁。节点不再需要时最省心,但之后不能复用该节点。 一个实用原则:只要这段代码可能被复用或被别人的代码包围,就用命名空间。 省下的几个字符远不如后期排查「监听器莫名消失」的成本高。 --- ## Stage 事件 来源:https://konva-doc-cn.front-end-js.top/docs/events/stage-events Stage 事件 点击画布空白处不会触发任何图形事件。要响应它,有两种思路。 用法​ 所有事件都从图形开始。 因此,如果您点击画布上的空白空间,点击事件将不会触发图层,甚至没有触发阶段对象。 但是如果你真的需要监控在Konva.Stage上空的空间点击(或任何其他类似的事件),你可以: 创建与Stage相同大小的透明矩形,并添加到形状的底部 或特意监听content事件。 支持的content事件: contentMouseover,contentMousemove,contentMouseout, contentMousedown,contentMouseup, contentClick,contentDblclick, contentTouchstart,contentTouchmove,contentTouchend, contentTap,contentDblTap. 说明:点击任何地方,然后查看控制台. Konva Stage Events Demo
常见问题​ 为什么点空白处什么都没发生?​ Konva 的事件从图形开始冒泡。空白处没有图形,命中检测返回空, 自然不会有事件沿着链条往上传。 这不是 bug,而是命中检测机制的直接结果——舞台不是一个「图形」, 它没有自己的可命中区域。 铺一个透明矩形是不是最简单的办法?​ 通常是。做一个和舞台等大的 Konva.Rect,放在最底层, 给它绑事件即可: const bg = new Konva.Rect({ width: stage.width(), height: stage.height() }); layer.add(bg); bg.moveToBottom(); 注意不要设 fill——不填充的图形不参与命中检测,那样又点不中了。 要透明又可点,设 fill: 'rgba(0,0,0,0)' 或者给它一个 hitFunc。 另外舞台尺寸变化时要同步更新这个矩形的大小。 content 事件和图形事件有什么区别?​ contentClick、contentMousemove 这类事件由舞台直接派发, 不参与图形的冒泡链,因此也拿不到有意义的 e.target。 它们在整个舞台范围内都会触发——点在图形上也会触发 contentClick, 和图形自己的 click 并行发生。如果两边都绑了处理器,一次点击会走两套逻辑。 所以它适合「不关心点到了什么,只要知道舞台被点了」的场景, 比如记录坐标、关闭浮层。 与其他方案的取舍​ 透明矩形的优点是它就是个普通图形——参与冒泡、有 e.target、 可以用事件委托统一处理。缺点是要自己维护尺寸, 而且它会出现在 find() 的结果里,遍历时得记得排除。 content 事件不需要额外节点,也不用管尺寸。但它与图形事件是两套并行的机制, 混用时容易出现「一次点击触发两处逻辑」,需要自己协调。 判断依据:需要区分点到了空白还是图形,用透明矩形加 e.target 判断最直接; 只是想在舞台任意位置捕获坐标,用 content 事件更轻。 --- ## 技巧 来源:https://konva-doc-cn.front-end-js.top/docs/performance/all-performance-tips 技巧 Konva 的性能优化手段不少,但它们各自解决的是不同的瓶颈。用错方向,努力会白费。 优化清单​ batchDraw 方法 图层管理 形状缓存 动画优化 形状重绘 如果你的形状只有位置变换(x 和 y, 没有scale, rotation),设置transformsEnabled = 'position'。 如果你不需要在图层上添加事件, 设置 layer.hitGraphEnabled(false) , 或者使用Konva.FastLayer。 参见 演示。 对于移动应用程序将视口设置为:。 如果您发现在视网膜屏幕的设备上的性能不佳, 设置 Konva.pixelRatio = 1 .确保结果的质量能满足您的需求。 当拖动节点时,您可以在单独的一个图层上移动它, 拖动完成后将其移回到原来的图层。 优化描边的绘制。 如果形状同时设置了填充,描边和透明度,您可以设置 shape.perfectDrawEnabled(false) 。详细信息参见 禁用 Perfect Drawing。 尽可能设置 shape.listening(false) 。 更多信息参见 Listening false。 防止内存泄漏。 常见问题​ 该从哪里开始优化?​ 先测量,不要凭感觉。打开浏览器的 Performance 面板录一段, 看时间花在哪——是脚本执行、还是 Canvas 绘制、还是布局。 经验上 Konva 应用的瓶颈按出现频率排序是:节点数量过多 > 每帧重绘了不需要重绘的图层 > 单个节点的绘制成本(阴影、滤镜、复杂路径)。 多数人一上来就去优化第三项,而真正的问题往往在前两项。 缓存是不是总能提速?​ 不是。cache() 把节点渲染成位图,之后每帧只贴图——前提是这张图不用重做。 如果节点每帧都在变(位置、颜色、尺寸),缓存就要每帧重建, 那比直接绘制还慢,因为多了一次离屏渲染和一次贴图。 判断标准很简单:这个节点多久变一次?几秒才变一次,缓存;每帧都变,别缓存。 注意位移和整体缩放不算「变」——移动一个缓存节点不需要重建缓存。 节点数量多少算多?​ 没有绝对数字,取决于每个节点的复杂度和有多少在监听事件。 一个粗略的参考:几百个简单图形通常没问题;上千个就要开始考虑 关掉不必要的监听、把静态部分合并缓存;上万个则需要改变思路—— 比如把静态内容预渲染成一张图片,只保留交互部分作为真实节点。 与其他方案的取舍​ 三种主要手段各有其适用信号,不要混为一谈: 分层(图层管理)解决的是「重绘范围过大」。 信号是:画面上只有一小部分在动,却每帧重绘全部内容。把动静分开即可。 缓存(图形缓存)解决的是「单个节点画得太慢」。 信号是:节点数量不多,但每个都带阴影、滤镜或复杂路径。 减少节点解决的是「节点太多」。信号是:节点数以千计,且多数是静态装饰。 办法是合并成一个自定义图形,或者预渲染成图片。 还有一类是关掉不需要的功能:禁用事件监听、 关闭 Perfect Drawing、 描边绘制优化。这些成本低、风险小, 可以先做,但收益通常不如前三项显著。 --- ## 防止内存溢出 来源:https://konva-doc-cn.front-end-js.top/docs/performance/avoid-memory-leaks 防止内存溢出 删除形状​ Konva中有两个非常接近的方法 remove() 和 destroy() 。 如果你想彻底地删除节点,你需要使用 destroy() 方法。 如果你将来需要重用节点,你应该使用 remove() 方法,然后在需要的时候将其再次添加到任何容器。 不要在 destroy()操作之后重用节点, 因为 destroy() 会从KonvaJS引擎中删除对节点的所有引用。 Tween动画​ 当你使用 Konva.Tween 实例来创建动画时,你必须在使用后销毁它。 var tween = new Konva.Tween({ node : circle, x : 0, duration : 0.5, onFinish : function() { // remove all references from Konva tween.destroy(); } }); tween.play(); 或者如果你不需要重用tween实例, 你可以使用更为简单的 to() 方法来创建动画: // tween will be automatically started and destroyed on finish circle.to({ x : 0, duration : 0.5 }); 监听节点销毁​ Konva 10.4.0 起,node.destroy() 开始时会触发 destroy 事件。这让你可以在 节点被销毁的那一刻清理挂在它身上的外部资源,例如定时器、订阅或缓存: shape.on('destroy', () => { clearInterval(timerId); unsubscribe(); }); 框架内部也用到了这个事件:Transformer 会把被销毁的节点从 nodes() 中移除, Tween 会停止该节点上的补间。在 10.4.0 之前,销毁一个正被 Transformer 选中 或正在补间的节点会留下悬空引用。 常见问题​ remove 和 destroy 到底该用哪个?​ remove() 只是把节点从父容器里摘出来,节点对象、它的事件监听、它的缓存都还在, 你手里的变量仍然引用着它。之后可以再 add 回任何容器。 destroy() 是彻底销毁:从父容器移除、解绑全部监听器、清理缓存、 并从 Konva 的内部注册表里删掉。销毁后的节点不能再使用。 判断很简单:以后还要用就 remove(),不要了就 destroy()。 用错的后果是:该用 destroy 时用了 remove,节点游离在内存里回收不掉; 该用 remove 时用了 destroy,之后 add 回去会得到一个坏掉的节点。 为什么 destroy 之后内存还是没降?​ 最常见的原因是你自己还持有引用。数组里存着、闭包里捕获着、 Map 的 key 是它——只要还有一条引用链,垃圾回收器就不会回收。 destroy() 解除的是 Konva 内部的引用,你自己代码里的引用得自己清。 第二个常见原因是缓存。被缓存的节点持有一张位图,destroy() 会清理它, 但如果你只是 remove() 了节点,那张位图会一直占着内存。 长时间运行的应用要注意什么?​ 三处最容易累积: 未停止的动画。Konva.Animation 不会因为节点销毁而自动停止, 它会继续每帧调用回调,而回调里往往还引用着已销毁的节点。 销毁节点前先 anim.stop()。 未解绑的监听器。尤其是绑在 stage 或 window 上的——节点销毁了, 监听器还在,闭包里捕获的节点也就回收不掉。用 命名空间批量清理。 未清理的缓存。定期检查是否有节点缓存了但再也用不到。 性能提示​ 内存问题不像性能问题那样立刻可见,需要主动测量。 浏览器 DevTools 的 Memory 面板可以拍堆快照。做法是:执行一轮 「创建一批节点 → 销毁它们」,然后强制 GC、拍快照,再重复一轮、再拍。 比较两次快照,如果 Konva 相关对象的数量持续增长,就是泄漏。 几个降低风险的习惯: 销毁走统一出口。写一个 destroyNode(node) 函数,里面依次停动画、 解监听、清缓存、destroy(),所有地方都调它,而不是各处散落 node.destroy()。 容器销毁优于逐个销毁。group.destroy() 会递归销毁全部子节点, 比自己遍历更不容易漏。 警惕缓存的内存量级。一个 500×500 的节点在 2 倍屏上缓存要占约 4MB, 这比多数人的直觉大得多。见图形缓存。 --- ## 批量绘制 来源:https://konva-doc-cn.front-end-js.top/docs/performance/batch-draw 批量绘制 batchDraw() 把多次重绘请求合并到下一帧。在 Konva 10 里,多数场景已经不需要手动调用它。 用法​ 在某些情况下,我们可能希望既不造成过多的重绘, 并且尽可能快地更新Konva形状.例如,如果我们要在mousemove事件中更新舞台上某个元素的状态, 我们并不愿意使用draw()方法来重绘图层, 因为mousemove事件可以在一秒内被触发成百上千次, 从而导致动画的帧率达到每秒一百多帧, 这样常常会使动画出现'跳跃', 毕竟浏览器处理重绘的能力是有限的. 对于这种情况,最好使用batchDraw()方法, 它会自动将重绘操作交给Konva动画引擎来处理。无论你调用batchDraw()多少次,Konva引擎都会根据浏览器每个时间点每秒所能处理的最大帧数自动限制图层每秒重绘的次数. 说明:将鼠标移到舞台上来快速旋转矩形 Konva Batch Draw Demo
常见问题​ Konva 10 还需要手动调 batchDraw 吗?​ 通常不需要。Konva.autoDrawEnabled 默认为 true,改动节点属性后 Konva 会 自动安排下一帧重绘,并且本身就做了合并。 手动调用仍然有效,只是多数情况下是冗余的。真正需要它的场景是: 你关掉了自动重绘(为了完全掌控绘制时机),或者在一个循环里改了大量节点后 想显式触发一次。 batchDraw 和 draw 有什么区别?​ draw() 是立即同步重绘,调用时就画。在一个循环里调一百次,就真的画一百次。 batchDraw() 是请求在下一帧重绘,无论调多少次,下一帧只画一次。 所以高频事件里(mousemove、dragmove、动画回调)永远不要用 draw()。 鼠标移动一秒能触发上百次,那意味着一秒画上百帧,远超浏览器的刷新能力, 表现出来就是掉帧和卡顿。 关掉 autoDrawEnabled 有什么好处?​ 拿回绘制时机的控制权。在需要精确协调多个图层绘制顺序、 或者要把绘制与外部的动画循环对齐时有用。 代价是所有重绘都要自己负责,漏掉一处就是「改了属性但画面没变」, 而且这类 bug 很难定位——代码逻辑完全正确,只是少了一次 draw()。 除非确有需要,保持默认的自动重绘。 与其他方案的取舍​ draw() 与 batchDraw() 的选择,本质是「现在就要结果」还是「下一帧再说」。 需要立刻拿到绘制结果的场景确实存在——最典型的是 toDataURL() 之前, 要确保画面是最新的。那种情况下用 draw()。 其余所有场景,尤其是任何由用户输入驱动的更新,都应该用 batchDraw(), 或者干脆依赖自动重绘。 还有一种做法是自己用 requestAnimationFrame 做节流,把一帧内的多次更新 收敛成一次。这与 batchDraw 做的是同一件事,除非你还要在同一帧里协调 其他非 Konva 的更新,否则没有必要重复实现。 --- ## 关闭 Perfect Drawing 来源:https://konva-doc-cn.front-end-js.top/docs/performance/disable-perfect-draw 关闭 Perfect Drawing 「完美绘制」为了消除描边与填充交叠处的色差而引入一层缓冲画布。不需要它时可以关掉。 用法​ 在某些情况下,直接在canvas画布上绘图出现意外的结果。 例如,我们绘制一个形状, 并且同时设置了填充, 描边和透明度. 由于描边在填充的上面绘制,因此描边和填充交叉的部分, 会出现一条颜色加深的细线. 这可能这不是你所期望的, 所以KonvaJS使用缓冲区画布来修复了这种行为。 在这种情况下KonvaJS是这样做的: 在缓冲区画布上绘制形状 在没有设置透明度的情况下, 绘制描边和填充 将透明度设置到图层上 将缓冲区画布上绘制好的形状绘制到当前画布上 但是使用缓冲区画布可能会降低性能。你可以使用下面的代码来禁用它: shape.perfectDrawEnabled(false); 看看有什么区别: Konva Disable Perfect Drawing Demo
常见问题​ 它到底在解决什么问题?​ 同时设置 fill、stroke 和 opacity 时,描边画在填充之上, 两者交叠的半个描边宽度内颜色会叠加,形成一条比预期更深的边。 Konva 的做法是先把图形画到一张离屏画布上,合成好之后再整体以指定透明度贴回来, 交叠处因此不会叠加。代价是多了一次离屏绘制和一次合成。 什么情况下可以安全关掉?​ 满足以下任一条件时,这层处理本就没有作用,关掉是净收益: 图形只有填充没有描边,或只有描边没有填充 图形不透明(opacity 为 1)——不透明时描边覆盖填充,本来就不会叠加 换句话说,只有「半透明 + 同时有填充和描边」这一种组合才需要它。 关掉之后会看到什么变化?​ 如果图形确实属于上面说的需要它的那类,关掉后会在填充与描边的交界处 看到一圈略深的轮廓。程度取决于透明度——opacity: 0.5 时最明显。 判断办法很直接:关掉,看一眼。看不出区别就说明本来就不需要。 性能提示​ 收益大小取决于图形数量与尺寸。每个启用完美绘制的图形,每帧都要多一次 离屏绘制加一次合成——离屏画布的尺寸与图形包围盒相关,大图形代价更高。 几个实用做法: 批量关闭。如果整个应用里没有半透明描边图形,可以在创建时统一设 perfectDrawEnabled: false,不必逐个判断。 优先照顾动态内容。静态图形通常已经被缓存,完美绘制只在建缓存时跑一次, 关不关差别不大。真正值得关的是每帧都在重绘的那些。 和描边优化一起做。描边绘制优化里的 strokeHitEnabled(false) 省的是命中图的开销,与这里省的绘制开销是两笔账, 两个一起关收益叠加。 --- ## 图层管理 来源:https://konva-doc-cn.front-end-js.top/docs/performance/layer-management 图层管理 每个图层是一张独立的 canvas。合理分层,可以让「只有一小块在动」时不必重绘整个画面。 用法​ 当创建Konva应用程序时,在性能方面最需要的考虑就是层的管理. 让Konva从其他画布库中脱颖而出的一大特点就是, 它使我们能够创建单独的层,每个层都有自己对应的画布元素。这意味着我们可以只动画,转换或更新舞台上的一部分元素,同时不需要重新绘制舞台上其他的元素。如果我们检查一个Konva舞台的DOM结构,我们将看到实际上每一个层都有一个对应的画布元素. 本教程有两个图层,一个是动画图层,另一个是包含文本的静态图层。 由于在动画中不需要重绘文本, 所以可以把它放置在一个单独的图层中. 下面的示例中有两个层, 一个是动画层, 一个是包含文本的静态层. 由于文本上没有动画, 不需要被连续地重绘, 因此被放在一个单独的层里. Konva Layer Management Demo
常见问题​ 图层是不是越多越好?​ 不是。每个 Konva.Layer 都会创建一张与舞台等大的 canvas——实际上是两张,场景图和命中图。 一个 1920×1080 的舞台,单张 canvas 的显存占用就是几 MB,高 DPI 屏幕下还要乘以 pixelRatio 的平方。图层开到十几个,内存会很可观,而且浏览器合成这些图层本身 也有成本。 实践中个位数的图层足够表达绝大多数场景。 怎么划分图层?​ 按更新频率分,不是按业务逻辑分。 典型的三层结构:静态背景(网格、底图)一层,几乎不重绘;主体内容一层; 交互反馈(拖拽中的元素、选择框、辅助线)一层,每帧都在变。 拖拽时把被拖的节点临时移到最上层,松手后移回去——这样拖拽过程中只有 那一层在重绘,主体内容原封不动。 图层顺序怎么调整?​ 图层之间用 moveToTop()、zIndex() 这些方法,和普通节点一样。 要注意的是跨图层的层级由图层顺序决定,节点自己的 zIndex 管不着。 一个节点在图层 A 里 moveToTop(),它也只是 A 里面最上面的, 仍然会被图层 B 的内容盖住。这是分层最容易造成困惑的地方。 性能提示​ 分层的收益来自避免无谓重绘,所以它只在「画面大部分静止」时才有意义。 如果每帧所有内容都在变,分层不但不省,还多了合成开销。 几个实操要点: 静态层要真的静态。背景层里混进一个会动的元素,整层就白分了。 检查办法是给每层的 draw 打点计数,看静态层是不是真的不重绘。 拖拽时临时提层。node.moveTo(dragLayer) 把节点搬到专用图层, dragend 时搬回。这是 Konva 应用里收益最稳定的一项优化。 不监听的图层设 listening: false。Konva 10.3.2 起这类图层会释放掉 与舞台等大的命中画布,内存收益直接可见。 --- ## 禁用事件监听 来源:https://konva-doc-cn.front-end-js.top/docs/performance/listening-false 禁用事件监听 关掉监听的节点不进命中图。省下的不只是命中检测,还有绘制命中图本身的开销。 用法​ 您可以通过给形状设置listening(false)来将其从命中检测区域中移除, 这样可以提高性能. 在某些情况下, 这个方法会非常有用, 并且不会破坏整个应用的逻辑. 例如,我们有一个包含矩形和文本的按钮(组)。 我们需要监听按钮点击。 在这种情况下,我们可以从命中捡测区域中移除文本,只监听在矩形上面的点击操作。 Konva Listening False Demo
常见问题​ 关掉之后节点还画得出来吗?​ 照常绘制。listening(false) 只影响命中检测,不影响视觉。 这是它与 visible(false) 的根本区别:后者连画都不画。 需要「看得见但点不到」时用前者,需要「彻底不存在」时用后者。 在容器上设置会影响子节点吗?​ 会,而且是强制的。在图层或分组上设 listening(false), 整棵子树都不响应,子节点自己设 listening(true) 也无效。 这个特性用来批量关闭很方便——整个装饰图层一行搞定。 但也意味着不能在关掉的容器里「开一个口子」,需要例外的节点得放到别的容器里。 改了之后没生效?​ 命中图在图层绘制时生成,改动要等下一次图层重绘才反映出来。 Konva 10 默认自动重绘,通常下一帧就好了。如果你关掉了 Konva.autoDrawEnabled, 需要手动 layer.draw()——注意不能只调 shape.draw(), 那不会重建图层的命中画布。 性能提示​ 这是收益与风险比最好的一项优化:改一个属性,不影响任何视觉效果。 收益有两层。命中检测时跳过——这部分随图形数量线性增长。 不必画进命中图——每个参与监听的图形,每次重绘实际上要画两遍 (场景图一遍、命中图一遍),关掉监听等于省掉后者。 典型可以关掉的内容:背景图、网格线、坐标轴、水印、说明文字、装饰性图标。 一个界面里这类元素往往占了大半。 Konva 10.3.2 补充了一项:整层都设 listening: false 时,该图层会释放 与舞台同尺寸的命中画布,重新开启时再创建。静态背景层因此能省下实打实的内存。 --- ## 动画优化 来源:https://konva-doc-cn.front-end-js.top/docs/performance/optimize-animation 动画优化 动画函数每帧都会执行。让它在无事可做时早早返回,可以省掉整层的重绘。 用法​ 如果你动画中的某些帧没有发生更新(没有节点的状态发生更改),则可以在动画函数直接return false。 这样的话,Konva将不会更新图层。 Konva Optimize Animation Demo
常见问题​ return false 到底省了什么?​ 省的是图层重绘,不是回调本身。 动画函数无论如何每帧都会被调用,return false 告诉 Konva「这一帧我什么都没改, 不用重画」。对一个复杂图层来说,省下的这次重绘可能是整帧开销的大头。 所以回调本身的逻辑也要轻——它是每帧必跑的,在里面做重活,return false 救不了。 怎么判断这一帧没有变化?​ 需要你自己维护状态。常见做法是把上一帧的关键值记下来,本帧算完之后比较: let lastX = null; const anim = new Konva.Animation((frame) => { const x = compute(frame.time); if (x === lastX) return false; lastX = x; node.x(x); }, layer); 如果动画本来就是持续变化的(匀速旋转、粒子运动),这个优化没有意义—— 每帧都在变,本来就该每帧重绘。 动画不传图层参数会怎样?​ Konva 不知道该重绘哪一层,于是什么都不重绘。你改了节点属性,画面却不动。 new Konva.Animation(fn, layer); // 传入要重绘的图层 可以传图层数组,动画涉及多层时都列上。但反过来,不要把不变的图层也传进去—— 那等于每帧强制重绘它们,分层的意义就没了。 性能提示​ 动画回调是全应用里执行频率最高的代码,任何浪费都会被乘以 60。 把常量提到外面。三角函数表、渐变对象、配置数组,都不该在回调里创建。 不要在回调里创建对象。每帧新建的对象会给垃圾回收器持续施压, 表现为周期性的卡顿。复用同一个对象,改它的字段。 用 frame.timeDiff 而非固定增量。固定增量意味着动画速度随帧率变化, 在低端设备上会变慢。用时间差算位移,各种帧率下速度一致。 页面不可见时停掉。监听 visibilitychange,切到后台时 anim.stop()。 浏览器通常会降低后台标签页的帧率,但不会完全停止,主动停掉更省电。 --- ## 描边绘制优化 来源:https://konva-doc-cn.front-end-js.top/docs/performance/optimize-strokes 描边绘制优化 描边会同时进入场景图和命中图。多数时候,命中图上那一份是可以省掉的。 用法​ 从命中检测中删除描边​ 如果您的图形同时拥有填充和非常小的描边, 您可以通过设置shape.strokeHitEnabled(false)将描边从命中检测区域中删除. 如果描边对于命中检测至关重要,请不要使用此属性。 禁用描边阴影​ 如果您真的不需要描边的阴影, 你可以设置shape.shadowForStrokeEnabled(false)。 请记住,如果您创建Konva.Line时没有设置填充属性,阴影也将会被禁用。 Konva Optimizing Strokes Demo
常见问题​ strokeHitEnabled 关掉之后有什么变化?​ 命中图上不再绘制描边,只有填充区域可点。视觉上毫无变化。 对一个有填充的图形来说,描边只是它边缘那几个像素,把它排除在命中之外 用户几乎察觉不到。但命中图少画一遍描边,尤其是复杂路径的描边, 省下的时间很实在。 什么时候不能关?​ 描边本身就是可点区域的时候。最典型的是没有填充的线条—— 一条 Konva.Line 全部的存在就是那条描边,关掉命中就完全点不中了。 另一类是描边很粗、用户预期能点到边框的场景,比如边框本身是一个可拖拽的手柄。 判断依据:去掉描边后,剩下的填充区域还够不够用户点。 线条太细点不中怎么办?​ 不是关掉描边命中,而是放大它——用 hitStrokeWidth: line.hitStrokeWidth(20); 视觉上仍是 1 像素的细线,命中区域却有 20 像素宽,手感立刻变好。 这在流程图连线、图表坐标轴这类场景里几乎是必备设置。 更精细的控制见自定义事件监听范围。 与其他方案的取舍​ 三种手段针对的是不同问题,不要混淆: strokeHitEnabled(false) 省的是命中图上绘制描边的开销,适合「有填充、 描边只是装饰」的图形。改动最小,风险最低。 hitStrokeWidth 解决的是反过来的问题——描边太细点不中。它不省开销, 是为了手感。 自定义 hitFunc 最灵活也最麻烦,适合形状复杂、需要大幅简化命中区域的场景。 比如一个上百顶点的路径,命中用一个矩形近似就够了。 还有一个更彻底的选项:这个图形压根不需要响应事件,那就 listening(false),连命中图都不进。 --- ## 图形缓存 来源:https://konva-doc-cn.front-end-js.top/docs/performance/shape-caching 图形缓存 cache() 把节点预渲染成位图,之后每帧只贴图。用对了收益巨大,用错了适得其反。 用法​ 在绘制复杂的Konva图形时, 一种能显著提高绘制性能的方法是将它们缓存为图像。这可以通过使用cache()方法将节点转换为图像对象来实现。 下面这个特定的教程通过绘制了10个缓存的星星图片,而不是直接绘制10个独立的Konva星星形状, 使绘图性能得到了4倍的提升 缓存方法可以应用于任何节点,包括舞台,图层,组和形状。 注意:cache()方法要求图片必须托管在和执行绘制的代码文件相同域名的服务器上. 同样的,cache()函数不能自动检测节点的尺寸大小。所以你需要小心拥有阴影和描边的组和形状。 如果你遇到了意外的结果, 可以通过设置x, y, width 和 height来给cache()函数传递一个弹性的属性值. Konva Shape Caching Demo
常见问题​ 什么样的节点值得缓存?​ 判断标准是绘制成本高、变化频率低。 绘制成本高:带阴影、带滤镜、路径顶点多、或者是包含很多子节点的分组。 变化频率低:几秒才变一次,甚至完全静止。 注意位移和整体缩放不算「变」——移动一个缓存节点只是把位图贴到新位置, 不需要重建缓存。所以「会动但形状不变」的节点非常适合缓存,这是最大的收益来源。 缓存之后图形变糊了?​ 缓存是按当前尺寸渲染成位图的,之后放大就是在放大位图。 两个办法:一是建缓存时提高分辨率,node.cache({ pixelRatio: 2 }); 二是缩放后重新缓存,node.clearCache(); node.cache();。 前者内存翻四倍,后者有重建开销。做可缩放的画布时,通常的策略是 缩放过程中容忍模糊,缩放结束后重建一次缓存。 改了属性之后画面没更新?​ 缓存不会自动失效。改了被缓存节点的填充色、子节点内容之后, 画面上还是旧位图。 node.clearCache(); node.cache(); 这是缓存最容易出问题的地方——bug 表现为「代码改了属性但界面没反应」, 很容易往数据流的方向去查,而根源在渲染层。 如果一个节点需要频繁 clearCache + cache,那说明它本来就不该被缓存。 性能提示​ 缓存的代价是内存和重建开销,两者都容易被低估。 内存方面:缓存位图的大小是节点包围盒面积乘以 pixelRatio 的平方,再乘 4 字节。 一个 500×500 的节点在 2 倍屏上就是 4MB。缓存几十个这样的节点,内存会很快吃紧。 不再需要时记得 clearCache()——Konva 10.4.0 修复了容器 clearCache() 时 泄漏子节点缓存画布的问题,但主动清理仍然是好习惯。 重建开销方面:每次 cache() 都是一次完整的离屏渲染。在动画循环里重建缓存, 比不缓存还慢。 Konva 10.4.0 还降低了 cache() 及各类导出方法的内存占用, 但基本的取舍没有变化:缓存换的是「用内存和一次性开销,省掉每帧的重复绘制」。 --- ## 形状重绘 来源:https://konva-doc-cn.front-end-js.top/docs/performance/shape-redraw 形状重绘 shape.draw() 只重绘单个节点。它比 layer.draw() 快,但适用面窄得多。 用法​ 通常当你需要更新画布时你应该调用layer.draw() 方法。 但在个别情况下,我们也可以通过调用shape.draw() 来只更新某个Konva.Node而不用更新整个层。 但请千万记住,在这种情况下,形状将被绘制在当前的画布上。 因此,如果节点位于在其他节点下面或者设置了透明度,就不能使用这种方法了。 说明:鼠标悬停到的方框会变成高亮状态。 Konva Shape Redraw Demo
常见问题​ 单个节点重绘为什么会画错?​ 因为它是画在当前画布之上的,不会先清除原来的内容。 这带来两个问题:如果节点位置变了,旧位置的图像还留在画布上, 成了一道残影;如果节点是半透明的,新画的会和旧的叠加,颜色越来越深。 所以它只在「节点原地不动、且不透明」时才可能正确。 被其他节点遮挡时会怎样?​ 重绘的节点会盖到遮挡它的节点上面去。 因为 shape.draw() 不管层级,就是在当前画布上画一遍。 原本在它上面的兄弟节点,这一帧就被盖住了,下一次图层重绘才恢复。 画面上表现为闪烁——节点时而在上时而在下。 那它到底什么时候有用?​ 场景相当有限:节点不透明、位置不变、没有任何东西遮挡它、且尺寸不增大。 一个实际例子是仪表盘上原地变色的指示灯——固定位置、不透明、最上层。 除此之外,老老实实用 layer.draw() 或依赖自动重绘。 与其他方案的取舍​ 想省重绘开销,shape.draw() 几乎总不是正确答案,它的约束太多而收益有限。 分层是更可靠的手段:把频繁变化的节点单独放一层, layer.draw() 那一层的成本本来就很低。见图层管理。 batchDraw() 解决的是「重绘太频繁」而不是「重绘范围太大」, 它把一帧内的多次请求合并成一次。见批量绘制。 真要精确控制重绘区域,Canvas 的做法是脏矩形——只清除并重绘变化的那一小块。 Konva 没有内置这个机制,自己实现的复杂度远高于分层,除非有极端性能要求, 否则不值得。 --- ## Arc 弧形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/arc Arc 弧形 弧形是圆环的一段,由内外半径和张角定义,进度环、仪表盘都靠它实现。 用法​ 要使用Konva添加弧形, 我们可以实例化一个Konva.Arc()对象. 有关属性和方法的完整列表,请参阅Konva.Arc文档 Konva Arc Demo
常见问题​ Arc 和 Wedge 有什么区别?​ Arc 有 innerRadius,画出来是环的一段,中间是空的; Wedge 从圆心张开,画出来是实心的扇形,像一块披萨。 做进度环、仪表盘用 Arc;做饼图用 Wedge。把 Arc 的 innerRadius 设为 0 确实也能得到扇形,但语义上不如直接用 Wedge 清楚。 设置了 clockwise 之后起止位置全乱了?​ clockwise: true 会让弧从起始角度反向扫过 angle 度。也就是说, 同样的 rotation 与 angle,翻转 clockwise 后弧出现在圆的另一侧。 比较稳妥的做法是先只调 rotation 把起点摆对,再决定 clockwise, 而不是两个一起试。 进度为 0 时为什么还残留一个点?​ angle: 0 的弧虽然没有面积,但它仍然是一条有效路径。如果设了 stroke, 描边会在起点位置画出一个小点或短线。 正确做法是进度为 0 时整个隐藏该节点(visible(false)),而不是把角度设成 0。 这同时也省掉了一次无意义的绘制。 与其他方案的取舍​ 完整圆环用 Ring,一段圆环用 Arc,实心扇形用 Wedge。这三个覆盖了绝大多数圆形相关的需求, 都不需要手写路径。 Konva.Path 的 A(椭圆弧)命令更灵活——它支持长短半径不同、可以任意旋转, 还能和其他路径命令拼接。代价是没有 innerRadius 这种现成语义, 画环的一段要自己算出内外两条弧并闭合。只在 Arc 表达不了时才值得这样做。 --- ## Arrow 箭头 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/arrow Arrow 箭头 箭头是带箭头尖的线条,Konva.Arrow 继承自 Konva.Line,points 的用法完全一致。 用法​ 要使用Konva创建箭头, 我们可以实例化一个Konva.Arrow()对象. 有关属性和方法的完整列表,请参阅Konva.Arrow文档 Konva Arrow Demo
常见问题​ 箭头画在哪一端?​ 默认画在 points 最后一段的末端,方向由最后两个点决定。 需要双向箭头时设置 pointerAtBeginning: true,起点也会长出箭头。 只要起点箭头、不要终点的,再把 pointerAtEnding 设为 false。 把线加粗之后箭头显得很小?​ pointerLength 与 pointerWidth 是独立的固定值,不会随 strokeWidth 自动缩放。 所以改粗线条后必须同步调大这两个值,否则箭头相对线身会越来越不成比例。 如果线宽是动态的,按 strokeWidth 的倍数计算箭头尺寸是个实用做法。 折线首尾重合时箭头方向乱跳?​ 最后两个点重合时,方向向量为零,无法确定箭头朝向。这在「双击结束绘制」的 交互里很常见——双击会产生两个坐标相同的点。 Konva 10.6.0 把这种情况的行为固定为水平指向,不再随机。 更彻底的解决办法是在提交折线前去掉重复的尾点。 与其他方案的取舍​ Arrow 的核心便利是箭头自动跟随线段方向——折线怎么转,箭头就怎么转, 不需要自己算角度。做流程图连线、标注指引时用它。 自己用 Line 加一个三角形 Line 也能拼出箭头, 好处是箭头形状完全自由(可以做成空心的、燕尾的、圆头的), 代价是线段方向一变就要重算三角形的三个顶点和旋转角。 需要曲线箭头时两者都不够用——Arrow 只支持直线段。这种情况下用 Path 画曲线,再在末端单独放一个旋转好的三角形。 --- ## Circle 圆形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/circle Circle 圆形 圆形的定位方式与矩形不同,x、y 指的是圆心——这是初学 Konva 时最容易混淆的一处。 用法​ 要使用Konva创建一个圆形, 我们可以实例化一个Konva.Circle()对象. 有关属性和方法的完整列表,请参阅Konva.Circle文档 /** * shapes/circle.html */ Konva Circle Demo
常见问题​ 圆形的 x、y 是圆心还是外接矩形的左上角?​ 是圆心。这和 Konva.Rect 正好相反,后者的 x、y 是左上角。 同一个场景里混用矩形和圆形时,这个差异会让布局计算频频出错。一个实用的习惯是: 凡是以中心定位的图形(Circle、Ellipse、Ring、Wedge、Star、RegularPolygon), 心里都按「中心」记;只有 Rect、Image、Text 是左上角。 为什么没有 radiusX 和 radiusY?​ Konva.Circle 只有一个 radius,按定义它就是正圆。需要长短轴不同的形状时用 Konva.Ellipse,它提供 radiusX 与 radiusY。 不要用 scaleX 把圆压扁来代替椭圆——缩放会同时作用在描边上,strokeWidth: 2 横向看起来会比纵向粗。 getClientRect 量出来的尺寸比 radius × 2 大?​ 描边是以路径为中线向两侧各画一半的,所以带 strokeWidth: 4 的圆,实际占据的 范围比直径大 4 像素(两侧各 2)。getClientRect() 返回的是包含描边与阴影的 视觉包围盒。 只想要几何尺寸时用 getSelfRect(),它不计描边。做碰撞检测或自动布局时, 选错这两个方法会带来几像素的系统性偏差。 与其他方案的取舍​ 画正圆一定用 Circle,它的绘制路径最短。需要椭圆时用 Ellipse 而不是给 Circle 加 scale,原因见上面关于描边失真的说明。 想画圆环或圆环的一段,不要用「大圆叠一个背景色小圆」的做法——在半透明背景或 有底图的场景下会立刻露馅。完整圆环用 Konva.Ring, 一段圆环用 Konva.Arc,两者都是真正挖空的。 另外,RegularPolygon 把 sides 设到 64 以上视觉上也接近圆,但顶点越多绘制越贵, 没有理由这样代替 Circle。 --- ## Custom 自定义图形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/custom Custom 自定义图形 当内置图形都表达不了时,自定义图形让你直接拿到 Canvas 上下文作画。 用法​ T要使用Konva创建自定义形状, 我们可以实例化一个Konva.Shapew()对象. 当创建自定义形状时,我们需要定义一个通过Konva.Canvas渲染器传递的绘图函数. 我们可以使用渲染器来访问HTML5 Canvas上下文,并使用像context.fillStrokeShape(this)这样的特殊方法,它会自动处理填充,描边和阴影. 有关属性和方法的完整列表,请参阅Konva.Shape文档 Konva Custom Shape Demo
常见问题​ fill、stroke、shadow 属性为什么全都不生效?​ sceneFunc 里必须调用 context.fillStrokeShape(shape)。Konva 的样式属性不会 自动应用——它把上下文交给你之后,需要你主动告诉它「现在按节点的样式把这条路径 填充并描边」。 sceneFunc: (context, shape) => { context.beginPath(); // ...绘制路径... context.closePath(); context.fillStrokeShape(shape); }, 漏掉这一行,路径画了但没有任何填充和描边,画面上什么都看不到。 自定义图形的点击区域为什么是个矩形?​ 没有 hitFunc 时,Konva 用节点的包围盒作为命中区域。对于凹形、镂空、 细长斜线这类图形,包围盒和实际形状差别很大,点空白处也会触发事件。 需要精确命中时另写一个 hitFunc,画法和 sceneFunc 一样, 只是最后调用 context.fillStrokeShape(shape) 时 Konva 会把它画到命中图上。 命中图可以比视觉图形简化,这也是性能优化的常用手段。 sceneFunc 里抛异常会怎样?​ 该帧的绘制会中断,这个图形之后的内容不会被画出来。 Konva 10.4.0 修复了一个更严重的问题:此前 sceneFunc 抛异常会让节点停留在 损坏状态,之后即使修好也无法恢复正常绘制。现在节点状态能正确恢复, 但异常本身仍会中断当前帧,所以 sceneFunc 里的边界情况要自己兜住。 性能提示​ sceneFunc 每一帧都会执行。把不随帧变化的东西提到外面去——路径顶点的计算、 渐变对象的创建、正弦余弦这类三角运算,都不该放在回调里反复做。 渐变尤其值得注意:context.createLinearGradient() 每次调用都会新建一个对象, 放在 sceneFunc 里意味着每帧都在制造垃圾。把它建好存在闭包或节点属性上复用。 如果自定义图形本身复杂且不常变化,直接 cache() 成位图, 这样 sceneFunc 只在缓存时执行一次。 --- ## Ellipse 椭圆 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/ellipse Ellipse 椭圆 椭圆由两个半轴定义,和圆形一样以中心定位。 用法​ 要使用Konva创建一个椭圆, 我们可以实例化一个Konva.Ellipse()对象. 有关属性和方法的完整列表,请参阅Konva.Ellipse文档 Konva Ellipse Demo
常见问题​ radiusX 是半轴还是直径?​ 是半轴,即从中心到边缘的距离,与 Konva.Circle 的 radius 口径一致。 想画一个宽 200、高 100 的椭圆,应该写 radiusX: 100, radiusY: 50。 误当成直径填写是最常见的错误,结果是图形大了一倍。 怎么画一个倾斜的椭圆?​ 用 rotation 属性,单位默认是角度。 不要通过交换 radiusX 与 radiusY 来「转 90 度」——那样得到的确实是另一个方向的 椭圆,但描边宽度、阴影偏移、渐变方向都不会跟着转,一旦这些属性非默认值, 结果就和预期不符了。 scaleX 和增大 radiusX 有什么区别?​ 几何轮廓上两者等价,但 scale 会连同描边一起缩放。strokeWidth: 2 的椭圆在 scaleX: 2 之后,横向的描边视觉宽度变成 4,纵向仍是 2,看起来是歪的。 要改变形状就改半轴;scale 留给需要整体(含描边)一起放大的场合, 比如悬停时的放大反馈。 与其他方案的取舍​ 正圆用 Circle,它少一个属性、语义更清楚。椭圆用 Ellipse, 不要用 Circle + scale 代替,理由见上面关于描边失真的说明。 需要椭圆的一段弧时 Konva 没有现成图形,得用 Konva.Path 的 A 命令手写——SVG 的椭圆弧命令支持两个不同的半径,正好对应这个需求。 --- ## Group 分组 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/group Group 分组 Konva.Group 把若干节点装进一个容器,之后对容器做的变换会作用到全部子节点。 拖一次就带走一整组、旋转一次整体转——不用给每个图形各自算坐标。 const group = new Konva.Group({ x: 20, y: 20, rotation: 12, draggable: true, }); group.add(rect); group.add(circle); group.add(text); layer.add(group); 子节点的 x / y 是相对于 Group 的,不是舞台坐标。 Group 移动时子节点的 x / y 不变,变的是 Group 自己。 Group 与 Layer 的区别​ 两者都是容器,都能 add() 子节点,但底层完全不同: LayerGroup 对应 DOM一个真实的 元素无 内存开销一张场景画布 + 一张命中画布几乎为零 独立重绘可以,其他层不受影响不能,随所在图层一起重绘 数量建议少,超过 5 个 Konva 会打印告警不限 判断标准很清楚:要「这部分变化时别的不用重画」用 Layer, 要「这几个东西一起动」用 Group。 新手常见的错误是为了组织结构而拆很多 Layer。 每个 Layer 都要在 DOM 里放一个画布元素,在高分屏上一张全屏画布 就是几千万像素的显存,拆到十几层会明显拖慢合成。 Konva 自己会拦你——stage.add() 时图层数超过 5 就打印一条告警, 原文是: The stage has N layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group. 纯粹的逻辑分组一律用 Group。 Group 没有自己的尺寸​ Konva.Group 不是 Shape,它没有 sceneFunc, 因此没有 fill、stroke,也没有真正的宽高: group.width(); // 0 —— 不管里面装了多大的东西 group.height(); // 0 想知道这一组占多大地方,用 getClientRect(): const box = group.getClientRect(); // { x, y, width, height } —— 含子节点、变换与描边的实际包围盒 这也是给 Group 加背景、加边框的办法:算出包围盒, 再往 Group 里插一个 Konva.Rect 放在最底层。注意插入之后包围盒会变, 要先算再插,不要在循环里反复取。 常见问题​ 变换 Group 之后子节点的坐标变了吗?​ 没有。子节点的 x() / y() 始终是相对父容器的值, Group 的变换矩阵在绘制时才累乘上去。 要拿舞台坐标系里的真实位置,用 getAbsolutePosition(): circle.x(); // 30 —— 相对 Group circle.getAbsolutePosition(); // { x: ..., y: ... } —— 相对舞台 这一点在做碰撞检测、吸附、对齐时特别容易出错—— 两个不同 Group 里的节点,直接比较 x() 没有意义。 能给 Group 设 fill 吗?​ 不能。Group 是容器,没有可填充的路径。 要给一组内容加背景,在 Group 里放一个 Konva.Rect 作为第一个子节点: const box = group.getClientRect({ relativeTo: group }); const bg = new Konva.Rect({ x: box.x, y: box.y, width: box.width, height: box.height, fill: '#f5f5f5', }); group.add(bg); bg.moveToBottom(); { relativeTo: group } 不能省——默认返回的是舞台坐标系里的包围盒, 直接拿去当子节点的 x / y,会额外偏移一个 Group 自身的位置。 怎么让 Group 里只有某几个子节点响应事件?​ 给不需要响应的子节点设 listening(false)。 注意 Group 自己设 listening(false) 会让整组都不响应, 包括所有子节点——这是命中检测里 最常见的困惑来源。 反过来,Group 设了 draggable: true 之后, 点在任意子节点上都能拖动整组; 如果想让某个子节点单独可拖,给它自己也设 draggable: true, 它的拖拽会覆盖父级的。 Group 可以嵌套吗?​ 可以,而且没有层数限制。但每多一层嵌套, 每个子节点在绘制和求绝对坐标时就多一次矩阵乘法。 深层嵌套在节点多的时候会明显拖慢。 如果你发现自己嵌了五六层,通常说明结构可以拍扁—— Group 的层级应该反映一起变换的单元,而不是业务上的分类。 性能提示​ Group 本身没有渲染开销——它不绘制任何东西,只是传递变换。 所以「为了组织结构而分组」是免费的,可以放心用。 真正影响性能的是围绕 Group 的两个操作。 group.cache() 是性价比很高的优化,也很容易用错。 它把整组内容烤成一张位图,之后绘制这一组就等于画一张图, 成本与内部有多少节点无关。一个装了两百个小图形的静态 Group, 缓存后绘制耗时能降一两个数量级。 代价是缓存之后子节点的改动不会自动反映—— 改了子节点的属性,画面纹丝不动,必须 group.clearCache() 或重新 group.cache()。这是缓存最常见的坑: 「我明明改了值,为什么没变化」。 所以判断标准是这一组会不会频繁变: 静态或很少变的,缓存收益巨大;每帧都在改内部的,缓存是净损失 (每次都要重建位图)。 getClientRect() 不便宜。 它要递归遍历所有子节点、 逐个求变换后的包围盒再取并集。不要在 dragmove 之类的高频回调里 对一个大 Group 反复调用——把结果缓存起来, 只在内容真正变化时重算。 Konva 10.4.0 起 clearCache() 的行为有过修正: 此前对容器调用会把所有后代的缓存也一起清掉并泄漏它们的画布, 现在不会了。如果你的项目还在更早的版本上大量使用嵌套缓存, 这是一个值得升级的理由。 --- ## Image 图片 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/image Image 图片 图片是唯一需要等待异步加载的图形,这决定了它的用法和其他图形都不一样。 用法​ 要使用Konva渲染一个图片, 我们可以实例化一个Konva.Image()对象. 有关属性和方法的完整列表,请参阅Konva.Image文档 {% include_code Konva Image Demo shapes/Image.html %} Konva Image Demo
常见问题​ 创建了 Konva.Image 但画布上一片空白?​ 几乎都是因为图片还没加载完。必须在 onload 回调里创建 Konva.Image, 或者至少在 onload 之后调用一次 layer.draw()。 const img = new Image(); img.onload = () => { layer.add(new Konva.Image({ image: img, x: 0, y: 0, width: 200, height: 150 })); }; img.src = "/assets/photo.jpg"; Konva 10.6.0 修复了一个相关问题:先 new Image() 再赋 src 的写法此前 不会触发重绘,现在会了。但显式等待 onload 仍然是更清楚的写法。 图片在高分屏上显得特别大?​ 不设 width、height 时按图片的像素尺寸绘制。一张 2x 图会占据两倍的逻辑像素, 看起来就过大了。 始终显式给出 width 与 height,按你想要的显示尺寸设置, 而不是依赖图片本身的尺寸。 toDataURL 报 SecurityError 或导出的图片是空白?​ 画布被跨域图片污染了。只要往 Canvas 上画过一张没有正确 CORS 头的跨域图片, 整个画布就无法再导出。 需要两个条件同时满足:图片服务端返回 Access-Control-Allow-Origin, 并且客户端在赋 src 之前设置 img.crossOrigin = "anonymous"。 只做其中一件都不够。 国内环境注意事项​ 跨域导出这件事在国内特别容易踩坑:主流图床与对象存储的默认配置不返回 Access-Control-Allow-Origin,需要到控制台里单独开启跨域规则。很多团队是在 「导出功能上线后才发现全部失败」时才意识到这一点。 更麻烦的是,img.crossOrigin = "anonymous" 在服务端不支持 CORS 时会让图片 加载失败——本来还能显示,加了这行反而白屏。所以不能「先加上再说」, 必须先确认服务端配置。 最省心的做法是把需要参与导出的图片与站点同域托管,或者通过自己的服务端代理一层。 如果图片只用于展示、不需要导出,那么不设 crossOrigin 即可,跨域不影响绘制。 --- ## Label 标签 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/label Label 标签 标签是「气泡框 + 文字」的组合节点,Konva 会让框自动跟随文字尺寸。 用法​ 要使用Konva创建标签,可用于创建带有背景,简单工具提示或带指针提示的文本. 我们可以实例化一个Konva.Lable()对象. 有关属性和方法的完整列表,请参阅Konva.Lable文档 Konva Label Demo
常见问题​ 创建了 Label 却什么都不显示?​ Konva.Label 本身只是容器,必须往里面同时 add 一个 Konva.Tag(背景框) 和一个 Konva.Text(文字)。少任何一个都不会显示,也没有报错。 顺序也有讲究:先 add Tag 再 add Text,否则背景会盖住文字。 给 Tag 设了 width 为什么没效果?​ Tag 的尺寸是由同一个 Label 里的 Text 自动撑开的,不接受手动设置。 想让气泡更宽,改 Text 的 width 或 padding。padding 设在 Text 上, 它会同时撑大背景框——这正是 Label 相对手工组合的便利之处。 指针(小三角)不显示或位置不对?​ pointerDirection 决定朝向(up/down/left/right), 但还需要配套设置 pointerWidth 与 pointerHeight,缺省时尺寸为 0,看不见。 另外 Label 的 x、y 指的是指针尖端的位置(有指针时), 不是气泡框的左上角——做 tooltip 跟随鼠标时这一点很有用,直接把鼠标坐标给它即可。 与其他方案的取舍​ Label 的价值在于自动尺寸:文字变了,背景框跟着变,指针位置也跟着算。 做 tooltip、数据标注这类内容不固定的场景,用它省去大量手工计算。 自己用 Konva.Group 组合 Rect + Text 也能实现,而且样式自由度更高—— 比如想要双层边框、渐变背景、或者图标加文字的复合内容,Tag 就不够用了。 代价是文字变化时要自己监听并重算 Rect 的尺寸。 判断标准很简单:内容会变就用 Label,样式复杂且内容固定就手工组合。 --- ## Line 线条总览 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/line Line 线条总览 Konva.Line 一个类承担了四种看起来很不一样的图形。 区别只来自两个属性:closed(首尾是否连起来)和 tension(曲线张力)。 两个属性,四种形态​ closedtension形态详见 false0直线 / 折线简单直线 true0多边形多边形 false> 0样条曲线样条曲线 true> 0平滑闭合的团块Blob 同一组 points,四种组合画出来的结果完全不同—— 上面的演示把它们并排放在一起。 new Konva.Line({ points: [0, 40, 40, 0, 80, 50, 120, 10], stroke: '#4078c0', strokeWidth: 3, closed: false, tension: 0, }); points 是扁平数组​ 坐标写成一个一维数组,x 和 y 交替排列: points: [0, 40, 40, 0, 80, 50] // ✅ 三个点 points: [[0, 40], [40, 0], [80, 50]] // ❌ 不支持 这是最常见的写错方式之一。传嵌套数组不会抛出清晰的错误, 画面只是不显示或形状诡异。 数组长度应当是偶数。长度为奇数时最后一个孤立的值没有配对的坐标, 会被忽略——同样不报错。 从对象数组转换的话: const pts = [{ x: 0, y: 40 }, { x: 40, y: 0 }]; const flat = pts.flatMap((p) => [p.x, p.y]); 常见问题​ tension 到底是什么?​ 它控制样条曲线的"松紧"。Konva 用它把折线的顶点转换成一组贝塞尔控制点, 顶点本身仍然会被曲线穿过——这一点和贝塞尔曲线不同, 贝塞尔的控制点是不在曲线上的。 0(默认)——直连,就是折线 0.5 左右——自然的平滑曲线,最常用 1 以上——曲线开始在顶点外"甩出去",出现明显的过冲 所以 tension 不是"曲率"也不是"控制点位置", 它更像是"让线在顶点处拐弯有多圆滑"。 需要精确控制曲线形状(指定控制点)时,用 Konva.Path 写 SVG 路径数据,tension 做不到。 为什么线条点不中?​ 两个原因叠加: 没有 fill 的线只有描边可命中。 设了 closed: true 并给 fill 之后内部才可点。 命中宽度默认等于 strokeWidth。 一条 1px 的线, 命中区域也只有 1px 宽,鼠标几乎不可能对准。给它加宽命中区: new Konva.Line({ points: [...], strokeWidth: 1, // 视觉上还是 1px hitStrokeWidth: 12, // 命中区 12px }); Konva 10.4.0 改进过细线的命中检测,细线现在能在自身像素上被命中, 但要让用户"好点中",hitStrokeWidth 仍然是必需的。 更多见自定义命中区域。 能画箭头吗?​ 用 Konva.Arrow,它继承自 Line, points、tension、closed 全都能用,额外多了箭头端的配置。 注意 Konva 10.6.0 修了一个相关问题:当线的首尾点重合时 (比如双击结束绘制导致最后两点相同),箭头会朝向水平方向而不是正确的角度。 怎么做可拖拽的顶点编辑?​ 标准做法是给每个顶点放一个小圆作为锚点, 在锚点的 dragmove 里改写 points 数组: anchor.on('dragmove', () => { const pts = line.points().slice(); // 注意要复制 pts[index * 2] = anchor.x(); pts[index * 2 + 1] = anchor.y(); line.points(pts); }); .slice() 不能省。 Konva 10.6.0 起,未设置过的数组属性 每次读取返回的是新数组,但已设置过的返回的是内部引用—— 直接原地修改它,Konva 检测不到变化,画面不会更新。 性能提示​ Line 的绘制成本随 points 的长度线性增长, 但真正的性能问题出在自由绘制这类场景: 用户按住鼠标画一笔,每次 mousemove 往 points 里推两个数, 然后整条线重绘。 一笔画上几千个点之后,每帧都在重建一条几千段的路径, 帧率会明显下降。三个办法: 采样抽稀。 不是每个 mousemove 都记点—— 和上一个点距离小于某个阈值(比如 2px)就跳过。 肉眼看不出区别,点数能少一半以上。 分段固化。 每画满若干个点就把当前这一笔封存成一个独立的 Konva.Line 并 cache(),新的笔画从头开始。 已完成的部分变成位图,不再参与路径重建, 这是白板类应用的标准做法,见图形缓存。 把正在画的那一笔单独放一层。 已完成的笔画留在静态图层, 只有当前活动的这一笔在上层。这样每帧只重绘一层, 而且那一层上只有一条线,见图层管理。 另外,闭合路径在 Konva 10.6.0 有一处行为变更: getLength() 现在包含闭合边(与 SVG 一致)。 如果你用 getLength() 做沿线动画或均匀取点, 升级后闭合图形的计算结果会变。 --- ## Blob 图形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/line-blob Blob 图形 Blob 是闭合的平滑曲线——Konva.Line 同时设置 closed: true 与 tension 即可。 用法​ 要使用Konva创建一个不规则图形, 我们可以实例化一个Konva.Line()对象和closed = true tension属性. 有关属性和方法的完整列表,请参阅Konva.Line文档 Konva Line Blob Demo
常见问题​ tension 设了却看不出平滑效果?​ 先确认点数:tension 需要至少三个点才有视觉效果,两个点之间无论张力多大都只能是直线。 再确认数值:tension 默认为 0,也就是不平滑。忘记设置时画出来就是普通折线, 很容易误以为功能没生效。常用取值在 0.3 到 0.6 之间。 tension 调大之后曲线打结了?​ tension 超过 1 左右时,Konva 推导出的控制点会越过相邻顶点,曲线于是向外甩出 并自我交叉,看起来像打了结。 这是插值算法的固有行为,不是缺陷。需要更夸张的弧度时,与其加大 tension, 不如增加顶点来引导曲线走向。 Blob 和 Spline 到底差在哪?​ 只差一个 closed。两者都是 Konva.Line 加 tension, Spline 是开放曲线,Blob 首尾相连因而可以填充。 换句话说,它们不是两种图形,而是同一种图形的两种配置。 与其他方案的取舍​ tension 的优点是只给顶点、控制点由 Konva 自动推导,改一个点整条曲线自然重算, 很适合「拖动锚点改形状」这类交互。代价是无法精确控制某一段的弯曲方式。 需要精确控制时用 Konva.Path 的贝塞尔命令(C、Q、S), 每段的控制点都由你指定。设计稿导出的曲线走这条路,才能和设计稿一致。 一个折中做法:交互编辑阶段用 tension 让用户拖点,定稿后导出为 Path 数据存档。 --- ## Polygon 多边形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/line-polygon Polygon 多边形 给 Konva.Line 加上 closed: true 就得到多边形——首尾自动相连,内部可以填充。 用法​ 要使用Konva创建一个多边形, 我们可以实例化一个Konva.Line()对象和closed=true属性. 有关属性和方法的完整列表,请参阅Konva.Line文档 Konva Line Polygon Demo
常见问题​ 设置了 fill 为什么没有填充?​ 多边形必须显式设置 closed: true。未闭合的线条即使首尾坐标相同,Konva 也当它是 开放路径,fill 不生效。 这和「把最后一个点设成和第一个点一样」不是一回事——那样只是视觉上闭合, 填充规则仍然不启用。 自相交的多边形填充结果为什么有空洞?​ Canvas 默认按非零环绕规则填充。路径自相交时,被反向环绕两次的区域会被判定为 「在形状之外」而留空。 这不是 bug,是填充规则的定义。如果不想要空洞,调整顶点顺序消除自相交; 如果恰恰想要这个效果(例如画五角星的镂空版),保持自相交即可。 多边形的 x、y 是重心吗?​ 不是。x、y 是整个 points 坐标系的原点偏移量,points 里的数值都相对它计算。 所以设置 rotation 时,多边形绕的是这个原点而非重心。想绕重心旋转, 需要自己算出重心并设为 offsetX、offsetY。 与其他方案的取舍​ 正多边形不要手算顶点,用 Konva.RegularPolygon—— 给个 sides 和 radius 就行,改边数时不用重算数组。 不规则多边形用 Line 加 closed: true,顶点完全由你控制。 如果形状是美术用矢量工具画好的,直接导出 SVG 把 d 贴进 Konva.Path,比把路径翻译成顶点数组可靠得多—— 尤其当路径里含曲线命令时,转成折线顶点必然损失精度。 --- ## Line 简单线条 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/line-simple-line Line 简单线条 线条是 Konva 里唯一用扁平数组描述坐标的图形,这个约定贯穿 Line、Polygon、Blob 与 Spline。 用法​ 要使用Konva创建一个简单的线条, 我们可以实例化一个Konva.Line()对象. 有关属性和方法的完整列表,请参阅Konva.Line文档 Konva Simple Line Demo
常见问题​ points 该传点对象数组还是扁平数组?​ 必须是扁平数组:[x1, y1, x2, y2, ...],不是 [{x, y}, ...]。 传错结构时 Konva 不会报错,只是画不出任何东西——对象被当作数字参与坐标计算, 结果是 NaN。排查「线条不显示」时,先把 points 打印出来确认它是一串数字。 改了 points 数组,画面为什么没变?​ 要重新赋值整个数组才会触发更新:line.points([...])。对 line.points() 返回的 数组原地 push 或改元素不会通知 Konva,它无从得知内容变了。 Konva 10.4.0 起,未显式设置过的数组属性每次读取都返回新数组,这条尤其要注意——原地修改的那份根本不是节点持有的数据。 水平线和垂直线为什么看起来发虚?​ 和矩形描边是同一个原因:strokeWidth: 1 的线以路径为中线、向两侧各画半像素, 落在整数坐标上时会横跨两个物理像素,各画一半,视觉上成为两像素宽的灰线。 把坐标偏移半像素(例如 y: 100.5)能让它落在单个像素内。画网格、刻度、 分隔线时这个差别很明显。 性能提示​ 一条折线应当用一个 Konva.Line 加多个点,而不是拆成多段各建一个节点。 节点数量是 Konva 性能的主要成本项,一百段折线拆成一百个节点,命中检测和 变换计算都要做一百次。 纯描边、无填充无阴影的线条可以设 perfectDrawEnabled(false)。这个开关关掉的是 「用缓冲画布消除描边与填充交叠处色差」的处理,而纯描边图形本来就没有这个问题, 关掉是净收益。详见关闭 Perfect Drawing。 --- ## Spline 样条曲线 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/line-spline Spline 样条曲线 样条曲线是开放的平滑曲线,与 Blob 的唯一区别是不闭合。 用法​ 要使用Konva创建一个曲线(样条), 我们可以实例化一个Konva.Line()对象和tension属性. 有关属性和方法的完整列表,请参阅Konva.Line文档 Konva Line Spline Demo
常见问题​ 为什么画出来还是折线?​ tension 的默认值是 0,此时样条退化为直线段连接。必须显式设置一个正值, 常用范围是 0.3 到 0.6。 这是最常见的「功能没生效」原因——代码里用了 Konva.Line 想画曲线, 却没设 tension。 getClientRect 返回的范围比 points 的包围盒还大?​ 是正常的。样条为了平滑会在顶点之间「冲出」一点,曲线的极值点可能落在 所有顶点构成的包围盒之外,tension 越大冲出越多。 做自动布局或碰撞检测时要用 getClientRect() 的实际值,不要拿 points 自己算—— 后者会小于真实占据的范围。 逐帧修改 points 做动画时曲线为什么会跳变?​ 如果每帧传入的数组长度不同,Konva 要重新推导全部控制点,曲线形状会整体重算, 视觉上就是跳变。 做曲线动画时保持点数不变、只改坐标值,曲线就会平滑过渡。确实需要增删点时, 考虑先在目标位置插入一个与相邻点重合的点,再把它移动到位。 性能提示​ 样条比同样点数的折线贵——每段都要计算控制点并绘制三次贝塞尔曲线,而折线只是 lineTo。点数很多的曲线(例如手绘轨迹)要留意这个成本。 高频更新的曲线上,关掉 perfectDrawEnabled 并避免使用 shadow。阴影会让每一帧 都多一遍模糊计算,是手绘、轨迹回放这类场景掉帧的常见原因。 如果曲线画完后不再变化,cache() 成位图;但正在被拖动编辑的曲线不要缓存, 那样每帧都要重建缓存,反而更慢。 --- ## SVG Path 路径 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/path SVG Path 路径 Path 直接接受 SVG 的路径字符串,是把矢量设计稿搬进 Canvas 最直接的方式。 用法​ 要使用Konva添加SVG路径, 我们可以实例化一个Konva.Path()对象. 当我们要将SVG路径导出到HTML5 Canvas,或者想将复杂的绘图显示为数据字符串而不是自定义形状时,路径是最常用的. 有关属性和方法的完整列表,请参阅Konva.Path文档 Konva Path Demo
常见问题​ 能直接用设计稿导出的 SVG 吗?​ 可以。把 SVG 里 元素的 d 属性整段复制给 data 即可, Konva 支持标准的 SVG 路径命令。 注意一个 SVG 文件里往往有多个 ,每个要建一个 Konva.Path; 另外 SVG 的 fill、stroke 是元素属性,不在 d 里,需要另行设置到 Konva 节点上。 路径粘贴进来之后跑到画布外面去了?​ SVG 的坐标是相对它自己的 viewBox 的,粘过来之后仍然按那套坐标绘制, 很容易整体偏移甚至完全在可视区之外。 用 getClientRect() 量出实际包围盒,再把 offsetX、offsetY 设为它的 x、y, 就能把路径的左上角对齐到节点的 x、y。 升级到 Konva 10.6.0 后路径长度变了?​ 是的,这是一处有意的行为修正。闭合命令 z 之后的相对命令现在从子路径起点 开始计算(此前从 z 之前的最后一个点算起),getLength() 也开始包含闭合边, 两者都与 SVG 规范一致。 如果你基于旧行为做过路径长度计算——比如用 getPointAtLength() 沿路径布点、 或者用描边虚线做绘制动画——升级后需要复核这些数值。 与其他方案的取舍​ 能用内置图形表达的就别用 Path。矩形、圆、多边形各有专门的图形,属性语义清楚、 改参数不用重写路径字符串。 Path 的适用场景是:形状来自设计稿、或者需要多种曲线命令拼接的复杂轮廓。 它有一个重要限制:整条路径只能有一套样式。想让某一段是红色、另一段是蓝色, 或者只给一段加阴影,都必须拆成多个 Konva.Path 节点。设计分段效果时要提前 把路径拆好,而不是画完再想办法。 --- ## Rect 矩形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/rect Rect 矩形 矩形是 Konva 里最常用的图形,也是理解其他图形定位方式的起点。 用法​ 要使用Konva创建一个矩形, 我们可以实例化一个Konva.Rect()对象. 有关属性和方法的完整列表,请参阅Konva.Rect文档 cornerRadius 既可以是一个数字(四角统一),也可以是 [左上, 右上, 右下, 左下] 形式的数组。 /** * shapes/rect.html */ Konva Rect Demo
常见问题​ 为什么设置了 rotation,矩形像是绕着左上角在转?​ Konva.Rect 的 x、y 是左上角坐标,不是中心点——这一点和 Konva.Circle、 Konva.Ellipse 正好相反,后两者的 x、y 指的是圆心。所以给矩形设置 rotation 时, 它默认绕左上角旋转。 想让它绕自身中心旋转,把 offsetX 设为宽的一半、offsetY 设为高的一半。注意设置 offset 之后,x、y 的含义也随之变成了中心点位置,原有的定位代码要一起调整。 描边为什么比设定的粗一点、边缘还发虚?​ Canvas 的描边是以路径为中线、向两侧各画一半的。strokeWidth: 1 的竖线如果落在整数 坐标上,就会横跨相邻两个像素、各画半格,视觉上呈现为两像素宽的灰线。 把坐标偏移半个像素(例如 x: 50.5)可以让 1 像素的描边正好落在单个物理像素内。 这是 Canvas 的固有行为,不是 Konva 的问题,画网格线、表格边框时尤其明显。 设置了 fill 却什么都看不到?​ 先确认三件事:图形已经 layer.add(rect)、图层已经 stage.add(layer)、 以及 width 与 height 都给了非零值。 Konva.Rect 不像 CSS 盒子会有内容撑开的默认尺寸,宽高必须显式给出,缺省时为 0, 填充色再鲜艳也画不出任何东西。 性能提示​ 矩形是绘制开销最低的图形之一,通常不需要缓存。但一旦加上 shadow,情况就完全不同—— 阴影在每一帧都要重新计算,是 Canvas 上最昂贵的操作之一。静态的带阴影矩形应当调用 cache(),把它一次性渲染成位图,之后每帧只是贴图。 另外,只用作背景或遮罩、不需要响应事件的矩形,设置 listening(false) 可以让它不参与 命中检测,详见禁用事件监听。命中检测是逐图形做的, 在图形数量多的场景下这项收益很可观。 --- ## Regular Polygon 等边多边形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/regular-polygon Regular Polygon 等边多边形 正多边形只需要边数和半径两个参数,不用自己算顶点坐标。 用法​ 要使用Konva添加正多边形, 我们可以实例化一个Konva.RegularPolygon()对象. 有关属性和方法的完整列表,请参阅Konva.RegularPolygon文档 Konva RegularPolygon Demo
圆角​ Konva 10 起 RegularPolygon 支持 cornerRadius,可以把正多边形的尖角磨圆: const hexagon = new Konva.RegularPolygon({ x: 100, y: 100, sides: 6, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, cornerRadius: 10, }); cornerRadius 不能为负值。Konva 10.4.0 之前负的 radius 或 cornerRadius 会在绘制时抛错,并导致它之后的图形全部不显示;10.4.0 起改为拒绝该值而不再中断绘制。 常见问题​ radius 是边长还是半径?​ 是外接圆半径,即中心到任一顶点的距离,不是边长。 这意味着相同 radius 下,边数越多图形看起来越「大」——三角形的三个顶点之间空得多, 而十二边形几乎填满整个外接圆。做一组不同边数的图标时,想让它们视觉大小一致, 需要按边数反推 radius,而不是取同一个值。 sides 设成 2 或不设会怎样?​ sides 必须至少为 3。Konva 10.4.0 修复了一个相关问题:此前不设置 sides 会让 该节点每次绘制都抛错,进而中断整个图层的渲染——表现为这个图形之后的所有内容 都不显示,很难定位到根源。 升级到 10.4.0 及以上后不再中断绘制,但仍然应当显式给出 sides。 第一个顶点朝哪个方向?​ 默认朝上(12 点方向),与 Konva.Star 一致。 用 rotation 调整朝向,x、y 是中心,旋转默认绕中心进行,不需要额外设 offset。 与其他方案的取舍​ 正多边形一律用 RegularPolygon,不要用 Konva.Line 手算顶点。手算的问题不在于算不出来,而在于边数一改就要重算整个数组, 而且很容易把外接圆半径和边长搞混。 反过来,需要不规则多边形——顶点不在同一个圆上、或者边长不等——RegularPolygon 就表达不了,这时才用 Line 配合 closed: true 自己给顶点。 有一种常见的误用是把 sides 设到 64 以上来模拟圆形。视觉上确实接近, 但顶点越多绘制越贵,而 Konva.Circle 是浏览器原生的 圆弧绘制,无论如何都更快也更圆。没有理由这样代替。 --- ## Ring 环形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/ring Ring 环形 环形是挖空了中心的圆,常用于进度环、甜甜圈图这类需要「中间留白」的场景。 用法​ 要使用Konva添加环形, 我们可以实例化一个Konva.Ring()对象. 有关属性和方法的完整列表,请参阅Konva.Ring文档 /** * shapes/Ring.html */ Konva Ring Demo
常见问题​ 设置了 innerRadius 却什么都没画出来?​ 检查 innerRadius 是不是大于或等于 outerRadius。两者相等时环的宽度为零, 大于时形状无效,两种情况都是什么都不显示,而且不会报错。 这在用变量计算半径时尤其容易发生,例如按百分比算内径却忘了它是相对外径的。 环形的 x、y 是圆心吗?​ 是圆心,和 Circle 一致。所有以「半径」定义的图形 (Circle、Ring、Wedge、Arc、Star、RegularPolygon)都以中心定位, 只有 Rect、Image、Text 用左上角。 怎么画只有一部分的环,比如进度环?​ Konva.Ring 总是完整的一圈,没有起止角度。要画一段环用 Konva.Arc,它同时有 innerRadius、outerRadius 和 angle, 正是为这个场景准备的。 典型做法是底下铺一个灰色 Ring 当轨道,上面叠一个彩色 Arc 表示进度。 与其他方案的取舍​ 完整圆环用 Ring,一段圆环用 Arc,两者都是真正挖空的, 放在任何背景上都正确。 不要用「大圆 + 背景色小圆」模拟环形。它在纯色背景上看着没问题,但只要环形本身 带了透明度、或者下面有图片、或者需要导出成带透明通道的 PNG,中间那块「假挖空」 就会暴露成一个实心色块。 需要更复杂的挖空形状(比如非圆形的孔)时,用 Path 配合 SVG 的填充规则,或者用容器的 clipFunc。 --- ## Sprite 精灵图 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/sprite Sprite 精灵图 精灵图把多帧画面放在一张图上,靠切换显示区域实现逐帧动画。 用法​ 要使用Konva创建一个精灵图动画, 我们可以实例化一个Konva.Sprite()对象. 有关属性和方法的完整列表,请参阅Konva.Sprite文档 Konva Sprite Demo
常见问题​ animations 的数值怎么组织?​ 每一帧四个数:[x, y, width, height],描述该帧在图片上的矩形区域。 一组动画就是若干帧的数值首尾相接拼成的扁平数组。 animations: { idle: [0,0,50,50, 50,0,50,50, 100,0,50,50], } 数组长度不是 4 的倍数时行为未定义——最后那组残缺的数值会被当成一帧, 画出错位的内容。 设置好了但动画不动?​ 必须显式调用 sprite.start()。仅仅创建节点并 add 到图层不会自动播放。 另外,改了 frameRate 之后需要重新 start() 才会按新速度播放, 正在播放时改这个值不生效。 销毁精灵之后 CPU 占用还是下不来?​ Konva 10.4.0 之前,Sprite 在 destroy() 后仍会保留内部定时器, 持续按帧率触发,造成看不见的持续开销。 10.4.0 起这个问题已修复。如果项目还停留在旧版本,习惯上应当先 stop() 再 destroy()。 性能提示​ 精灵图本身就是为性能设计的——一张图、一次上传 GPU,切帧只是改变绘制的源矩形, 比每帧换一张图便宜得多。 但不要对 Sprite 调用 cache()。缓存会把当前这一帧固定成位图, 之后的帧切换全部失效,看起来就是动画卡住了。这是缓存机制的正常表现: 它缓存的是「此刻的样子」,而精灵图的本质就是这个样子一直在变。 同理,任何持续变化的节点都不该缓存。缓存适用的是静态或低频变化的内容, 详见图形缓存。 --- ## Star 星形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/star Star 星形 星形由角数和内外半径定义,是少数几个 Konva 内置的复合多边形之一。 用法​ 要使用Konva添加星形, 我们可以实例化一个Konva.Star()对象. 有关属性和方法的完整列表,请参阅Konva.Star文档 Konva Star Demo
常见问题​ numPoints 是顶点数还是角数?​ 是角数。实际顶点数是它的两倍——每个角由一个外顶点和一个内顶点组成,交替排列。 numPoints: 5 得到常见的五角星,共 10 个顶点。想数清楚绘制成本时按两倍算, 不过星形的顶点数通常很少,这方面不构成负担。 星形看起来不够尖,怎么调?​ 尖锐程度由 innerRadius 与 outerRadius 的比值决定,比值越小越尖。 常见的五角星比值约为 0.38,比值接近 1 时星形会退化成一个近似正多边形的轮廓。 注意 outerRadius 是外接圆半径,不是星形的「宽度」——角数变化时, 相同 outerRadius 下星形的视觉大小也会变。 星形默认朝向是哪边?​ 第一个外顶点朝上(12 点方向),与 RegularPolygon 一致。需要其他朝向用 rotation 调整。 x、y 是星形中心,旋转默认绕中心进行,不需要额外设置 offset。 与其他方案的取舍​ 规则星形用 Star,它只要三个参数,绘制路径也由 Konva 算好。 需要不规则星形——比如每个角长短不一、或者角的位置不均匀分布——Star 就做不到了, 这时用 Konva.Line 配合 closed: true, 自己算出所有顶点坐标传给 points。代价是顶点计算要自己维护, 改角数时得重算整个数组。 如果星形形状固定不变,还有第三条路:用矢量工具画好导出 SVG, 把 d 属性贴进 Konva.Path。这适合美术提供的异形星标。 --- ## Text 文字 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/text Text 文字 文本是 Canvas 上最需要小心的元素——字体、换行、度量都不像 DOM 那样由浏览器代劳。 用法​ 要使用Konva添加文本, 我们可以实例化一个Konva.Text()对象. 有关属性和方法的完整列表,请参阅 Konva.Text文档 Konva Text Demo
逐字渲染​ Konva 10 提供 charRenderFunc,可以对每个字符单独控制绘制。回调里设置的 fillStyle / strokeStyle 对该字符生效,优先级高于图形本身的属性: const text = new Konva.Text({ x: 20, y: 20, text: '逐字上色', fontSize: 40, // 回调只接收一个对象参数,不是 (ctx, charInfo) charRenderFunc: ({ char, index, x, y, context }) => { context.fillStyle = index % 2 === 0 ? '#4078c0' : '#c04040'; context.fillText(char, x, y); }, }); 回调参数的完整字段: 字段含义 char当前字素 index全局字素索引,跨行累加,不是行内位置 x / y该字素的绘制坐标 lineIndex所在行号,从 0 开始 column行内列号 isLastInLine是否是该行最后一个字素 width该字素的测量宽度 contextKonva 的 Context 包装对象,fillText、fillStyle 等可直接用 要按行内位置做效果,用 column 而不是 index。 字素感知排版​ Konva 10.4.0 起,文本排版按字素(grapheme)而非 UTF-16 码元处理。这解决了 中文与表情场景下的两类老问题:国旗 emoji 与 ZWJ 组合表情(如 👨‍👩‍👧)不会再被 从中间拆开,letterSpacing 也改为每个字素加一次间距,而不是每个码元加一次。 如果你的布局依赖旧的按码元计算的间距值,升级到 10.4.0 后含 emoji 的文本 宽度会变化,需要重新确认排版。 常见问题​ 中文长文本为什么不换行?​ Konva.Text 只在设置了 width 之后才会自动换行。没有 width 时,整段文字排成一行, 超出画布的部分直接看不见,也不会有任何提示。 设了 width 还想在超长时截断,再加 ellipsis: true,配合 height 限制行数。 fontFamily 写「微软雅黑」在同事机器上不生效?​ Canvas 的字体解析走的是系统字体,不同操作系统装的中文字体不一样: Microsoft YaHei 在 macOS 上通常没有,PingFang SC 在 Windows 上也没有。 正确做法是给一串回退列表,例如 fontFamily: 'PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif', 让每个平台都能落到一个实际存在的中文字体上。只写一个字体名, 在缺少它的系统上会回退到默认字体,字宽随之改变,布局也跟着乱。 能不能在一段文字里混用两种颜色或字号?​ Konva.Text 不支持富文本,一个节点只有一套样式。 需要混排有两条路:拆成多个 Konva.Text 节点自己排版(要处理换行和对齐, 比较麻烦但可控),或者用 charRenderFunc 逐字控制样式(见上面的「逐字渲染」, 适合按位置规律变化的效果,不适合任意富文本)。 国内环境注意事项​ Web Font 在国内加载慢,会带来一个 DOM 上没有的问题:Canvas 没有 CSS 的 font-display 机制。浏览器会先用回退字体把文字画到画布上,等自定义字体加载完成后, 已经画上去的内容不会自动重绘——画面就一直停留在回退字体的样子, 直到下一次 layer.draw()。 而且单纯重绘并不够:Konva.Text 的换行在创建时就按当时的字体算好了, 字体到达后调用 layer.draw(),字形会换成新字体,换行和基线却还是按回退字体算的。 可靠的做法是等字体就绪再创建文字节点;节点已经存在的话,要强制它重排: // 创建之前:第二个参数传实际要显示的文字,切片字体才会下载到对应的分片 await document.fonts.load('16px "Source Han Sans SC"', content); // 已经创建过的节点:设成相同的值不会触发重排,先换一个值再换回来 const family = text.fontFamily(); text.fontFamily(''); text.fontFamily(family); 另一个后果是布局。回退字体与目标字体的字宽不同,依赖 text.width() 做的定位、 换行、居中计算,在字体切换前后会得到两组不同的结果。如果必须先渲染再替换字体, 记得在字体就绪后重新执行一遍布局计算,而不只是 draw()。 实测数据、为什么纯中文段落看不出问题,以及导出前的注意事项,见中文字体加载。 --- ## Text 路径文字 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/text-path Text 路径文字 路径文字让文本沿任意曲线排列,常用于弧形标题、环绕徽章这类效果。 用法​ 要使用Konva添加文本路径, 我们可以实例化一个Konva.TextPath()对象. 有关属性和方法的完整列表,请参阅Konva.TextPath文档 Konva TextPath Demo
常见问题​ 文字尾部被截断了,没有任何提示?​ TextPath 会沿路径逐字摆放,路径走完就停——多出来的字符直接不画,不报错也不换行。 要么缩短文本,要么加长路径。可以用 path.getLength() 量出路径长度, 和 textPath.getTextWidth() 比较,提前判断是否放得下。 文字上下颠倒了?​ 文字的朝向由路径方向决定。同一条视觉上相同的曲线,点的顺序反过来, 文字就会倒过来贴在另一侧。 不需要改文本或加旋转,把 data 里的路径方向反转即可——例如把一段从左到右的 弧线改写成从右到左。 中文字符在弯曲处间距为什么不均匀?​ 字符是沿路径逐个定位的,曲率大的地方相邻字符的基线方向差异大,视觉间距就会 显得忽宽忽窄。汉字是方块字、字宽一致,这个现象比西文更明显。 用 letterSpacing 做整体微调可以缓解。Konva 10.4.0 起 letterSpacing 按字素 计算,含 emoji 的文本间距行为和以前不同,如果之前调过这个值,升级后要复核。 国内环境注意事项​ 路径文字对字体的敏感度比普通文本更高。Konva.Text 那一页 提到的 Web Font 问题在这里同样存在,而且后果更严重——回退字体的字宽不同, 整段文字沿路径的总长度随之改变,原本刚好排满的文字可能变成排不下而被截断。 所以路径文字务必在 document.fonts.ready 之后再创建或重绘, 不要依赖「先画出来再说」。 另外,如果字体是从公共 CDN 加载的中文字体,体积通常在几 MB 量级, 国内加载时间可能达到数秒。这种场景下更稳妥的做法是只加载用到的字形子集, 或者干脆改用系统字体。 --- ## Wedge 扇形 来源:https://konva-doc-cn.front-end-js.top/docs/shapes/wedge Wedge 扇形 扇形是从圆心张开的一块,饼图就是由一组扇形拼成的。 用法​ 要使用Konva创建一个扇形, 我们可以实例化一个Konva.Wedge()对象. 有关属性和方法的完整列表,请参阅Konva.Wedge文档 Konva Wedge Demo
常见问题​ angle 填的是角度还是弧度?​ 默认是角度(0–360)。这由全局开关 Konva.angleDeg 控制,它默认为 true。 如果项目里有人把 Konva.angleDeg 设成了 false,全站所有图形的角度属性 (包括 rotation)都会改按弧度解释。这是个全局开关,改动它会影响每一处, 排查「角度突然全错了」时值得先确认这个值。 扇形从哪个方向开始张开?​ 默认从 3 点钟方向(正右)开始,顺时针张开 angle 度。用 rotation 调整起始方向—— 画饼图时通常希望从 12 点开始,设 rotation: -90 即可。 每一块的 rotation 等于它之前所有扇形角度之和(再加上起始偏移), 按顺序累加即可,不需要额外的三角计算。 饼图相邻两块之间为什么有一条细缝?​ 这是抗锯齿造成的。两个扇形共享一条边时,各自的边缘像素都只被画了「一半的覆盖率」, 叠加后仍然不足以填满,于是露出底色形成细线。 让相邻扇形的角度略微重叠(例如每块多画 0.5 度)就能消除。另一种做法是给每块 加上与填充同色的 stroke,用描边把缝盖住。 性能提示​ 单个扇形很便宜,但饼图往往由十几块组成,且多数时候是静态的。这种情况下把整张饼图 放进一个 Konva.Group 并对该组调用 cache(),一次渲染成位图,之后每帧只贴一张图, 比逐块重绘划算得多。 需要注意缓存会固定当前外观,任何一块的颜色或角度变化后都要 clearCache() 再重新 cache()。所以带悬停高亮的饼图不适合整体缓存——那种场景下更好的做法是只给 不变的部分做缓存,把会变的那块单独留在外面。 ---