Vue与Konva:解锁Canvas绘图的无限可能

前言

在现代Web开发中,动态、交互式的图形界面已成为提升用户体验的关键要素。Vue.js,作为一款轻量级且高效的前端框架,凭借其响应式数据绑定和组件化开发模式,赢得了众多开发者的青睐。而当Vue.js邂逅Konva.js,两者结合产生的化学反应更是令人惊叹。今天,就让我们一同深入探索如何利用Vue.js和Konva.js绘制复杂且交互丰富的Canvas图形。


一、Vue Konva简介

Vue Konva是一款基于Vue.js的JavaScript库,它为开发者提供了声明式和响应式的绑定方式,使得在Vue中使用Konva框架变得异常简单。通过Vue Konva,我们可以轻松地在Vue组件中绘制各种复杂的Canvas图形,并且能够实现图形的动态更新和交互。

二、快速上手

  1. 安装
    首先,确保你的项目中已经安装了Vue.js 2.4+版本。接下来,通过npm安装vue-konva和konva:
    对于Vue 3
bash 复制代码
npm install vue-konva konva --save

对于Vue 2

bash 复制代码
npm install vue-konva@2 konva --save
  1. 引入和使用VueKonva
    在Vue 3中,你需要在main.js中引入VueKonva并使用它:
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');

在Vue 2中,引入和使用VueKonva的方式如下:

js 复制代码
import Vue from 'vue';
import VueKonva from 'vue-konva';

Vue.use(VueKonva);
  1. 在组件模板中引用
    在你的Vue组件中,你可以通过以下方式使用Vue Konva的组件:
html 复制代码
<template>
  <v-stage :config="configKonva">
    <v-layer>
      <v-circle :config="configCircle"></v-circle>
    </v-layer>
  </v-stage>
</template>

<script>
export default {
  data() {
    return {
      configKonva: {
        width: 200,
        height: 200
      },
      configCircle: {
        x: 100,
        y: 100,
        radius: 70,
        fill: "red",
        stroke: "black",
        strokeWidth: 4
      }
    };
  }
};
</script>

三、绘制基础图形

Vue Konva提供了与Konva框架相同名称的组件,前缀为v-。你可以通过config属性传递参数来配置这些组件。以下是一些核心形状的示例:

  • v-rect(矩形)
  • v-circle(圆形)
  • v-ellipse(椭圆)
  • v-line(线条)
  • v-image(图片)
  • v-text(文本)
  • v-text-path(文本路径)
  • v-star(星形)
  • v-label(标签)
  • v-path(路径)
  • v-regular-polygon(正多边形)
html 复制代码
<template>
  <div>
    <v-stage ref="stage" :config="stageSize">
      <v-layer>
        <v-text :config="{text: 'Some text on canvas', fontSize: 15}"/>
        <v-rect :config="{
            x: 20,
            y: 50,
            width: 100,
            height: 100,
            fill: 'red',
            shadowBlur: 10
          }"
        />
        <v-circle :config="{
            x: 200,
            y: 100,
            radius: 50,
            fill: 'green'
          }"
        />
        <v-line :config="{
          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']
        }"/>
      </v-layer>
      <v-layer ref="dragLayer"></v-layer>
    </v-stage>
  </div>
</template>

<script>
const width = window.innerWidth;
const height = window.innerHeight;
export default {
  data() {
    return {
      stageSize: {
        width: width,
        height: height
      }
    };
  }
};
</script>

四、自定义形状

除了基础形状,Vue Konva还允许你创建自定义形状。通过使用v-shape组件,你可以定义一个绘图函数,该函数接收一个Konva.Canvas渲染器,你可以利用它访问HTML5 Canvas上下文,并使用特殊方法如context.fillStrokeShape(shape)来自动处理填充、描边和阴影效果。

