Thứ hai, 22/01/2018 | 00:00 GMT+7

Điều khiển HTML5 Canvas bằng Vue.js


Hầu hết thời gian, bạn sẽ viết các thành phần Vue.js tương tác với một trang web thông qua DOM. Nhưng hệ thống phản ứng của Vue hữu ích cho nhiều hơn thế. Trong ví dụ này, ta sẽ tạo một tập hợp các thành phần để hiển thị một biểu đồ thanh cơ bản với Vue, trong HTML5 canvas.

Cài đặt

Đây là một dự án nâng cao hơn bình thường, nhưng bạn vẫn muốn cài đặt webpack-đơn giản điển hình của bạn hoạt động.

Khi bạn đã hiểu, hãy tiếp tục và thay thế nội dung của thành phần App.vue của bạn App.vue thành phần này. (Bây giờ tôi sẽ không bận tâm giải thích vì nó không phải trọng tâm của bài viết này. Đây chỉ là mẫu ứng dụng và cập nhật các giá trị biểu đồ một cách ngẫu nhiên.)

App.vue
<template>   <div id="app">     <h2>Bar Chart Example</h2>     <!-- These are the custom components we'll create -->     <!-- Values for `my-box` are percentages of the width of the canvas. -->     <!-- Each bar will take up an equal space of the canvas. -->     <my-canvas style="width: 100%; height: 600px;">       <my-box         v-for="obj, index of chartValues"         :x1="((index / chartValues.length) * 100)"         :x2="((index / chartValues.length) * 100) + (100 / chartValues.length)"         :y1="100"         :y2="100 - obj.val"         :color="obj.color"         :value="obj.val"       >       </my-box>     </my-canvas>   </div> </template>  <script> import MyCanvas from './MyCanvas.vue'; import MyBox from './MyBox.vue';  export default {   name: 'app',   components: {     MyCanvas,     MyBox   },    data () {     return {       chartValues: [         {val: 24, color: 'red'},         {val: 32, color: '#0f0'},         {val: 66, color: 'rebeccapurple'},         {val: 1, color: 'green'},         {val: 28, color: 'blue'},         {val: 60, color: 'rgba(150, 100, 0, 0.2)'},       ]     }   },    // Randomly selects a value to randomly increment or decrement every 16 ms.   // Not really important, just demonstrates that reactivity still works.   mounted () {     let dir = 1;     let selectedVal = Math.floor(Math.random() * this.chartValues.length);      setInterval(() => {       if (Math.random() > 0.995) dir *= -1;       if (Math.random() > 0.99) selectedVal = Math.floor(Math.random() * this.chartValues.length);        this.chartValues[selectedVal].val = Math.min(Math.max(this.chartValues[selectedVal].val + dir * 0.5, 0), 100);     }, 16);   } } </script>  <style> html, body {   margin: 0;   padding: 0; }  #app {   position: relative;   height: 100vh;   width: 100vw;   padding: 20px;   box-sizing: border-box; } </style> 

Thành phần Canvas (MyCanvas)

Bản thân thành phần canvas tương đối đơn giản. Nó chỉ đơn giản là tạo một phần tử canvas và đưa ngữ cảnh kết xuất canvas vào tất cả các thành phần con của nó thông qua trình cung cấp phản ứng.

