HTML5 Canvas Game Development: Building Interactive Games with JavaScript
HTML5 is the new HTML standard. One of the most interesting new features in HTML5 is the canvas element canvas for 2D …
Extending our JavaScript Canvas applications and paint app development, this guide explores advanced techniques for creating high-performance, real-time data visualization dashboards using HTML5 Canvas and modern JavaScript.
1class CanvasRenderingEngine {
2 constructor(canvasId, width = 800, height = 600) {
3 this.canvas = document.getElementById(canvasId);
4 this.ctx = this.canvas.getContext('2d');
5
6 // Set canvas dimensions
7 this.canvas.width = width;
8 this.canvas.height = height;
9
10 // Performance optimizations
11 this.ctx.imageSmoothingEnabled = false;
12
13 // Off-screen canvas for double buffering
14 this.offscreenCanvas = document.createElement('canvas');
15 this.offscreenCanvas.width = width;
16 this.offscreenCanvas.height = height;
17 this.offscreenCtx = this.offscreenCanvas.getContext('2d');
18
19 // Animation frame tracking
20 this.animationId = null;
21 this.lastFrameTime = 0;
22 this.targetFPS = 60;
23 this.frameInterval = 1000 / this.targetFPS;
24
25 // Viewport and transformations
26 this.viewport = { x: 0, y: 0, width: width, height: height };
27 this.transform = { x: 0, y: 0, scale: 1 };
28
29 // Render queue for layered rendering
30 this.renderQueue = new Map();
31
32 this.initializeEventHandlers();
33 }
34
35 // Optimized rendering loop with frame rate control
36 render(timestamp) {
37 const deltaTime = timestamp - this.lastFrameTime;
38
39 if (deltaTime >= this.frameInterval) {
40 this.clearCanvas();
41 this.processRenderQueue();
42 this.presentFrame();
43
44 this.lastFrameTime = timestamp;
45 }
46
47 this.animationId = requestAnimationFrame((ts) => this.render(ts));
48 }
49
50 clearCanvas() {
51 this.offscreenCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
52 }
53
54 processRenderQueue() {
55 // Sort render items by layer
56 const sortedLayers = Array.from(this.renderQueue.entries())
57 .sort(([layerA], [layerB]) => layerA - layerB);
58
59 for (const [layer, renderItems] of sortedLayers) {
60 for (const item of renderItems) {
61 this.renderItem(item);
62 }
63 }
64 }
65
66 renderItem(item) {
67 this.offscreenCtx.save();
68
69 // Apply transformations
70 this.offscreenCtx.translate(this.transform.x, this.transform.y);
71 this.offscreenCtx.scale(this.transform.scale, this.transform.scale);
72
73 // Render the item
74 item.render(this.offscreenCtx);
75
76 this.offscreenCtx.restore();
77 }
78
79 presentFrame() {
80 // Copy off-screen canvas to main canvas
81 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
82 this.ctx.drawImage(this.offscreenCanvas, 0, 0);
83 }
84
85 addRenderItem(layer, item) {
86 if (!this.renderQueue.has(layer)) {
87 this.renderQueue.set(layer, []);
88 }
89 this.renderQueue.get(layer).push(item);
90 }
91
92 clearRenderQueue() {
93 this.renderQueue.clear();
94 }
95
96 startRendering() {
97 if (!this.animationId) {
98 this.render(performance.now());
99 }
100 }
101
102 stopRendering() {
103 if (this.animationId) {
104 cancelAnimationFrame(this.animationId);
105 this.animationId = null;
106 }
107 }
108
109 initializeEventHandlers() {
110 // Mouse interaction for pan and zoom
111 let isDragging = false;
112 let lastMousePos = { x: 0, y: 0 };
113
114 this.canvas.addEventListener('mousedown', (e) => {
115 isDragging = true;
116 lastMousePos = { x: e.offsetX, y: e.offsetY };
117 });
118
119 this.canvas.addEventListener('mousemove', (e) => {
120 if (isDragging) {
121 const deltaX = e.offsetX - lastMousePos.x;
122 const deltaY = e.offsetY - lastMousePos.y;
123
124 this.transform.x += deltaX;
125 this.transform.y += deltaY;
126
127 lastMousePos = { x: e.offsetX, y: e.offsetY };
128 }
129 });
130
131 this.canvas.addEventListener('mouseup', () => {
132 isDragging = false;
133 });
134
135 this.canvas.addEventListener('wheel', (e) => {
136 e.preventDefault();
137 const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
138 this.transform.scale *= zoomFactor;
139 this.transform.scale = Math.max(0.1, Math.min(5, this.transform.scale));
140 });
141 }
142}
143
144// Renderable item base class
145class RenderableItem {
146 constructor(x, y) {
147 this.x = x;
148 this.y = y;
149 this.visible = true;
150 this.opacity = 1.0;
151 }
152
153 render(ctx) {
154 if (!this.visible || this.opacity <= 0) return;
155
156 if (this.opacity < 1.0) {
157 ctx.globalAlpha = this.opacity;
158 }
159
160 this.draw(ctx);
161
162 if (this.opacity < 1.0) {
163 ctx.globalAlpha = 1.0;
164 }
165 }
166
167 draw(ctx) {
168 // Override in subclasses
169 }
170}
1class RealTimeLineChart extends RenderableItem {
2 constructor(x, y, width, height, maxDataPoints = 100) {
3 super(x, y);
4 this.width = width;
5 this.height = height;
6 this.maxDataPoints = maxDataPoints;
7
8 this.dataSeries = new Map();
9 this.timeRange = 60000; // 60 seconds
10 this.yRange = { min: 0, max: 100 };
11 this.autoScale = true;
12
13 // Styling
14 this.backgroundColor = 'rgba(20, 20, 20, 0.8)';
15 this.gridColor = 'rgba(255, 255, 255, 0.1)';
16 this.textColor = 'white';
17 this.font = '12px Arial';
18 }
19
20 addDataSeries(name, color = '#00ff00', lineWidth = 2) {
21 this.dataSeries.set(name, {
22 data: [],
23 color: color,
24 lineWidth: lineWidth,
25 visible: true
26 });
27 }
28
29 addDataPoint(seriesName, value, timestamp = Date.now()) {
30 const series = this.dataSeries.get(seriesName);
31 if (!series) return;
32
33 series.data.push({ value, timestamp });
34
35 // Remove old data points
36 const cutoffTime = timestamp - this.timeRange;
37 series.data = series.data.filter(point => point.timestamp > cutoffTime);
38
39 // Auto-scale Y axis
40 if (this.autoScale) {
41 this.updateYRange();
42 }
43 }
44
45 updateYRange() {
46 let min = Infinity;
47 let max = -Infinity;
48
49 for (const series of this.dataSeries.values()) {
50 for (const point of series.data) {
51 min = Math.min(min, point.value);
52 max = Math.max(max, point.value);
53 }
54 }
55
56 if (min !== Infinity && max !== -Infinity) {
57 const padding = (max - min) * 0.1;
58 this.yRange.min = min - padding;
59 this.yRange.max = max + padding;
60 }
61 }
62
63 draw(ctx) {
64 // Draw background
65 ctx.fillStyle = this.backgroundColor;
66 ctx.fillRect(this.x, this.y, this.width, this.height);
67
68 // Draw grid
69 this.drawGrid(ctx);
70
71 // Draw data series
72 for (const [name, series] of this.dataSeries) {
73 if (series.visible && series.data.length > 1) {
74 this.drawSeries(ctx, series);
75 }
76 }
77
78 // Draw axes and labels
79 this.drawAxes(ctx);
80 }
81
82 drawGrid(ctx) {
83 ctx.strokeStyle = this.gridColor;
84 ctx.lineWidth = 1;
85 ctx.setLineDash([2, 2]);
86
87 // Vertical grid lines (time)
88 const timeStep = this.timeRange / 6;
89 for (let i = 1; i < 6; i++) {
90 const x = this.x + (i * this.width / 6);
91 ctx.beginPath();
92 ctx.moveTo(x, this.y);
93 ctx.lineTo(x, this.y + this.height);
94 ctx.stroke();
95 }
96
97 // Horizontal grid lines (values)
98 for (let i = 1; i < 5; i++) {
99 const y = this.y + (i * this.height / 5);
100 ctx.beginPath();
101 ctx.moveTo(this.x, y);
102 ctx.lineTo(this.x + this.width, y);
103 ctx.stroke();
104 }
105
106 ctx.setLineDash([]);
107 }
108
109 drawSeries(ctx, series) {
110 if (series.data.length < 2) return;
111
112 ctx.strokeStyle = series.color;
113 ctx.lineWidth = series.lineWidth;
114
115 const currentTime = Date.now();
116 const startTime = currentTime - this.timeRange;
117
118 ctx.beginPath();
119
120 let firstPoint = true;
121 for (const point of series.data) {
122 const x = this.x + ((point.timestamp - startTime) / this.timeRange) * this.width;
123 const y = this.y + this.height -
124 ((point.value - this.yRange.min) / (this.yRange.max - this.yRange.min)) * this.height;
125
126 if (firstPoint) {
127 ctx.moveTo(x, y);
128 firstPoint = false;
129 } else {
130 ctx.lineTo(x, y);
131 }
132 }
133
134 ctx.stroke();
135
136 // Draw data points
137 ctx.fillStyle = series.color;
138 for (const point of series.data) {
139 const x = this.x + ((point.timestamp - startTime) / this.timeRange) * this.width;
140 const y = this.y + this.height -
141 ((point.value - this.yRange.min) / (this.yRange.max - this.yRange.min)) * this.height;
142
143 ctx.beginPath();
144 ctx.arc(x, y, 2, 0, 2 * Math.PI);
145 ctx.fill();
146 }
147 }
148
149 drawAxes(ctx) {
150 ctx.strokeStyle = this.textColor;
151 ctx.lineWidth = 2;
152 ctx.font = this.font;
153 ctx.fillStyle = this.textColor;
154
155 // Y-axis
156 ctx.beginPath();
157 ctx.moveTo(this.x, this.y);
158 ctx.lineTo(this.x, this.y + this.height);
159 ctx.stroke();
160
161 // X-axis
162 ctx.beginPath();
163 ctx.moveTo(this.x, this.y + this.height);
164 ctx.lineTo(this.x + this.width, this.y + this.height);
165 ctx.stroke();
166
167 // Y-axis labels
168 for (let i = 0; i <= 4; i++) {
169 const value = this.yRange.min + (i * (this.yRange.max - this.yRange.min) / 4);
170 const y = this.y + this.height - (i * this.height / 4);
171
172 ctx.fillText(value.toFixed(1), this.x - 40, y + 4);
173 }
174
175 // X-axis labels (time)
176 for (let i = 0; i <= 6; i++) {
177 const timeAgo = (6 - i) * 10; // seconds ago
178 const x = this.x + (i * this.width / 6);
179
180 ctx.fillText(`-${timeAgo}s`, x - 15, this.y + this.height + 20);
181 }
182 }
183}
184
185// Real-time gauge component
186class RealTimeGauge extends RenderableItem {
187 constructor(x, y, radius, min = 0, max = 100) {
188 super(x, y);
189 this.radius = radius;
190 this.min = min;
191 this.max = max;
192 this.value = min;
193 this.targetValue = min;
194
195 // Animation properties
196 this.animationSpeed = 0.1;
197
198 // Styling
199 this.backgroundColor = 'rgba(50, 50, 50, 0.8)';
200 this.gaugeColor = '#00ff00';
201 this.needleColor = '#ff0000';
202 this.textColor = 'white';
203
204 // Thresholds for color coding
205 this.thresholds = [
206 { value: 70, color: '#ffff00' },
207 { value: 90, color: '#ff0000' }
208 ];
209 }
210
211 setValue(newValue) {
212 this.targetValue = Math.max(this.min, Math.min(this.max, newValue));
213 }
214
215 update() {
216 // Smooth animation towards target value
217 const diff = this.targetValue - this.value;
218 this.value += diff * this.animationSpeed;
219 }
220
221 draw(ctx) {
222 this.update();
223
224 const centerX = this.x;
225 const centerY = this.y;
226
227 // Draw background circle
228 ctx.fillStyle = this.backgroundColor;
229 ctx.beginPath();
230 ctx.arc(centerX, centerY, this.radius, 0, 2 * Math.PI);
231 ctx.fill();
232
233 // Draw gauge arc
234 const startAngle = -Math.PI * 0.75;
235 const endAngle = Math.PI * 0.75;
236 const totalAngle = endAngle - startAngle;
237 const valueAngle = startAngle + (this.value / (this.max - this.min)) * totalAngle;
238
239 // Determine color based on thresholds
240 let gaugeColor = this.gaugeColor;
241 for (const threshold of this.thresholds) {
242 if (this.value >= threshold.value) {
243 gaugeColor = threshold.color;
244 }
245 }
246
247 ctx.strokeStyle = gaugeColor;
248 ctx.lineWidth = 10;
249 ctx.beginPath();
250 ctx.arc(centerX, centerY, this.radius - 20, startAngle, valueAngle);
251 ctx.stroke();
252
253 // Draw needle
254 ctx.strokeStyle = this.needleColor;
255 ctx.lineWidth = 3;
256 const needleLength = this.radius - 10;
257 const needleX = centerX + Math.cos(valueAngle) * needleLength;
258 const needleY = centerY + Math.sin(valueAngle) * needleLength;
259
260 ctx.beginPath();
261 ctx.moveTo(centerX, centerY);
262 ctx.lineTo(needleX, needleY);
263 ctx.stroke();
264
265 // Draw center circle
266 ctx.fillStyle = this.needleColor;
267 ctx.beginPath();
268 ctx.arc(centerX, centerY, 8, 0, 2 * Math.PI);
269 ctx.fill();
270
271 // Draw value text
272 ctx.fillStyle = this.textColor;
273 ctx.font = '24px Arial';
274 ctx.textAlign = 'center';
275 ctx.fillText(this.value.toFixed(1), centerX, centerY + this.radius + 30);
276
277 // Draw min/max labels
278 ctx.font = '14px Arial';
279 ctx.fillText(this.min.toString(), centerX - this.radius + 10, centerY + 30);
280 ctx.fillText(this.max.toString(), centerX + this.radius - 10, centerY + 30);
281 }
282}
1class RealTimeDashboard {
2 constructor(canvasId) {
3 this.engine = new CanvasRenderingEngine(canvasId, 1200, 800);
4 this.charts = new Map();
5 this.dataConnections = new Map();
6
7 this.setupCharts();
8 this.setupDataConnections();
9 this.engine.startRendering();
10 }
11
12 setupCharts() {
13 // CPU Usage Chart
14 const cpuChart = new RealTimeLineChart(50, 50, 400, 200);
15 cpuChart.addDataSeries('CPU', '#00ff00');
16 cpuChart.yRange = { min: 0, max: 100 };
17 cpuChart.autoScale = false;
18 this.engine.addRenderItem(1, cpuChart);
19 this.charts.set('cpu', cpuChart);
20
21 // Memory Usage Gauge
22 const memoryGauge = new RealTimeGauge(650, 150, 80);
23 this.engine.addRenderItem(1, memoryGauge);
24 this.charts.set('memory', memoryGauge);
25
26 // Network Traffic Chart
27 const networkChart = new RealTimeLineChart(50, 300, 400, 200);
28 networkChart.addDataSeries('Download', '#00ff00');
29 networkChart.addDataSeries('Upload', '#ff0000');
30 this.engine.addRenderItem(1, networkChart);
31 this.charts.set('network', networkChart);
32
33 // Temperature Gauge
34 const tempGauge = new RealTimeGauge(850, 150, 80, 0, 100);
35 tempGauge.thresholds = [
36 { value: 60, color: '#ffff00' },
37 { value: 80, color: '#ff0000' }
38 ];
39 this.engine.addRenderItem(1, tempGauge);
40 this.charts.set('temperature', tempGauge);
41 }
42
43 setupDataConnections() {
44 // WebSocket connection for real-time data
45 const ws = new WebSocket('wss://your-data-server.com/realtime');
46
47 ws.onmessage = (event) => {
48 const data = JSON.parse(event.data);
49 this.updateCharts(data);
50 };
51
52 // Fallback to polling if WebSocket fails
53 ws.onerror = () => {
54 console.log('WebSocket failed, falling back to polling');
55 this.startPolling();
56 };
57
58 // Simulated data for demo
59 this.startSimulatedData();
60 }
61
62 startSimulatedData() {
63 setInterval(() => {
64 const simulatedData = {
65 cpu: Math.random() * 100,
66 memory: 40 + Math.random() * 40,
67 networkDown: Math.random() * 1000,
68 networkUp: Math.random() * 200,
69 temperature: 30 + Math.random() * 40
70 };
71
72 this.updateCharts(simulatedData);
73 }, 1000);
74 }
75
76 updateCharts(data) {
77 const timestamp = Date.now();
78
79 if (data.cpu !== undefined) {
80 this.charts.get('cpu').addDataPoint('CPU', data.cpu, timestamp);
81 }
82
83 if (data.memory !== undefined) {
84 this.charts.get('memory').setValue(data.memory);
85 }
86
87 if (data.networkDown !== undefined) {
88 this.charts.get('network').addDataPoint('Download', data.networkDown, timestamp);
89 }
90
91 if (data.networkUp !== undefined) {
92 this.charts.get('network').addDataPoint('Upload', data.networkUp, timestamp);
93 }
94
95 if (data.temperature !== undefined) {
96 this.charts.get('temperature').setValue(data.temperature);
97 }
98 }
99
100 startPolling() {
101 setInterval(async () => {
102 try {
103 const response = await fetch('/api/metrics');
104 const data = await response.json();
105 this.updateCharts(data);
106 } catch (error) {
107 console.error('Failed to fetch metrics:', error);
108 }
109 }, 2000);
110 }
111}
112
113// Initialize dashboard
114document.addEventListener('DOMContentLoaded', () => {
115 const dashboard = new RealTimeDashboard('dashboard-canvas');
116});
This advanced Canvas-based visualization framework provides the foundation for building high-performance, interactive dashboards. The modular architecture supports real-time data streaming, smooth animations, and responsive user interactions.
For foundational Canvas concepts, see our HTML5 Canvas game tutorial and JavaScript paint application.