Skip to content

Canvas Components

Overview

Canvas components in uni-app provide powerful drawing capabilities for creating graphics, animations, and interactive visual elements. These components enable developers to create rich visual experiences across all supported platforms.

canvas Component

Basic Usage

The canvas component provides a drawing surface for 2D graphics operations.

vue
<template>
  <view class="canvas-container">
    <canvas 
      canvas-id="myCanvas" 
      class="canvas"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
    ></canvas>
    
    <view class="controls">
      <button @click="drawRect">Draw Rectangle</button>
      <button @click="drawCircle">Draw Circle</button>
      <button @click="drawText">Draw Text</button>
      <button @click="clearCanvas">Clear Canvas</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      ctx: null,
      isDrawing: false,
      lastX: 0,
      lastY: 0
    }
  },
  
  onReady() {
    this.ctx = uni.createCanvasContext('myCanvas', this)
    this.initCanvas()
  },
  
  methods: {
    initCanvas() {
      // Set canvas background
      this.ctx.setFillStyle('#ffffff')
      this.ctx.fillRect(0, 0, 300, 200)
      this.ctx.draw()
    },
    
    drawRect() {
      this.ctx.setFillStyle('#ff6b6b')
      this.ctx.fillRect(50, 50, 100, 80)
      this.ctx.setStrokeStyle('#333')
      this.ctx.strokeRect(50, 50, 100, 80)
      this.ctx.draw(true)
    },
    
    drawCircle() {
      this.ctx.beginPath()
      this.ctx.arc(200, 90, 40, 0, 2 * Math.PI)
      this.ctx.setFillStyle('#4ecdc4')
      this.ctx.fill()
      this.ctx.setStrokeStyle('#333')
      this.ctx.stroke()
      this.ctx.draw(true)
    },
    
    drawText() {
      this.ctx.setFontSize(20)
      this.ctx.setFillStyle('#333')
      this.ctx.fillText('Hello Canvas!', 50, 160)
      this.ctx.draw(true)
    },
    
    clearCanvas() {
      this.ctx.clearRect(0, 0, 300, 200)
      this.ctx.draw()
      this.initCanvas()
    },
    
    handleTouchStart(e) {
      this.isDrawing = true
      this.lastX = e.touches[0].x
      this.lastY = e.touches[0].y
    },
    
    handleTouchMove(e) {
      if (!this.isDrawing) return
      
      const currentX = e.touches[0].x
      const currentY = e.touches[0].y
      
      this.ctx.beginPath()
      this.ctx.moveTo(this.lastX, this.lastY)
      this.ctx.lineTo(currentX, currentY)
      this.ctx.setStrokeStyle('#333')
      this.ctx.setLineWidth(2)
      this.ctx.stroke()
      this.ctx.draw(true)
      
      this.lastX = currentX
      this.lastY = currentY
    },
    
    handleTouchEnd() {
      this.isDrawing = false
    }
  }
}
</script>

<style>
.canvas-container {
  padding: 20px;
  text-align: center;
}

.canvas {
  width: 300px;
  height: 200px;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}

.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}

.controls button {
  padding: 8px 16px;
  font-size: 14px;
}
</style>

Advanced Canvas Operations

Drawing Shapes and Paths