MyCanvas.vue
<template>   <div class="my-canvas-wrapper">     <canvas ref="my-canvas"></canvas>     <slot></slot>   </div> </template>  <script> export default {   data() {     return {       // By creating the provider in the data property, it becomes reactive,       // so child components will update when `context` changes.       provider: {         // This is the CanvasRenderingContext that children will draw to.         context: null       }     }   },    // Allows any child component to `inject: ['provider']` and have access to it.   provide () {     return {       provider: this.provider     }   },    mounted () {     // We can't access the rendering context until the canvas is mounted to the DOM.     // Once we have it, provide it to all child components.     this.provider.context = this.$refs['my-canvas'].getContext('2d')      // Resize the canvas to fit its parent's width.     // Normally you'd use a more flexible resize system.     this.$refs['my-canvas'].width = this.$refs['my-canvas'].parentElement.clientWidth     this.$refs['my-canvas'].height = this.$refs['my-canvas'].parentElement.clientHeight   } } </script> 

Thành phần hộp (MyBox)

MyBox.vue là nơi điều kỳ diệu xảy ra. Nó là một thành phần trừu tượng , không phải là một thành phần “thực”, vì vậy nó không thực sự hiển thị cho DOM. Thay vào đó, trong hàm kết xuất, ta sử dụng lệnh gọi canvas bình thường để vẽ trên canvas được chèn. Do đó, mỗi thành phần vẫn hiển thị khi thuộc tính của chúng thay đổi mà không cần thực hiện thêm bất kỳ thao tác nào.

MyBox.vue
<script> // Note how there's no template or styles in this component.  // Helper functions to convert a percentage of canvas area to pixels. const percentWidthToPix = (percent, ctx) => Math.floor((ctx.canvas.width / 100) * percent) const percentHeightToPix = (percent, ctx) => Math.floor((ctx.canvas.height / 100) * percent)  export default {   // Gets us the provider property from the parent <my-canvas> component.   inject: ['provider'],    props: {     // Start coordinates (percentage of canvas dimensions).     x1: {       type: Number,       default: 0     },     y1: {       type: Number,       default: 0     },      // End coordinates (percentage of canvas dimensions).     x2: {       type: Number,       default: 0     },     y2: {       type: Number,       default: 0     },      // The value to display.     value: {       type: Number,       defualt: 0     },      // The color of the box.     color: {       type: String,       default: '#F00'     }   },    data () {     return {       // We cache the dimensions of the previous       // render so that we can clear the area later.       oldBox: {         x: null,         y: null,         w: null,         h: null       }     }   },    computed: {     calculatedBox () {       const ctx = this.provider.context        // Turn start / end percentages into x, y, width, height in pixels.       const calculated = {         x: percentWidthToPix(this.x1, ctx),         y: percentHeightToPix(this.y1, ctx),         w: percentWidthToPix(this.x2 - this.x1, ctx),         h: percentHeightToPix(this.y2 - this.y1, ctx)       }        // Yes yes, side-effects. This lets us cache the box dimensions of the previous render.       // before we re-calculate calculatedBox the next render.       this.oldBox = calculated       return calculated     }   },    render () {     // Since the parent canvas has to mount first, it's *possible* that the context may not be     // injected by the time this render function runs the first time.     if(!this.provider.context) return;     const ctx = this.provider.context;      // Keep a reference to the box used in the previous render call.     const oldBox = this.oldBox     // Calculate the new box. (Computed properties update on-demand.)     const newBox = this.calculatedBox      ctx.beginPath();     // Clear the old area from the previous render.     ctx.clearRect(oldBox.x, oldBox.y, oldBox.w, oldBox.h);     // Clear the area for the text.     ctx.clearRect(newBox.x, newBox.y - 42, newBox.w, 100);      // Draw the new rectangle.     ctx.rect(newBox.x, newBox.y, newBox.w, newBox.h);     ctx.fillStyle = this.color;     ctx.fill();      // Draw the text     ctx.fillStyle = '#000'     ctx.font = '28px sans-serif';     ctx.textAlign = 'center';     ctx.fillText(Math.floor(this.value), (newBox.x + (newBox.w / 2)), newBox.y - 14)   } } </script> 

Nếu mọi việc suôn sẻ, bạn sẽ kết thúc với một cái gì đó giống như sau:

Biểu đồ thanh HTML5 Canvas được hiển thị với Vue.js.

Kết luận

Phương pháp này được dùng cho bất kỳ loại kết xuất canvas nào hoặc thậm chí là nội dung 3D với WebGL và / hoặc WebVR! Sử dụng trí tưởng tượng của bạn!

Hiện tại, đây là một thách thức. Cố gắng thêm xử lý sự kiện riêng lẻ bằng cách chuyển các kích thước của từng hộp cho nhà cung cấp được đưa vào và để canvas chính quyết định nơi gửi các sự kiện.

Chúc vui vẻ! 🚀


Tags:

Các tin liên quan