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
Performance Optimization
- Batch drawing operations when possible
- Use appropriate canvas size for the device
- Clear only necessary regions of the canvas
- Optimize image operations
Memory Management
- Clean up animation frames when components are destroyed
- Avoid memory leaks in long-running animations
- Properly dispose of canvas contexts
Cross-Platform Compatibility
- Test canvas operations on all target platforms
- Handle platform-specific differences
- Use appropriate APIs for each platform
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.