vue
<template>
  <view class="advanced-canvas">
    <canvas canvas-id="advancedCanvas" class="canvas"></canvas>
    
    <view class="shape-controls">
      <button @click="drawComplexShape">Complex Shape</button>
      <button @click="drawGradient">Gradient</button>
      <button @click="drawPattern">Pattern</button>
      <button @click="animateShape">Animate</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      ctx: null,
      animationId: null
    }
  },
  
  onReady() {
    this.ctx = uni.createCanvasContext('advancedCanvas', this)
    this.setupCanvas()
  },
  
  methods: {
    setupCanvas() {
      this.ctx.setFillStyle('#f8f9fa')
      this.ctx.fillRect(0, 0, 350, 250)
      this.ctx.draw()
    },
    
    drawComplexShape() {
      // Draw a star shape
      const centerX = 175
      const centerY = 125
      const outerRadius = 50
      const innerRadius = 25
      const spikes = 5
      
      this.ctx.beginPath()
      
      for (let i = 0; i < spikes * 2; i++) {
        const radius = i % 2 === 0 ? outerRadius : innerRadius
        const angle = (i * Math.PI) / spikes
        const x = centerX + Math.cos(angle) * radius
        const y = centerY + Math.sin(angle) * radius
        
        if (i === 0) {
          this.ctx.moveTo(x, y)
        } else {
          this.ctx.lineTo(x, y)
        }
      }
      
      this.ctx.closePath()
      this.ctx.setFillStyle('#ffd93d')
      this.ctx.fill()
      this.ctx.setStrokeStyle('#333')
      this.ctx.setLineWidth(2)
      this.ctx.stroke()
      this.ctx.draw(true)
    },
    
    drawGradient() {
      // Create linear gradient
      const gradient = this.ctx.createLinearGradient(50, 50, 150, 150)
      gradient.addColorStop(0, '#ff6b6b')
      gradient.addColorStop(0.5, '#4ecdc4')
      gradient.addColorStop(1, '#45b7d1')
      
      this.ctx.setFillStyle(gradient)
      this.ctx.fillRect(50, 50, 100, 100)
      this.ctx.draw(true)
    },
    
    drawPattern() {
      // Draw a checkered pattern
      const squareSize = 20
      const rows = 8
      const cols = 10
      
      for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
          const x = col * squareSize + 75
          const y = row * squareSize + 50
          
          if ((row + col) % 2 === 0) {
            this.ctx.setFillStyle('#333')
          } else {
            this.ctx.setFillStyle('#fff')
          }
          
          this.ctx.fillRect(x, y, squareSize, squareSize)
        }
      }
      
      this.ctx.draw(true)
    },
    
    animateShape() {
      let angle = 0
      const centerX = 175
      const centerY = 125
      const radius = 30
      
      const animate = () => {
        // Clear canvas
        this.ctx.clearRect(0, 0, 350, 250)
        this.ctx.setFillStyle('#f8f9fa')
        this.ctx.fillRect(0, 0, 350, 250)
        
        // Calculate position
        const x = centerX + Math.cos(angle) * radius
        const y = centerY + Math.sin(angle) * radius
        
        // Draw animated circle
        this.ctx.beginPath()
        this.ctx.arc(x, y, 15, 0, 2 * Math.PI)
        this.ctx.setFillStyle('#ff6b6b')
        this.ctx.fill()
        this.ctx.draw()
        
        angle += 0.1
        
        // Continue animation
        this.animationId = requestAnimationFrame(animate)
      }
      
      // Stop previous animation
      if (this.animationId) {
        cancelAnimationFrame(this.animationId)
      }
      
      animate()
      
      // Stop animation after 5 seconds
      setTimeout(() => {
        if (this.animationId) {
          cancelAnimationFrame(this.animationId)
          this.animationId = null
        }
      }, 5000)
    }
  },
  
  beforeDestroy() {
    if (this.animationId) {
      cancelAnimationFrame(this.animationId)
    }
  }
}
</script>

<style>
.advanced-canvas {
  padding: 20px;
  text-align: center;
}

.canvas {
  width: 350px;
  height: 250px;
  border: 1px solid #ddd;
  margin-bottom: 20px;
}

.shape-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}
</style>

Canvas Image Operations

Loading and Manipulating Images

