跪拜 Guibai
← Back to the summary

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:

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:

  1. getContext('2d') returns the 2D drawing context, which is our main battlefield for this lesson.
  2. 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:

And three commonly used properties:

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:

  1. clear: Clear the previous frame
  2. update: Update the current state
  3. draw: Draw the current state

Why Use requestAnimationFrame

A special emphasis: do not use setInterval for animation; use requestAnimationFrame instead. The reasons are:

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:

"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:

  1. Initialize the project with Vite, along with Git for version control.
  2. 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.
  3. Have the LLM generate code, then manually review and iterate.

The project structure is standard:

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:

  1. Import ECharts
  2. Prepare a DOM container (usually a div)
  3. Configure the option object (describing chart type, data, styles)
  4. Call echarts.init() and setOption() 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:

  1. 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.
  2. 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)
  3. Agent Generates Code: Based on our description, the AI automatically generated complete HTML, CSS, and JavaScript code, including ECharts option configurations.
  4. 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

  1. What is the purpose of the object returned by getContext('2d')?
  2. What is the difference between fillRect and strokeRect?
  3. What role does clearRect play in frame animation?
  4. Why is it not recommended to use setInterval for animation?
  5. 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.