html 复制代码
<template>
    <v-stage ref="stage" :config="stageSize">
      <v-layer>
        <v-shape :config="{
        width: 260,
        height: 170,
        sceneFunc: function (context, shape) {
          const width = shape.width();
          const height = shape.height();
          context.beginPath();
          context.moveTo(0, 0);
          context.lineTo(width - 40, height - 90);
          context.quadraticCurveTo(width - 110, height - 70, width, height);
          context.closePath();

          // (!) Konva特定方法,非常重要
          context.fillStrokeShape(shape);
        },
        fill: '#00D2FF',
        stroke: 'black',
        strokeWidth: 4
      }"/>
    </v-layer>
  </v-stage>
</template>

<script>
const width = window.innerWidth;
const height = window.innerHeight;

export default {
  data() {
    return {
      stageSize: {
        width: width,
        height: height
      }
    };
  }
};
</script>

五、事件监听

Vue Konva使得监听用户输入事件(如click、dblclick、mouseover、tap、dbltap、touchstart等)和拖拽事件(如dragstart、dragmove、dragend)变得非常简单。你可以通过在组件上添加事件监听器来实现这些功能。


html 复制代码
<template>
<v-stage ref="stage" :config="stageSize">
      <v-layer ref="layer">
        <v-regular-polygon
          @mousemove="handleMouseMove"
          @mouseout="handleMouseOut"
          :config="{
            x: 80,
            y: 120,
            sides: 3,
            radius: 80,
            fill: '#00D2FF',
            stroke: 'black',
            strokeWidth: 4
          }"
        />
        <v-text ref="text" :config="{
          x: 10,
          y: 10,
          fontFamily: 'Calibri',
          fontSize: 24,
          text: text,
          fill: 'black'
        }" />
      </v-layer>
    </v-stage>
</template>

<script>
const width = window.innerWidth;
const height = window.innerHeight;

export default {
  data() {
    return {
      stageSize: {
        width: width,
        height: height
      },
      text: ''
    };
  },
  methods: {
    writeMessage(message) {
      this.text = message;
    },
    handleMouseOut(event) {
      this.writeMessage('Mouseout triangle');
    },
    handleMouseMove(event) {
      const mousePos = this.$refs.stage.getNode().getPointerPosition();
      const x = mousePos.x - 190;
      const y = mousePos.y - 40;
      this.writeMessage('x: ' + x + ', y: ' + y);
    }
  }
};
</script>

六、绘制图片

在Vue Konva中,绘制图片需要手动创建一个原生的window.Image实例或canvas元素,并将其作为v-image组件的image属性。

html 复制代码
<template>
  <v-stage ref="stage" :config="stageSize">
    <v-layer ref="layer">
      <v-image :config="{
            image: image
          }"/>
    </v-layer>
  </v-stage>
</template>

<script>
const width = window.innerWidth;
const height = window.innerHeight;

export default {
  data() {
    return {
      stageSize: {
        width: width,
        height: height
      },
      image: null
    };
  },
  created() {
    const image = new window.Image();
    image.src = "https://konvajs.org/assets/yoda.jpg";
    image.onload = () => {
      // 图片加载完成后设置
      this.image = image;
    };
  }
};
</script>

七、应用滤镜

Vue Konva允许你为图形应用各种滤镜效果。你需要手动缓存Konva.Node,并在需要时重新缓存节点。

html 复制代码
<template>
<v-stage ref="stage" :config="stageSize">
      <v-layer ref="layer">
        <v-rect
          ref="rect"
          @mousemove="handleMouseMove"
          :config="{
            filters: filters,
            noise: 1,
            x: 10,
            y: 10,
            width: 50,
            height: 50,
            fill: color,
            shadowBlur: 10
          }"
        />
      </v-layer>
    </v-stage>
</template>

<script>
const width = window.innerWidth;
const height = window.innerHeight;
import Konva from 'konva';