vue
<template>
  <view class="image-canvas">
    <canvas canvas-id="imageCanvas" class="canvas"></canvas>
    
    <view class="image-controls">
      <button @click="loadImage">Load Image</button>
      <button @click="applyFilter">Apply Filter</button>
      <button @click="cropImage">Crop Image</button>
      <button @click="saveImage">Save Image</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      ctx: null,
      imageLoaded: false
    }
  },
  
  onReady() {
    this.ctx = uni.createCanvasContext('imageCanvas', this)
  },
  
  methods: {
    loadImage() {
      // Choose image from gallery
      uni.chooseImage({
        count: 1,
        success: (res) => {
          const imagePath = res.tempFilePaths[0]
          
          // Draw image on canvas
          this.ctx.drawImage(imagePath, 0, 0, 300, 200)
          this.ctx.draw()
          this.imageLoaded = true
        }
      })
    },
    
    applyFilter() {
      if (!this.imageLoaded) {
        uni.showToast({
          title: 'Please load an image first',
          icon: 'none'
        })
        return
      }
      
      // Get image data
      uni.canvasGetImageData({
        canvasId: 'imageCanvas',
        x: 0,
        y: 0,
        width: 300,
        height: 200,
        success: (res) => {
          const imageData = res.data
          
          // Apply grayscale filter
          for (let i = 0; i < imageData.length; i += 4) {
            const gray = imageData[i] * 0.299 + imageData[i + 1] * 0.587 + imageData[i + 2] * 0.114
            imageData[i] = gray     // Red
            imageData[i + 1] = gray // Green
            imageData[i + 2] = gray // Blue
            // Alpha channel (imageData[i + 3]) remains unchanged
          }
          
          // Put modified image data back
          uni.canvasPutImageData({
            canvasId: 'imageCanvas',
            data: imageData,
            x: 0,
            y: 0,
            width: 300,
            height: 200,
            success: () => {
              console.log('Filter applied successfully')
            }
          }, this)
        }
      }, this)
    },
    
    cropImage() {
      if (!this.imageLoaded) {
        uni.showToast({
          title: 'Please load an image first',
          icon: 'none'
        })
        return
      }
      
      // Clear canvas and redraw cropped portion
      this.ctx.clearRect(0, 0, 300, 200)
      
      // Draw cropped image (center crop)
      const cropX = 50
      const cropY = 25
      const cropWidth = 200
      const cropHeight = 150
      
      this.ctx.drawImage(
        '/static/temp-image.jpg', // You would use the actual image path
        cropX, cropY, cropWidth, cropHeight,
        0, 0, 300, 200
      )
      this.ctx.draw()
    },
    
    saveImage() {
      if (!this.imageLoaded) {
        uni.showToast({
          title: 'Please load an image first',
          icon: 'none'
        })
        return
      }
      
      // Convert canvas to image
      uni.canvasToTempFilePath({
        canvasId: 'imageCanvas',
        success: (res) => {
          // Save to photo album
          uni.saveImageToPhotosAlbum({
            filePath: res.tempFilePath,
            success: () => {
              uni.showToast({
                title: 'Image saved successfully',
                icon: 'success'
              })
            },
            fail: (err) => {
              console.error('Failed to save image:', err)
              uni.showToast({
                title: 'Failed to save image',
                icon: 'none'
              })
            }
          })
        },
        fail: (err) => {
          console.error('Failed to convert canvas:', err)
        }
      }, this)
    }
  }
}
</script>

<style>
.image-canvas {
  padding: 20px;
  text-align: center;
}

.canvas {
  width: 300px;
  height: 200px;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}

.image-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}
</style>

Canvas Performance Optimization

Best Practices for Canvas Performance

javascript
// Canvas performance optimization utilities
export const CanvasOptimization = {
  // Batch drawing operations
  batchDraw(ctx, operations) {
    operations.forEach(operation => {
      operation(ctx)
    })
    ctx.draw(true)
  },
  
  // Use off-screen canvas for complex operations
  createOffscreenCanvas(width, height) {
    // This would be implemented differently on different platforms
    return {
      width,
      height,
      getContext: () => {
        // Return appropriate context
      }
    }
  },
  
  // Optimize image drawing
  optimizeImageDraw(ctx, imagePath, dx, dy, dWidth, dHeight) {
    // Pre-scale images if needed
    const scaledWidth = dWidth
    const scaledHeight = dHeight
    
    ctx.drawImage(imagePath, dx, dy, scaledWidth, scaledHeight)
  },
  
  // Debounce canvas updates
  debounceCanvasUpdate(func, delay) {
    let timeoutId
    return function(...args) {
      clearTimeout(timeoutId)
      timeoutId = setTimeout(() => func.apply(this, args), delay)
    }
  },
  
  // Clear specific regions instead of entire canvas
  clearRegion(ctx, x, y, width, height) {
    ctx.clearRect(x, y, width, height)
  }
}

Platform-Specific Considerations

WeChat Mini Program Canvas

