Canvas from Scratch: Frame Animation, a Vibe-Coded Shooter, and the Declarative Power of ECharts
HTML5 Canvas Basics: From Frame-by-Frame Animation to ECharts Data Visualization
This time, let's turn our attention to the foundation of front-end drawing: the Canvas tag. Canvas is a very important element in HTML5, providing an area for drawing graphics via JavaScript.
Why Front-End Needs Canvas
Before understanding <canvas>, my understanding of front-end "drawing" was relatively simple: HTML builds the structure, CSS handles styling, and JavaScript manages interactions. But <canvas> provides a different capability — directly performing bitmap drawing within the browser.
Typical application scenarios for Canvas fall into three main categories:
- Data visualization (charts, dashboards)
- Web games (2D / 3D)
- Cool interactive pages
Canvas itself only provides a "canvas"; the actual drawing is done by the JavaScript Canvas API. In other words, <canvas> is the stage, and JavaScript is the actor and director.
The Canvas Tag: First, Get a Canvas
Using the <canvas> tag opens up a bitmap drawing area on the page. The width and height attributes determine the actual pixel dimensions of the canvas, not the CSS display size. If these attributes are not set, the default is 300×150.
Browsers that don't support Canvas will display the fallback text inside the tag, similar to the fallback mechanism of the <video> tag.
<canvas id="myCanvas" width="400" height="400" style="border: 1px solid #333;">
Your browser does not support the canvas tag (old IE will show this text).
</canvas>
First place <canvas>, then use <script> to get it and draw.
Getting the 2D Context: The Entry Point for All Drawing
Once the Canvas tag is ready, the next step is to get its "context object." All subsequent drawing operations are done through this object.
// Canvas element, the tag is just the beginning
const canvas = document.querySelector('#myCanvas');
// Drawing context object
const ctx = canvas.getContext('2d');
Two key points I noted:
getContext('2d')returns the 2D drawing context, which is our main battlefield for this lesson.- Using
getContext('webgl')enters the 3D context, invoking GPU capabilities.
A quick note on 3D: For AI gaming directions, keep an eye on the three.js framework. With the rise of large physics models, three.js has good prospects in 3D interaction and visualization. Although we won't dive into 3D in this lesson, three.js is an important clue if you later go into gaming or 3D visualization.
Common Drawing APIs: Rectangles, Colors, and Lines
In Canvas's 2D API, rectangles are the most basic shapes. Here are three methods:
fillRect(x, y, width, height): Fills a rectanglestrokeRect(x, y, width, height): Strokes a rectangleclearRect(x, y, width, height): Clears pixels in a rectangular area
And three commonly used properties:
fillStyle: Fill colorstrokeStyle: Stroke colorlineWidth: Stroke width
1.html strings these six APIs together, with complete comments:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML5 Cool New Features - Canvas</title>
</head>
<body>
<!-- The width/height attributes are the actual pixel dimensions of the canvas -->
<canvas id="myCanvas" width="400" height="400"
style="border: 1px solid #333;">
Your browser does not support the canvas tag (old IE will show this text).
</canvas>
<script>
// Canvas element, the tag is just the beginning
const canvas = document.querySelector('#myCanvas');
// Drawing context object
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';// Fill color
ctx.fillRect(10, 10, 100, 100);// rect: rectangle
ctx.strokeStyle = 'blue';// Border color
ctx.lineWidth = 5;// Border width
ctx.strokeRect(10, 10, 100, 100);// strokeRect: bordered rectangle
ctx.clearRect(10, 10, 100, 100);// clearRect: clear rectangular area
</script>
</body>
</html>
I clearly remember the effect of this code: first draw a red solid rectangle, then draw a blue border around it, and finally use clearRect to erase that area. clearRect will be very important in the frame animation later.
Frame Animation: Erase First, Then Redraw
After covering static drawing, the next topic is animation. The essence of frame-by-frame animation is simple: first erase the previous frame, then draw the current frame.
Display devices (e.g., phones) typically refresh at 60Hz, meaning 60 frames per second. Each frame performs three actions:
- clear: Clear the previous frame
- update: Update the current state
- draw: Draw the current state
Why Use requestAnimationFrame
A special emphasis: do not use setInterval for animation; use requestAnimationFrame instead. The reasons are:
setInterval's fixed time interval may not match the display device's refresh rate, leading to frame drops, stuttering, or wasted draws.requestAnimationFramedirectly syncs with the display device's refresh signal, providing a smoother experience and being key for performance optimization.
The usage is to recursively call requestAnimationFrame(draw) at the end of the drawing function.
Moving Small Square
The classroom example 2.html implements this logic: a square moves from left to right, and when it goes off the canvas, it re-enters from the left:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- Originally the html page is a DOM tree, now we add a canvas tag rendering area -->
<!-- The width/height attributes are the actual pixel dimensions of the canvas -->
<canvas id="myCanvas" width="400" height="400"
style="border: 1px solid #333;">
Your browser does not support the canvas tag (old IE will show this text).
</canvas>
<script>
// Canvas element, the tag is just the beginning
const canvas = document.querySelector('#myCanvas');
// Drawing context object
// 3d activates GPU capabilities
const ctx = canvas.getContext('2d');
let x = 20;
let y = 20;
const width = 100;
const height = 80;
const speed = 3;
function animate() {
// Clear canvas: erase the rectangular area drawn in the previous frame
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'red';
ctx.fillRect(x, y, width, height);
x += speed;
if(x > canvas.width) {
x = -width;
}
// Request keyframe animation
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>
This example clearly demonstrates the three steps of the animation loop:
clearRecterases the entire canvas;fillRectdraws the rectangle at the new position;x += speedupdates the position for the next frame;requestAnimationFrame(animate)requests the next frame.
"Frame animation + interaction = game." By handling user input, collision detection, and state updates in each frame, you can build a complete game loop.
Airplane Mini-Game: Turning Frame Animation into a Complete Project
The hands-on part of this lesson is an airplane mini-game built using the Vibe Coding approach. Let's walk through the process:
- Initialize the project with Vite, along with Git for version control.
- Brainstorm with an AI Agent:
- Product side: List game features, first focus on MVP (Minimum Viable Product).
- Technical side: Determine the technical route and architecture plan.
- Have the LLM generate code, then manually review and iterate.
The project structure is standard:
index.html: Page entrysrc/main.js: Main game logicsrc/style.css: Basic styles
Page and Styles
index.html is very simple, with only a full-screen <canvas>:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>Thunder Airplane</title>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script type="module" src="/src/main.js"></script>
</body>
</html>
style.css makes the canvas full-screen:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
package.json only depends on Vite:
{
"name": "airplane",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^8.1.0"
}
}
Main Game Logic
src/main.js is the core of the entire project. The teacher didn't explain line by line but walked us through the overall structure. I broke it down by module.
First, canvas initialization and game state:
import './style.css'
// ==================== Canvas Initialization ====================
const canvas = document.getElementById('gameCanvas')
const ctx = canvas.getContext('2d')
function resizeCanvas() {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
resizeCanvas()
window.addEventListener('resize', resizeCanvas)
// ==================== Game State ====================
const GAME_STATE = {
READY: 0,
PLAYING: 1,
OVER: 2,
}
let state = GAME_STATE.READY
let score = 0
let lastEnemySpawn = 0
let enemySpawnInterval = 1200 // milliseconds, enemy spawn interval
Then key management, supporting arrow keys and WASD:
// ==================== Key Management ====================
const keys = {}
window.addEventListener('keydown', (e) => {
keys[e.code] = true
// Space to shoot (prevent too rapid fire from holding, use throttling)
if (e.code === 'Space' && state === GAME_STATE.PLAYING) {
e.preventDefault()
playerShoot()
}
// Enter to start/restart
if (e.code === 'Enter') {
if (state === GAME_STATE.READY || state === GAME_STATE.OVER) {
startGame()
}
}
})
window.addEventListener('keyup', (e) => {
keys[e.code] = false
})
The player airplane, bullets, enemies, and explosion effects are all split into independent modules. For example, the player airplane:
// ==================== Player Airplane ====================
const player = {
x: 0,
y: 0,
width: 50,
height: 40,
speed: 6,
color: '#00bfff',
lastShoot: 0,
shootCooldown: 150, // milliseconds
}
function resetPlayer() {
player.x = canvas.width / 2 - player.width / 2
player.y = canvas.height - player.height - 30
}
function updatePlayer() {
if (keys['ArrowLeft'] || keys['KeyA']) {
player.x -= player.speed
}
if (keys['ArrowRight'] || keys['KeyD']) {
player.x += player.speed
}
if (keys['ArrowUp'] || keys['KeyW']) {
player.y -= player.speed
}
if (keys['ArrowDown'] || keys['KeyS']) {
player.y += player.speed
}
// Boundary constraints
if (player.x < 0) player.x = 0
if (player.x + player.width > canvas.width) player.x = canvas.width - player.width
if (player.y < 0) player.y = 0
if (player.y + player.height > canvas.height) player.y = canvas.height - player.height
}
The enemy system includes random generation and difficulty progression logic:
function spawnEnemy() {
const w = 36 + Math.random() * 20 // 36~56
const h = w * 0.8
enemies.push({
x: Math.random() * (canvas.width - w),
y: -h,
width: w,
height: h,
speed: 1.5 + Math.random() * 2, // 1.5~3.5
type: Math.random() < 0.2 ? 'red' : 'gray', // 20% red elite
hp: Math.random() < 0.2 ? 2 : 1,
})
}
function updateEnemies() {
const now = Date.now()
// Spawn enemies periodically
if (now - lastEnemySpawn > enemySpawnInterval) {
spawnEnemy()
lastEnemySpawn = now
// Increase difficulty with score: speed up spawn interval
enemySpawnInterval = Math.max(400, 1200 - score * 2)
}
// ...
}
Collision detection uses the AABB (Axis-Aligned Bounding Box) algorithm:
// ==================== Collision Detection ====================
function rectCollide(a, b) {
return (
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
)
}
Finally, the main game loop, which fully implements the "clear → update → draw → requestAnimationFrame" pattern learned earlier:
// ==================== Main Game Loop ====================
let lastTime = 0
function gameLoop(timestamp) {
// Clear screen
ctx.fillStyle = '#000011'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Starfield always drawn
updateStars()
drawStars()
if (state === GAME_STATE.READY) {
drawReadyScreen()
} else if (state === GAME_STATE.PLAYING) {
// Update
updatePlayer()
updateBullets()
updateEnemies()
checkCollisions()
updateExplosions()
// Draw
drawBullets()
drawEnemies()
drawPlayer()
drawExplosions()
drawScore()
} else if (state === GAME_STATE.OVER) {
// After game over, still draw remaining explosions and score
updateExplosions()
drawBullets()
drawEnemies()
drawExplosions()
drawScore()
drawGameOverScreen()
}
requestAnimationFrame(gameLoop)
}
// Start
resetPlayer()
requestAnimationFrame(gameLoop)
This example truly made me understand the phrase "frame animation + interaction = game." A game isn't drawn all at once; it's erased, updated, and redrawn every frame.
ECharts: Declarative Data Visualization
Finally, let's talk about ECharts, a front-end visualization framework that supports both Canvas and SVG rendering. It supports various chart types like bar charts, line charts, pie charts, maps, radar charts, etc., suitable for reports, dashboards, and large screens for data display.
Core Idea of ECharts
ECharts' biggest feature is declarative configuration. We don't need to manually call fillRect to draw each bar like in Canvas; we just tell ECharts "what chart I want, what the data is, and how to style it," and it automatically renders it for us.
The usage can be summarized in four steps:
- Import ECharts
- Prepare a DOM container (usually a
div) - Configure the
optionobject (describing chart type, data, styles) - Call
echarts.init()andsetOption()to render the chart
Generating an ECharts Page with an AI Agent
In this lesson's practical session, we tried using an AI Agent to generate a complete data visualization page. The specific process was:
- Fictional Data: We created a "Xiao's E-Commerce Group" and set its annual sneaker sales data (12 months, unit: million yuan), along with various product category proportions, regional sales distribution, etc.
- Describe Requirements to the Agent: We told the AI we needed a data dashboard containing these charts:
- Annual sales trend (line chart)
- Product category proportion (pie chart)
- Regional sales distribution (bar chart)
- Core KPI numbers (total sales, order count, active users, conversion rate)
- Agent Generates Code: Based on our description, the AI automatically generated complete HTML, CSS, and JavaScript code, including ECharts
optionconfigurations. - Manual Review and Iteration: We checked the generated code, adjusted style details, and ensured the data display met expectations.
What an ECharts option Looks Like
This is the core of ECharts configuration. A typical option object structure is as follows:
const option = {
title: { text: 'Xiao's E-Commerce Group - Annual Sneaker Sales Trend' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: monthLabels },
yAxis: { type: 'value', name: 'Sales (million yuan)' },
series: [{ data: salesData, type: 'bar' }]
}
As you can see, we only need to describe "what the title is, what type the X-axis is, what the data is," and ECharts automatically calculates coordinates, draws bars, and adds interactions. That's the beauty of "declarative" configuration.
Generated Page Effect
The page generated by the Agent contains multiple charts and KPI panels, laid out using CSS Grid into a complete dashboard. The page automatically adapts to window size, and the KPI numbers have an animation effect scrolling from 0 to the target value (using requestAnimationFrame, which we learned earlier).
This practice made me realize that ECharts encapsulates Canvas's low-level drawing capabilities into declarative configurations. We don't need to manually calculate the coordinates of each bar; we just describe the data and styles. This forms an interesting contrast with the hand-drawn frames in the airplane mini-game: the former is "imperative drawing," while the latter is "declarative configuration."
Review: A Few Easily Confused Points
Several key questions, I've compiled them here.
1. Why Recommend requestAnimationFrame
Syncs with screen refresh rate: reduces wasted draws, lowers CPU/GPU overhead.
Auto-throttling: automatically pauses when the page is not visible, saving resources.
Better performance: more suitable for animation loops than setInterval/setTimeout.
2. Three Steps of the Animation Loop
1. clear: Clear the previous frame
2. draw: Draw the current state
3. update: Update the next state
3. Self-Test Questions
- What is the purpose of the object returned by
getContext('2d')? - What is the difference between
fillRectandstrokeRect? - What role does
clearRectplay in frame animation? - Why is it not recommended to use
setIntervalfor animation? - What is the relationship between ECharts and Canvas?
How to Understand Now
I hope this article gives everyone a more concrete understanding of "what front-end can draw."
<canvas> is not a replacement for DOM, but a supplement. When you need a large number of dynamic graphics, game screens, or data visualizations, Canvas is more efficient than manipulating DOM nodes. Libraries like ECharts further illustrate that in an engineering society, we rarely call Canvas API directly to draw a complete dashboard; more often, we configure and extend on top of existing abstractions.
The airplane mini-game example also let me experience the Vibe Coding workflow again: first, break down requirements into an MVP with AI, then have AI generate the code skeleton, and finally manually review, debug, and iterate. The technical points of the Canvas game itself are certainly important, but what's more worth remembering is "how to turn an idea into a runnable project."
Additionally, although the requestAnimationFrame knowledge point is small, it connects many basic concepts of front-end performance optimization: screen refresh rate, rendering pipeline, wasted draws. In the future, when I see animation stuttering, game frame drops, or dashboard refresh issues, I'll first think about whether the animation scheduling method was chosen correctly.
This lesson didn't involve any major new frameworks to learn, but it gave us a feel for the browser's underlying drawing capabilities. If we continue in the direction of gaming or visualization, three.js and more complex ECharts configurations will be natural extensions.