export default {
  data() {
    return {
      stageSize: {
        width: width,
        height: height
      },
      color: 'green',
      filters: [Konva.Filters.Noise]
    };
  },
  methods: {
    handleMouseMove() {
      this.color = Konva.Util.getRandomColor();
      // 重新缓存
      const rectNode = this.$refs.rect.getNode();
      // 可能需要手动重绘图层
      rectNode.cache();
    }
  },
  mounted() {
    const rectNode = this.$refs.rect.getNode();
    rectNode.cache();
  },
};
</script>

八、保存和加载Canvas

Vue Konva提供了简单的方法来保存和加载Canvas状态。你可以通过保存应用的状态来实现这一点,而不需要保存Konva内部的节点。

html 复制代码
<template>
  <div>
    点击Canvas创建一个圆。
    <a href=".">刷新页面</a>。圆应该仍然在这里。
    <v-stage ref="stage"
      :config="stageSize"
      @click="handleClick"
    >
      <v-layer ref="layer">
        <v-circle
          v-for="item in list"
          :key="item.id"
          :config="item"></v-circle>
      </v-layer>
      <v-layer ref="dragLayer"></v-layer>
    </v-stage>
  </div>
</template>

<script>
const width = window.innerWidth;
const height = window.innerHeight;

export default {
  data() {
    return {
      list: [{ x: 100, y: 100, radius: 50, fill: 'blue' }],
      stageSize: {
        width: width,
        height: height
      }
    };
  },
  methods: {
    handleClick(evt) {
      const stage = evt.target.getStage();
      const pos = stage.getPointerPosition();
      this.list.push({
        radius: 50,
        fill: 'red',
        ...pos
      });

      this.save();
    },

    load() {
      const data = localStorage.getItem('storage');
      if (data) this.list = JSON.parse(data);
    },

    save() {
      localStorage.setItem('storage', JSON.stringify(this.list));
    }
  },
  mounted() {
    this.load();
  }
};
</script>

九、拖拽功能

Vue Konva使得实现图形的拖拽功能变得非常简单。你只需要在组件中添加draggable: true属性即可。

html 复制代码
<template>
<v-stage ref="stage" :config="stageSize">
      <v-layer ref="layer">
        <v-text
          @dragstart="handleDragStart"
          @dragend="handleDragEnd"
          :config="{
            text: 'Draggable Text',
            x: 50,
            y: 50,
            draggable: true,
            fill: isDragging ? 'green' : 'black'
          }"
        />
      </v-layer>
    </v-stage>
</template>

<script>
const width = window.innerWidth;
const height = window.innerHeight;

export default {
  data() {
    return {
      stageSize: {
        width: width,
        height: height
      },
      isDragging: false
    };
  },
  methods: {
    handleDragStart() {
      this.isDragging = true;
    },
    handleDragEnd() {
      this.isDragging = false;
    }
  }
};
</script>

十、调整大小和旋转

虽然Vue Konva没有提供纯声明式的"Vue方式"来使用Transformer工具,但你仍然可以通过手动操作Konva节点来实现这一功能。

html 复制代码
<template>
  <v-stage
    ref="stage"
    :config="stageSize"
    @mousedown="handleStageMouseDown"
    @touchstart="handleStageMouseDown"
  >
    <v-layer ref="layer">
      <v-rect
        v-for="item in rectangles"
        :key="item.id"
        :config="item"
        @transformend="handleTransformEnd"
      />
      <v-transformer ref="transformer" />
    </v-layer>
  </v-stage>
</template>

<script>
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;