javascript
// WeChat Mini Program specific canvas features
export const WeChatCanvasFeatures = {
  // Use Canvas 2D API (newer approach)
  createCanvas2D(canvasId, component) {
    return new Promise((resolve) => {
      const query = uni.createSelectorQuery().in(component)
      query.select(`#${canvasId}`)
        .fields({ node: true, size: true })
        .exec((res) => {
          const canvas = res[0].node
          const ctx = canvas.getContext('2d')
          
          // Set canvas size
          const dpr = uni.getSystemInfoSync().pixelRatio
          canvas.width = res[0].width * dpr
          canvas.height = res[0].height * dpr
          ctx.scale(dpr, dpr)
          
          resolve({ canvas, ctx })
        })
    })
  },
  
  // Handle high DPI displays
  setupHighDPI(canvas, ctx) {
    const dpr = uni.getSystemInfoSync().pixelRatio
    const rect = canvas.getBoundingClientRect()
    
    canvas.width = rect.width * dpr
    canvas.height = rect.height * dpr
    
    ctx.scale(dpr, dpr)
    
    canvas.style.width = rect.width + 'px'
    canvas.style.height = rect.height + 'px'
  }
}

App Platform Canvas

javascript
// App platform specific canvas optimizations
export const AppCanvasFeatures = {
  // Use native canvas acceleration
  enableHardwareAcceleration(canvasId) {
    // #ifdef APP-PLUS
    const canvas = plus.webview.currentWebview().getStyle()
    canvas.hardwareAccelerated = true
    // #endif
  },
  
  // Handle canvas in background
  handleBackgroundCanvas() {
    // #ifdef APP-PLUS
    plus.globalEvent.addEventListener('pause', () => {
      // Pause canvas animations
      this.pauseAnimations()
    })
    
    plus.globalEvent.addEventListener('resume', () => {
      // Resume canvas animations
      this.resumeAnimations()
    })
    // #endif
  }
}

Canvas Animation Framework

Simple Animation System

javascript
// Canvas animation framework
export class CanvasAnimator {
  constructor(ctx) {
    this.ctx = ctx
    this.animations = []
    this.isRunning = false
    this.lastTime = 0
  }
  
  // Add animation
  addAnimation(animation) {
    this.animations.push(animation)
  }
  
  // Remove animation
  removeAnimation(animation) {
    const index = this.animations.indexOf(animation)
    if (index > -1) {
      this.animations.splice(index, 1)
    }
  }
  
  // Start animation loop
  start() {
    if (this.isRunning) return
    
    this.isRunning = true
    this.lastTime = Date.now()
    this.animate()
  }
  
  // Stop animation loop
  stop() {
    this.isRunning = false
  }
  
  // Animation loop
  animate() {
    if (!this.isRunning) return
    
    const currentTime = Date.now()
    const deltaTime = currentTime - this.lastTime
    this.lastTime = currentTime
    
    // Clear canvas
    this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
    
    // Update and draw animations
    this.animations.forEach(animation => {
      animation.update(deltaTime)
      animation.draw(this.ctx)
    })
    
    this.ctx.draw()
    
    // Continue animation
    requestAnimationFrame(() => this.animate())
  }
}

// Example animation class
export class BouncingBall {
  constructor(x, y, radius, color) {
    this.x = x
    this.y = y
    this.radius = radius
    this.color = color
    this.vx = Math.random() * 4 - 2
    this.vy = Math.random() * 4 - 2
    this.canvasWidth = 300
    this.canvasHeight = 200
  }
  
  update(deltaTime) {
    this.x += this.vx
    this.y += this.vy
    
    // Bounce off walls
    if (this.x + this.radius > this.canvasWidth || this.x - this.radius < 0) {
      this.vx = -this.vx
    }
    if (this.y + this.radius > this.canvasHeight || this.y - this.radius < 0) {
      this.vy = -this.vy
    }
  }
  
  draw(ctx) {
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
    ctx.setFillStyle(this.color)
    ctx.fill()
  }
}

Best Practices

  1. Performance Optimization

    • Batch drawing operations when possible
    • Use appropriate canvas size for the device
    • Clear only necessary regions of the canvas
    • Optimize image operations
  2. Memory Management

    • Clean up animation frames when components are destroyed
    • Avoid memory leaks in long-running animations
    • Properly dispose of canvas contexts
  3. Cross-Platform Compatibility

    • Test canvas operations on all target platforms
    • Handle platform-specific differences
    • Use appropriate APIs for each platform
  4. User Experience

    • Provide loading states for complex operations
    • Handle touch interactions smoothly
    • Optimize for different screen sizes and densities

Summary

Canvas components in uni-app provide powerful capabilities for creating rich visual experiences. By understanding the various drawing operations, optimization techniques, and platform-specific considerations, developers can create engaging graphics and animations that work seamlessly across all supported platforms.

一次开发,多端部署 - 让跨平台开发更简单