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.

Develop once, deploy everywhere - Making cross-platform development easier