export default {
  data() {
    return {
      stageSize: {
        width: width,
        height: height,
      },
      rectangles: [
        {
          rotation: 0,
          x: 10,
          y: 10,
          width: 100,
          height: 100,
          scaleX: 1,
          scaleY: 1,
          fill: 'red',
          name: 'rect1',
          draggable: true,
        },
        {
          rotation: 0,
          x: 150,
          y: 150,
          width: 100,
          height: 100,
          scaleX: 1,
          scaleY: 1,
          fill: 'green',
          name: 'rect2',
          draggable: true,
        },
      ],
      selectedShapeName: '',
    };
  },
  methods: {
    handleTransformEnd(e) {
      // 形状被变换,保存新的属性回节点
      // 在我们的状态中找到元素
      const rect = this.rectangles.find(
        (r) => r.name === this.selectedShapeName
      );
      // 更新状态
      rect.x = e.target.x();
      rect.y = e.target.y();
      rect.rotation = e.target.rotation();
      rect.scaleX = e.target.scaleX();
      rect.scaleY = e.target.scaleY();

      // 更改填充色
      rect.fill = Konva.Util.getRandomColor();
    },
    handleStageMouseDown(e) {
      // 点击舞台 - 清除选择
      if (e.target === e.target.getStage()) {
        this.selectedShapeName = '';
        this.updateTransformer();
        return;
      }

      // 点击变换器 - 什么都不做
      const clickedOnTransformer =
        e.target.getParent().className === 'Transformer';
      if (clickedOnTransformer) {
        return;
      }

      // 通过名称找到被点击的矩形
      const name = e.target.name();
      const rect = this.rectangles.find((r) => r.name === name);
      if (rect) {
        this.selectedShapeName = name;
      } else {
        this.selectedShapeName = '';
      }
      this.updateTransformer();
    },
    updateTransformer() {
      // 手动附加或分离变换器节点
      const transformerNode = this.$refs.transformer.getNode();
      const stage = transformerNode.getStage();
      const { selectedShapeName } = this;

      const selectedNode = stage.findOne('.' + selectedShapeName);
      // 如果选定节点已经附加,则什么都不做
      if (selectedNode === transformerNode.node()) {
        return;
      }

      if (selectedNode) {
        // 附加到另一个节点
        transformerNode.nodes([selectedNode]);
      } else {
        // 移除变换器
        transformerNode.nodes([]);
      }
    },
  },
};
</script>

十一、动画效果

Vue Konva允许你为图形添加动画效果。你可以使用Konva的Tween和Animation方法来实现这一点。

html 复制代码
<template>
  <v-stage ref="stage" :config="stageSize">
    <v-layer ref="layer">
      <v-rect
        ref="rect"
        @dragstart="changeSize"
        @dragend="changeSize"
        :config="{
            width: 50,
            height: 50,
            fill: 'green',
            draggable: true
          }"
      />
      <v-regular-polygon
        ref="hexagon"
        :config="{
          x: 200,
          y: 200,
          sides: 6,
          radius: 20,
          fill: 'red',
          stroke: 'black',
          strokeWidth: 4
        }"
      />
    </v-layer>
  </v-stage>
</template>

<script>
import Konva from "konva";
const width = window.innerWidth;
const height = window.innerHeight;

export default {
  data() {
    return {
      stageSize: {
        width: width,
        height: height
      }
    };
  },
  methods: {
    changeSize(e) {
      // to()是Konva.Node实例的方法
      e.target.to({
        scaleX: Math.random() + 0.8,
        scaleY: Math.random() + 0.8,
        duration: 0.2
      });
    }
  },
  mounted() {
    const vm = this;
    const amplitude = 100;
    const period = 5000;
    // in ms
    const centerX = vm.$refs.stage.getNode().getWidth() / 2;

    const hexagon = this.$refs.hexagon.getNode();

    // Konva.Animation示例
    const anim = new Konva.Animation(function(frame) {
      hexagon.setX(
        amplitude * Math.sin((frame.time * 2 * Math.PI) / period) + centerX
      );
    }, hexagon.getLayer());

    anim.start();
  }
};
</script>

十二、缓存图形

在Vue Konva中,你可以通过访问Konva节点并使用node.cache()函数来缓存节点。

html 复制代码
<template>
  <div>
    <v-stage ref="stage" :config="stageConfig">
      <v-layer ref="layer">
        <v-group ref="group">
          <v-star
            v-for="item in list"
            :key="item.id"
            :config="{
            x: item.x,
            y: item.y,
            rotation: item.rotation,
            id: item.id,
            numPoints: 5,
            innerRadius: 30,
            outerRadius: 50, fill: '#89b717',
            opacity: 0.8,
            shadowColor: 'black',
            shadowBlur: 10,
            shadowOpacity: 0.6,
            scaleX: item.scale,
            scaleY: item.scale,
          }"
          />
        </v-group>
      </v-layer>
    </v-stage>
    <div class="cache">
      <input type="checkbox" @change="handleCacheChange"> 缓存图形
    </div>
  </div>
</template>

<script>
const width = window.innerWidth;
const height = window.innerHeight;
export default {
  data() {
    return {
      list: [],
      dragItemId: null,
      stageConfig: {
        width: width,
        height: height,
        draggable: true
      }
    };
  },
  methods: {
    handleCacheChange(e) {
      const shouldCache = e.target.checked;
      if (shouldCache) {
        this.$refs.group.getNode().cache();
      } else {
        this.$refs.group.getNode().clearCache();
      }
    }
  },
  mounted() {
    for (let n = 0; n < 300; n++) {
      this.list.push({
        id: Math.round(Math.random() * 10000).toString(),
        x: Math.random() * width,
        y: Math.random() * height,
        rotation: Math.random() * 180,
        scale: Math.random()
      });
    }
  }
};
</script>

<style>
body {
  margin: 0;
  padding: 0;
}

.cache {
  position: absolute;
  top: 0;
  left: 0;
}
</style>

十三、改变zIndex和组件排序

在Vue Konva中,不推荐直接使用zIndex来改变组件的堆叠顺序。相反,你应该通过更新应用的数据来确保组件在模板中的顺序正确。

html 复制代码
<template>
  <div>
    <v-stage ref="stage" :config="configKonva">
      <v-layer ref="layer">
        <v-circle
          v-for="item in items"
          :key="item.id"
          :config="item"
          @dragstart="handleDragstart"
          @dragend="handleDragend"
        ></v-circle>
      </v-layer>
    </v-stage>
  </div>
</template>

<script>
import Konva from "konva";
const width = window.innerWidth;
const height = window.innerHeight;

function generateItems() {
  const items = [];
  for (let i = 0; i < 10; i++) {
    items.push({
      x: Math.random() * width,
      y: Math.random() * height,
      radius: 50,
      id: "node-" + i,
      fill: Konva.Util.getRandomColor(),
      draggable: true,
    });
  }
  return items;
}

export default {
  data() {
    return {
      items: [],
      dragItemId: null,
      configKonva: {
        width: width,
        height: height,
      },
    };
  },
  methods: {
    handleDragstart(e) {
      // 保存拖动元素:
      this.dragItemId = e.target.id();
      // 通过重新排列items数组,将当前元素移动到顶部:
      const item = this.items.find((i) => i.id === this.dragItemId);
      const index = this.items.indexOf(item);
      this.items.splice(index, 1);
      this.items.push(item);
    },
    handleDragend(e) {
      this.dragItemId = null;
    },
  },
  mounted() {
    this.items = generateItems();
  },
};
</script>

<style>
body {
  margin: 0;
  padding: 0;
}
</style>

总结

通过以上内容,我们全面地介绍了Vue Konva的各种功能和用法。从基础图形的绘制到复杂的动画效果,Vue Konva都提供了强大的支持。希望这篇博客能够帮助你更好地理解和使用Vue Konva,为你的Web应用增添更多的交互性和视觉效果。

相关推荐
翻滚吧键盘几秒前
js代码09
开发语言·javascript·ecmascript
万少31 分钟前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL36 分钟前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl021 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang1 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景1 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼1 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿1 小时前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再1 小时前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
jingling5551 小时前
面试版-前端开发核心知识
开发语言·前端·javascript·vue.js·面试·前端框架