一、引言
HTML5 为 Web 平台带来了革命性的多媒体能力。曾经需要 Flash、Silverlight 或 Java Applet 才能实现的功能——播放视频、渲染矢量图形、实时绘制动画——如今都已原生内置于浏览器中。本文将深入探讨 HTML 多媒体生态的四大核心领域:图片优化与响应式技术、SVG 矢量图形、Canvas 2D/3D 绘制、以及 Video/Audio 媒体播放。
每个知识点均配有可直接运行的完整代码示例,旨在帮助你从"会用"进阶到"精通"。
二、图片深度:从 <img> 到现代化图像管道
2.1 基础 <img> 元素
图片是 Web 最古老的多媒体形式。<img> 元素的核心属性:
<img
src="photo.jpg"
alt="描述文字"
width="800"
height="600"
decoding="async"
fetchpriority="high"
>alt:必需——屏幕阅读器依赖它,SEO 也依赖它。width/height:设置宽高比,防止布局偏移(CLS)。decoding="async":异步解码,不阻塞 DOM 解析。fetchpriority="high":提示浏览器优先加载(LCP 优化)。
2.2 响应式图片:srcset 与 sizes
同一张图片在不同设备上需要不同分辨率。srcset 提供多套候选图,sizes 告诉浏览器图片实际渲染宽度。
<img
src="photo-800.jpg"
srcset="
photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w,
photo-1600.jpg 1600w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw
"
alt="响应式示例"
>工作原理:
- 浏览器检查
sizes,根据当前视口宽度计算图片渲染宽度。 - 在
srcset中选取大于等于渲染宽度且最接近的那张图。 - 如果设备像素比(DPR)为 2x,还会加倍选取。
你也可以用 x 描述符按 DPR 匹配:
<img
src="photo-1x.jpg"
srcset="photo-1x.jpg 1x, photo-2x.jpg 2x, photo-3x.jpg 3x"
alt="DPR 响应式"
>2.3 <picture> 与 <source>:艺术指导与格式回退
当需要不同裁剪(艺术指导)或不同格式时,用 <picture> 包裹多个 <source>:
<picture>
<!-- AVIF 格式,优先 -->
<source
srcset="photo.avif"
type="image/avif"
>
<!-- WebP 格式回退 -->
<source
srcset="photo.webp"
type="image/webp"
>
<!-- 手机端横图裁剪 -->
<source
srcset="photo-mobile.jpg"
media="(max-width: 600px)"
>
<!-- 最终 fallback -->
<img
src="photo.jpg"
alt="完整回退链"
loading="lazy"
>
</picture><source> 的 media 属性支持任何媒体查询,type 属性用于格式协商。浏览器从第一个 <source> 开始依次尝试,一旦匹配就停止。
2.4 现代图片格式:WebP 与 AVIF
| 格式 | 压缩率 vs JPEG | 特性 |
|---|---|---|
| WebP | 小 25-35% | 有损/无损/透明/动画,Chrome 系原生支持 |
| AVIF | 小 50%+ | AV1 编码,更优压缩,HDR 支持,渐进解码 |
检测浏览器是否支持 AVIF:
function checkAVIFSupport() {
const img = new Image();
img.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlm';
return new Promise(resolve => {
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
});
}
checkAVIFSupport().then(supported => {
console.log('AVIF 支持:', supported);
});2.5 懒加载:loading="lazy"
HTML 原生的懒加载无需 JavaScript:
<img src="large-photo.jpg" loading="lazy" alt="懒加载图片">
<iframe src="map.html" loading="lazy" title="地图"></iframe>loading 属性值:
| 值 | 行为 |
|---|---|
lazy |
图片进入视口附近才开始加载 |
eager |
立即加载(默认) |
白名单机制:loading=lazy 只在以下情况生效:
- 设置了
width和height(浏览器需提前预留空间) - 图片不在首屏视口内
- 未被
fetchpriority="high"覆盖
Intersection Observer 自定义懒加载(更精细控制):
<img
data-src="real-image.jpg"
class="lazy"
alt="IO 懒加载"
width="800"
height="600"
>const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('img.lazy').forEach(img => observer.observe(img));2.6 图片 CDN 与优化策略
专业图片 CDN(Cloudinary、Imgix、Akamai Image & Video Manager)可在 URL 层面动态处理图片:
https://res.cloudinary.com/demo/image/upload/
w_800,h_600,c_fill,f_auto,q_auto/v1/photo.jpg参数解读:
w_800,h_600,c_fill:裁剪为 800×600f_auto:浏览器协商最优格式(WebP/AVIF)q_auto:自动质量
自建优化清单:
1. 格式选择:优先 AVIF → WebP → JPEG/PNG
2. 分辨率:每张图不超过实际渲染尺寸的 2x(DPR)
3. 压缩:JPEG 质量 85,PNG 用 pngquant
4. 响应式:提供 5-10 个断点版本
5. 懒加载:所有非首屏图片加 loading=lazy
6. CDN:开启格式协商、质量自适应、WebP 转换
7. 预加载关键图片(LCP):
<link rel="preload" as="image" href="hero.webp">三、SVG:可缩放矢量图形
SVG(Scalable Vector Graphics)是用 XML 描述 2D 图形的格式。它无限缩放不失真,文件体积通常小于位图,且可通过 CSS/JavaScript 完全控制。
3.1 在 HTML 中使用 SVG 的四种方式
方式一:内联 SVG(推荐)
<svg width="100" height="100" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="#4A90D9" />
</svg>优点:可被 CSS/JS 完全控制,可继承文档样式,无需额外 HTTP 请求。
缺点:增加 HTML 体积,不易缓存。
方式二:<img> 引用
<img src="icon.svg" alt="SVG 图标">优点:浏览器缓存,HTML 简洁。
缺点:不能通过 CSS 修改内部颜色,不能响应内部交互。
方式三:<object> 嵌入
<object data="chart.svg" type="image/svg+xml" width="400" height="300">
<!-- 回退内容 -->
<img src="chart.png" alt="图表回退">
</object>优点:保留 SVG 内部脚本和交互,支持回退内容。
缺点:与宿主页面的 CSS 隔离。
方式四:<iframe> 嵌入
<iframe src="widget.svg" width="400" height="300" title="SVG 部件"></iframe>优点:完全隔离,适合第三方 SVG 应用。
缺点:无法与父页面通信(除非 postMessage),SEO 不友好。
3.2 常用 SVG 元素
<svg viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg">
<!-- rect:矩形 -->
<rect x="10" y="10" width="80" height="60"
rx="8" ry="8" fill="#FF6B6B" stroke="#333" stroke-width="2" />
<!-- circle:圆形 -->
<circle cx="150" cy="40" r="30" fill="#4ECDC4" />
<!-- ellipse:椭圆 -->
<ellipse cx="250" cy="40" rx="40" ry="20" fill="#45B7D1" />
<!-- line:直线 -->
<line x1="320" y1="10" x2="380" y2="70" stroke="#96CEB4" stroke-width="4" />
<!-- path:路径(最强大的 SVG 元素) -->
<!-- M = moveTo, L = lineTo, C = cubic bezier, Z = close -->
<path d="M 20 120 L 80 180 L 20 180 Z" fill="#DDA0DD" stroke="#333" />
<path d="M 120 120 C 120 60, 180 60, 180 120 S 240 180, 240 120"
fill="none" stroke="#FF8C00" stroke-width="3" />
<!-- polygon:多边形 -->
<polygon points="280,160 320,120 360,160 340,200 300,200"
fill="#9B59B6" />
<!-- text:文字 -->
<text x="20" y="240" font-family="Arial" font-size="20" fill="#2C3E50">
你好,SVG!
</text>
<!-- g:分组 -->
<g transform="translate(200, 240) scale(1.5)">
<circle cx="0" cy="0" r="15" fill="#E74C3C" />
<text x="0" y="5" text-anchor="middle" fill="white" font-size="14">!</text>
</g>
</svg>3.3 defs 与 use:SVG 精灵图
<defs> 定义可复用的图形资源,<use> 引用它们。这是SVG 精灵图的核心技术。
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<defs>
<!-- 定义一个图标 -->
<g id="icon-home">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
</g>
<g id="icon-user">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
</g>
<!-- 带多种颜色的复杂图标 -->
<g id="icon-star">
<polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" />
</g>
</defs>
</svg>
<!-- 使用图标 -->
<svg width="24" height="24" viewBox="0 0 24 24">
<use href="#icon-home" fill="#333" />
</svg>
<svg width="32" height="32" viewBox="0 0 24 24">
<use href="#icon-user" fill="#4A90D9" />
</svg>
<svg width="48" height="48" viewBox="0 0 24 24">
<use href="#icon-star" fill="#F1C40F" />
</svg>SVG 精灵图 vs 字体图标 vs PNG Sprite:
| 方案 | 清晰度 | 色彩 | 体积 | 可控性 |
|---|---|---|---|---|
| SVG Sprite | ∞ | 多色 | 小 | 最高 |
| 字体图标 | 仅矢量 | 单色 | 小 | 一般 |
| PNG Sprite | 受限 | 任意 | 大 | 最低 |
3.4 响应式 SVG
SVG 天生响应式,但需要正确设置 viewBox。
<!-- 正确:使用 viewBox 设置宽高比,不设 width/height 或设 100% -->
<svg viewBox="0 0 200 100" preserveAspectRatio="xMidYMid meet">
<rect x="0" y="0" width="200" height="100" fill="#3498DB" rx="10" />
<circle cx="50" cy="50" r="30" fill="#E74C3C" />
<circle cx="150" cy="50" r="30" fill="#2ECC71" />
</svg>preserveAspectRatio 详解:
语法:preserveAspectRatio="<align> <meetOrSlice>"
| 对齐值 | 含义 |
|---|---|
| xMinYMin | 左上角对齐 |
| xMidYMid | 居中(默认) |
| xMaxYMax | 右下角对齐 |
| meetOrSlice | 含义 |
|---|---|
| meet | 完整显示(类似 contain) |
| slice | 裁剪填充(类似 cover) |
/* CSS 让 SVG 铺满容器 */
.responsive-svg {
width: 100%;
height: auto;
max-width: 800px;
display: block;
}3.5 SVG 动画
方式一:CSS @keyframes + SVG 属性
<style>
.pulse-circle {
animation: pulse 2s ease-in-out infinite;
transform-origin: center;
}
@keyframes pulse {
0% { r: 20; fill: #3498DB; }
50% { r: 35; fill: #E74C3C; }
100% { r: 20; fill: #3498DB; }
}
.rotate-gear {
animation: spin 4s linear infinite;
transform-origin: 100px 100px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.draw-path {
stroke-dasharray: 500;
stroke-dashoffset: 500;
animation: draw 3s ease forwards;
}
@keyframes draw {
to { stroke-dashoffset: 0; }
}
</style>
<svg viewBox="0 0 200 200">
<!-- 脉冲动画 -->
<circle cx="60" cy="60" r="20" class="pulse-circle" />
<!-- 旋转动画 -->
<g class="rotate-gear">
<circle cx="100" cy="100" r="30" fill="none" stroke="#2C3E50" stroke-width="6" />
<line x1="100" y1="70" x2="100" y2="130" stroke="#2C3E50" stroke-width="4" />
<line x1="70" y1="100" x2="130" y2="100" stroke="#2C3E50" stroke-width="4" />
</g>
<!-- 路径描绘动画 -->
<path d="M 140 140 Q 160 110, 180 140 T 190 170"
fill="none" stroke="#E67E22" stroke-width="4" class="draw-path" />
</svg>方式二:SMIL 动画(原生 SVG 动画)
SMIL(Synchronized Multimedia Integration Language)直接在 SVG 标签内定义动画,无需 CSS 或 JS。
<svg viewBox="0 0 400 200">
<!-- 移动 -->
<circle cx="40" cy="100" r="20" fill="#3498DB">
<animate
attributeName="cx"
values="40;360;40"
dur="4s"
repeatCount="indefinite" />
</circle>
<!-- 变色 + 缩放 -->
<rect x="150" y="40" width="100" height="60" rx="8">
<animate
attributeName="fill"
values="#E74C3C;#2ECC71;#3498DB;#E74C3C"
dur="6s"
repeatCount="indefinite" />
<animateTransform
attributeName="transform"
type="rotate"
from="0 200 70"
to="360 200 70"
dur="4s"
repeatCount="indefinite" />
</rect>
<!-- 渐变透明度 -->
<text x="320" y="110" font-size="24" text-anchor="middle" fill="#9B59B6">
淡入淡出
<animate
attributeName="opacity"
values="0;1;0"
dur="3s"
repeatCount="indefinite" />
</text>
</svg>SMIL 核心元素:
| 元素 | 作用 |
|---|---|
<animate> |
驱动单一属性动画 |
<animateTransform> |
驱动变换动画(旋转、缩放、平移) |
<animateMotion> |
沿路径移动 |
<set> |
瞬间设置属性值(非连续动画) |
四、Canvas:像素级的绘图画布
Canvas 提供位图级别的绘制 API,通过 JavaScript 逐像素控制。与 SVG 的"保留模式"(保留图形对象树)不同,Canvas 使用立即模式——绘制后不保留对象信息。
4.1 基础与上下文
<canvas id="myCanvas" width="800" height="600">
您的浏览器不支持 Canvas。
</canvas>const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 检查上下文
if (!ctx) {
console.error('Canvas 2D 上下文获取失败');
}4.2 绘制形状
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// ----- 矩形 -----
ctx.fillStyle = '#3498DB';
ctx.fillRect(20, 20, 150, 100); // 填充矩形
ctx.strokeStyle = '#E74C3C';
ctx.lineWidth = 4;
ctx.strokeRect(200, 20, 150, 100); // 描边矩形
ctx.clearRect(50, 40, 80, 60); // 擦除区域
// ----- 圆形(通过路径) -----
ctx.beginPath();
ctx.arc(400, 70, 50, 0, Math.PI * 2); // 完整圆
ctx.fillStyle = '#2ECC71';
ctx.fill();
ctx.strokeStyle = '#27AE60';
ctx.lineWidth = 3;
ctx.stroke();
// 半圆(弧)
ctx.beginPath();
ctx.arc(520, 70, 50, 0, Math.PI); // 上半圆
ctx.fillStyle = '#E67E22';
ctx.fill();4.3 路径与贝塞尔曲线
// ----- 路径 -----
ctx.beginPath();
ctx.moveTo(20, 200); // 起点
ctx.lineTo(150, 250); // 直线
ctx.lineTo(80, 320);
ctx.closePath(); // 闭合路径
ctx.fillStyle = '#9B59B6';
ctx.fill();
ctx.strokeStyle = '#8E44AD';
ctx.stroke();
// ----- 贝塞尔曲线 -----
ctx.beginPath();
ctx.moveTo(200, 300);
// 二次贝塞尔曲线:控制点 (300, 150),终点 (400, 300)
ctx.quadraticCurveTo(300, 150, 400, 300);
ctx.strokeStyle = '#E74C3C';
ctx.lineWidth = 3;
ctx.stroke();
// 三次贝塞尔曲线:控制点1 (450, 200),控制点2 (550, 400),终点 (600, 300)
ctx.beginPath();
ctx.moveTo(450, 300);
ctx.bezierCurveTo(500, 200, 550, 400, 600, 300);
ctx.strokeStyle = '#3498DB';
ctx.stroke();
// 弧线
ctx.beginPath();
ctx.arc(700, 280, 50, Math.PI / 4, Math.PI * 1.5);
ctx.strokeStyle = '#2ECC71';
ctx.lineWidth = 4;
ctx.stroke();4.4 文字渲染
// ----- 文字 -----
ctx.font = 'bold 48px Arial, sans-serif';
ctx.fillStyle = '#2C3E50';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Canvas 文字', 400, 400);
// 描边文字
ctx.font = '36px Georgia, serif';
ctx.strokeStyle = '#E74C3C';
ctx.lineWidth = 2;
ctx.strokeText('描边效果', 400, 480);
// 多行文字(Canvas 无自动换行,需手动处理)
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const chars = text.split('');
let line = '';
let lineY = y;
for (const char of chars) {
const testLine = line + char;
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && line !== '') {
ctx.fillText(line, x, lineY);
line = char;
lineY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, x, lineY);
}
ctx.font = '16px Arial';
ctx.fillStyle = '#333';
wrapText(ctx, 'Canvas 本身不提供自动换行,需要开发者手动计算文字宽度并换行。这是一个自定义的换行函数示例。',
20, 540, 760, 24);4.5 渐变与阴影
// ----- 线性渐变 -----
const linearGrad = ctx.createLinearGradient(20, 400, 180, 500);
linearGrad.addColorStop(0, '#FF6B6B');
linearGrad.addColorStop(0.5, '#FECA57');
linearGrad.addColorStop(1, '#48DBFB');
ctx.fillStyle = linearGrad;
ctx.fillRect(20, 400, 160, 100);
// ----- 径向渐变 -----
const radialGrad = ctx.createRadialGradient(300, 450, 10, 300, 450, 60);
radialGrad.addColorStop(0, '#FFF');
radialGrad.addColorStop(0.3, '#FECA57');
radialGrad.addColorStop(1, '#FF6B6B');
ctx.beginPath();
ctx.arc(300, 450, 60, 0, Math.PI * 2);
ctx.fillStyle = radialGrad;
ctx.fill();
// ----- 阴影 -----
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = '#9B59B6';
ctx.fillRect(420, 400, 150, 100);
// 重置阴影(性能考虑,不用时务必清除)
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;4.6 图像处理:drawImage、getImageData、putImageData
// ----- 绘制图像 -----
const img = new Image();
img.onload = function() {
// drawImage(img, dx, dy, dWidth, dHeight)
ctx.drawImage(img, 20, 20, 200, 150);
// drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
// 裁剪原图的一部分再绘制
ctx.drawImage(img, 50, 50, 100, 100, 250, 20, 200, 150);
};
img.src = 'https://via.placeholder.com/400x300';
// ----- 像素级操作 -----
function applyGrayscale(canvas) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // Uint8ClampedArray
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// 灰度公式:0.299R + 0.587G + 0.114B
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
data[i] = gray; // R
data[i + 1] = gray; // G
data[i + 2] = gray; // B
// data[i + 3] = alpha (保持不变)
}
ctx.putImageData(imageData, 0, 0);
}
// ----- 截图(将 Canvas 转为图片) -----
function captureCanvas(canvas) {
// 方式一:toDataURL
const dataURL = canvas.toDataURL('image/png');
// 下载
const link = document.createElement('a');
link.download = 'screenshot.png';
link.href = dataURL;
link.click();
// 方式二:toBlob(更适合大图)
canvas.toBlob(function(blob) {
const url = URL.createObjectURL(blob);
// 创建 FormData 上传
// const formData = new FormData();
// formData.append('image', blob, 'canvas.png');
}, 'image/png');
}4.7 Canvas 动画循环
使用 requestAnimationFrame 替代 setInterval 或 setTimeout,因为它:
- 与浏览器渲染帧同步(通常 60fps)
- 页面不可见时自动暂停,节省 CPU
- 更省电,动画更流畅
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 粒子系统示例
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 4;
this.vy = (Math.random() - 0.5) * 4;
this.size = Math.random() * 6 + 2;
this.color = `hsl(${Math.random() * 360}, 70%, 55%)`;
this.life = 1;
this.decay = 0.005 + Math.random() * 0.015;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life -= this.decay;
this.size *= 0.995;
}
draw(ctx) {
ctx.globalAlpha = Math.max(0, this.life);
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
const particles = [];
let animFrameId;
function animate() {
// 清空画布(带拖尾效果:用半透明填充替代 clearRect)
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成新粒子
if (particles.length < 150) {
particles.push(new Particle(
canvas.width / 2 + (Math.random() - 0.5) * 40,
canvas.height / 2 + (Math.random() - 0.5) * 40
));
}
// 更新和绘制粒子
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
particles[i].draw(ctx);
if (particles[i].life <= 0 || particles[i].size < 0.5) {
particles.splice(i, 1);
}
}
animFrameId = requestAnimationFrame(animate);
}
// 启动动画
animate();
// 停止动画
// cancelAnimationFrame(animFrameId);4.8 Canvas vs SVG 对比
| 维度 | Canvas | SVG |
|---|---|---|
| 渲染模型 | 立即模式(像素) | 保留模式(矢量对象树) |
| 清晰度 | 受分辨率限制 | 无限缩放 |
| 对象操作 | 需重绘整个画布 | DOM 操作单个元素 |
| 事件处理 | 需手动计算命中(isPointInPath) | 原生 DOM 事件 |
| 性能(大量元素) | 极优(>1000 元素) | 下降明显(>1000 元素) |
| 性能(少量元素 + 动画) | 优 | 优 |
| 文本渲染 | 有限(无自动换行) | 完整(与 HTML 一致) |
| 无障碍 | 差(屏幕阅读器无法读取) | 好(可加 aria-label) |
| 导出 | toDataURL / toBlob | 序列化为 SVG 字符串 |
| 适用场景 | 游戏、图像处理、数据可视化(大量点)、实时视频处理 | 图标、图表、UI 组件、交互式地图、Logo |
选择黄金法则:
如果元素数量 < 500 且需要交互 → SVG
如果元素数量 > 500 或需要像素操作 → Canvas
如果两样都要 → Canvas 上层叠 SVG 层五、Video 与 Audio
5.1 <video> 元素属性
<video
src="movie.mp4"
controls
width="720"
height="404"
poster="thumbnail.jpg"
preload="metadata"
muted
loop
>
您的浏览器不支持视频播放。
</video>核心属性详解:
| 属性 | 值 | 说明 |
|---|---|---|
src |
URL | 视频源地址 |
controls |
布尔 | 显示浏览器原生控制栏 |
autoplay |
布尔 | 自动播放(被大多数浏览器限制,需配合 muted) |
muted |
布尔 | 静音 |
loop |
布尔 | 循环播放 |
poster |
URL | 封面图 |
preload |
none / metadata / auto |
预加载策略 |
playsinline |
布尔 | iOS Safari 内联播放 |
crossorigin |
anonymous / use-credentials |
CORS 设置 |
自动播放三原则(现代浏览器):
1. 必须 muted(静音)
2. 必须有 user gesture(用户点击)
3. 或者浏览器判定用户"经常在此站播放媒体"完整的多格式回退示例:
<video controls width="720" height="404" poster="poster.jpg">
<!-- 按顺序尝试,直到找到可播放格式 -->
<source src="movie.av1.mp4" type='video/mp4; codecs="av01.0.05M.08"'>
<source src="movie.hevc.mp4" type='video/mp4; codecs="hvc1"'>
<source src="movie.webm" type="video/webm">
<source src="movie.mp4" type="video/mp4">
<!-- 字幕 -->
<track src="subtitles_zh.vtt" kind="subtitles" srclang="zh" label="中文">
<track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English">
<!-- 终极回退 -->
<p>
您的浏览器不支持 HTML5 视频。
<a href="movie.mp4">下载视频文件</a>
</p>
</video>5.2 <track> 字幕与 WebVTT
WebVTT(Web Video Text Tracks)格式:
WEBVTT
00:00:01.000 --> 00:00:04.000
大家好,欢迎来到本教程
00:00:04.500 --> 00:00:08.000
今天我们来学习 HTML 多媒体技术
00:00:09.000 --> 00:00:12.500
首先是 Canvas 画布
00:00:15.000 --> 00:00:20.000 align:end
<v 旁白>这段是旁白说明</v>
00:00:22.000 --> 00:00:28.000
支持 <b>加粗</b> <i>斜体</i> 和 <c.textHighlight>高亮</c> 样式<track> 的 kind 属性:
| kind | 用途 |
|---|---|
subtitles |
翻译字幕 |
captions |
听障字幕(含音效描述) |
descriptions |
视频内容语音描述 |
chapters |
章节导航 |
metadata |
元数据轨道(供脚本读取) |
// JS 控制字幕切换
const video = document.querySelector('video');
const tracks = video.textTracks;
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
// 切换到中文
if (track.language === 'zh') {
track.mode = 'showing';
} else {
track.mode = 'hidden';
}
}
// 监听字幕队列变化
video.addEventListener('cuechange', function() {
const activeCues = video.textTracks[0].activeCues;
for (let i = 0; i < activeCues.length; i++) {
console.log('当前字幕:', activeCues[i].text);
}
});5.3 JavaScript 媒体 API
const video = document.querySelector('video');
// ----- 播放控制 -----
function playVideo() {
const playPromise = video.play();
// play() 返回 Promise
if (playPromise !== undefined) {
playPromise
.then(() => console.log('播放开始'))
.catch(error => {
// 自动播放被阻止
console.error('播放失败:', error);
showPlayButton(); // 显示自定义播放按钮
});
}
}
function pauseVideo() { video.pause(); }
// ----- 属性控制 -----
function setVolume(level) {
video.volume = Math.max(0, Math.min(1, level));
}
function setPlaybackRate(rate) {
video.playbackRate = rate; // 0.5 慢速, 1 正常, 2 两倍速
}
function seekTo(seconds) {
video.currentTime = seconds;
}
function toggleMute() { video.muted = !video.muted; }
// ----- 全屏 API -----
function toggleFullscreen() {
if (!document.fullscreenElement) {
video.requestFullscreen();
} else {
document.exitFullscreen();
}
}
// iOS Safari 兼容
function toggleWebkitFullscreen() {
if (video.webkitSupportsFullscreen) {
video.webkitEnterFullscreen();
}
}
// ----- 自定义进度条 -----
const progressBar = document.getElementById('progress');
video.addEventListener('timeupdate', function() {
if (video.duration) {
const percent = (video.currentTime / video.duration) * 100;
progressBar.value = percent;
progressBar.style.setProperty('--progress', percent + '%');
}
});
progressBar.addEventListener('input', function() {
const time = (this.value / 100) * video.duration;
video.currentTime = time;
});5.4 媒体事件
HTML5 媒体元素触发丰富的事件,构建自定义播放器时非常有用。
const video = document.querySelector('video');
// 加载事件
video.addEventListener('loadstart', () => console.log('开始加载媒体'));
video.addEventListener('loadedmetadata', () => {
console.log('已加载元数据');
console.log(`时长: ${video.duration}s`);
console.log(`分辨率: ${video.videoWidth}x${video.videoHeight}`);
});
video.addEventListener('loadeddata', () => console.log('已加载第一帧'));
video.addEventListener('canplay', () => console.log('可以播放(可能需缓冲)'));
video.addEventListener('canplaythrough', () => console.log('可以完整播放(不需再缓冲)'));
// 播放事件
video.addEventListener('play', () => console.log('播放'));
video.addEventListener('pause', () => console.log('暂停'));
video.addEventListener('ended', () => console.log('播放结束'));
// 进度事件
video.addEventListener('timeupdate', () => {
// 每秒触发 4 次(默认频率)
// 可用此事件驱动进度条
});
video.addEventListener('waiting', () => {
// 因缓冲不足而等待
showBufferingSpinner();
});
video.addEventListener('playing', () => {
// 缓冲足够,继续播放
hideBufferingSpinner();
});
// 缓冲进度
video.addEventListener('progress', () => {
const buffered = video.buffered;
if (buffered.length > 0) {
const end = buffered.end(buffered.length - 1);
const percent = (end / video.duration) * 100;
updateBufferBar(percent);
}
});
// 错误事件
video.addEventListener('error', () => {
const error = video.error;
switch (error.code) {
case MediaError.MEDIA_ERR_ABORTED:
console.error('播放中止');
break;
case MediaError.MEDIA_ERR_NETWORK:
console.error('网络错误');
break;
case MediaError.MEDIA_ERR_DECODE:
console.error('解码错误');
break;
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
console.error('格式不支持');
break;
}
});
// 音量/速率变化
video.addEventListener('volumechange', () => {
console.log(`音量: ${video.volume}, 静音: ${video.muted}`);
});
video.addEventListener('ratechange', () => {
console.log(`播放速度: ${video.playbackRate}`);
});
// 视口可见性(自动暂停不可见图)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
video.pause();
}
});5.5 自适应流式传输简介
HLS(HTTP Live Streaming)
Apple 开发的流媒体协议,将视频切分成 10s 的 TS 片段 + 一个 m3u8 索引文件。
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
360p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=854x480
480p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p.m3u8// 通过 hls.js 在非 Safari 浏览器播放 HLS
import Hls from 'hls.js';
const video = document.getElementById('video');
const hlsUrl = 'https://example.com/stream.m3u8';
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play();
});
// 监听自适应切换
hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
console.log(`切换到 ${data.level} 码率`);
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 原生支持
video.src = hlsUrl;
}DASH(Dynamic Adaptive Streaming over HTTP)
MPEG-DASH 是国际标准,使用 MP4 片段 + MPD 清单文件。
// 通过 dash.js 播放
import dashjs from 'dashjs';
const player = dashjs.MediaPlayer().create();
player.initialize(
document.getElementById('video'),
'https://example.com/stream.mpd',
true // autoPlay
);为什么需要自适应流:
- 自适应码率:根据网络状况自动切换画质
- 直播支持:低延迟直播
- DRM:数字版权保护
- 广告插入:在清单层面无缝插入
5.6 <audio> 基础
<audio> 的 API 与 <video> 几乎完全一致。
<audio controls preload="metadata">
<source src="music.mp3" type="audio/mpeg">
<source src="music.ogg" type="audio/ogg">
<source src="music.flac" type="audio/flac">
您的浏览器不支持音频播放。
</audio>音频可视化(结合 Canvas):
const audio = document.querySelector('audio');
const canvas = document.getElementById('audioVisualizer');
const ctx = canvas.getContext('2d');
// 创建 AudioContext 和 AnalyserNode
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const source = audioCtx.createMediaElementSource(audio);
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
analyser.connect(audioCtx.destination);
const bufferLength = analyser.frequencyBinCount; // fftSize / 2
const dataArray = new Uint8Array(bufferLength);
function visualize() {
requestAnimationFrame(visualize);
analyser.getByteFrequencyData(dataArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i] / 2;
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
// 用户交互才启动 AudioContext(浏览器策略)
document.getElementById('startBtn').addEventListener('click', () => {
audioCtx.resume();
audio.play();
visualize();
});5.7 MediaRecorder API:录音与录屏
// ----- 录音 -----
let mediaRecorder;
let audioChunks = [];
async function startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
});
audioChunks = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
const audioUrl = URL.createObjectURL(audioBlob);
// 创建可播放的音频元素
const audio = new Audio(audioUrl);
audio.controls = true;
document.body.appendChild(audio);
// 上传到服务器
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.webm');
// await fetch('/upload', { method: 'POST', body: formData });
};
mediaRecorder.start();
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
}
// ----- 录屏 -----
async function startScreenCapture() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { mediaSource: 'screen' },
audio: true
});
const recorder = new MediaRecorder(stream);
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
const video = document.createElement('video');
video.src = url;
video.controls = true;
video.width = 640;
document.body.appendChild(video);
};
recorder.start();
// 停止按钮
setTimeout(() => recorder.stop(), 10000); // 录 10s
} catch (err) {
console.error('录屏失败:', err);
}
}六、WebGL 简介
WebGL(Web Graphics Library)是 Canvas 的 3D 上下文,基于 OpenGL ES 2.0/3.0。它通过 GPU 加速实现高性能 3D 渲染。
6.1 获取 WebGL 上下文
const canvas = document.getElementById('glCanvas');
let gl;
try {
gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
} catch (e) {
console.error('WebGL 不支持');
}
if (!gl) {
alert('您的浏览器不支持 WebGL');
}6.2 原生 WebGL 示例:渲染一个三角形
原生 WebGL 使用着色器(Shader)编程。我们来看一个最简示例:
<canvas id="glCanvas" width="500" height="500"></canvas>
<script>
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
// 顶点着色器(GLSL)
const vertexShaderSource = `
attribute vec2 aPosition;
void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
}
`;
// 片段着色器(GLSL)
const fragmentShaderSource = `
precision mediump float;
uniform vec4 uColor;
void main() {
gl_FragColor = uColor;
}
`;
// 编译着色器
function createShader(gl, source, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('着色器编译错误:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
// 创建程序
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('程序链接错误:', gl.getProgramInfoLog(program));
}
gl.useProgram(program);
// 顶点数据:三角形
const vertices = new Float32Array([
0.0, 0.5, // 上
-0.5, -0.5, // 左下
0.5, -0.5 // 右下
]);
// 创建缓冲区
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 获取 attribute 位置并启用
const aPosition = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
// 设置颜色 uniform
const uColor = gl.getUniformLocation(program, 'uColor');
gl.uniform4f(uColor, 0.29, 0.57, 0.86, 1.0); // #4A90D9
// 清除并绘制
gl.clearColor(0.1, 0.1, 0.15, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>6.3 Three.js 示例
实际项目中通常使用 Three.js 等库,封装了着色器和矩阵计算。
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.0/build/three.module.js"
}
}
</script>
<canvas id="threeCanvas" width="800" height="600"></canvas>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'https://unpkg.com/three@0.164.0/examples/jsm/controls/OrbitControls.js';
// 1. 场景(Scene)
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
// 2. 相机(Camera)
const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
camera.position.set(3, 2, 5);
camera.lookAt(0, 0, 0);
// 3. 渲染器(Renderer)
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('threeCanvas'),
antialias: true
});
renderer.setSize(800, 600);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// 4. 轨道控制器(交互)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 5. 灯光
// 环境光
const ambientLight = new THREE.AmbientLight(0x404060);
scene.add(ambientLight);
// 主光源(投射阴影)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
scene.add(directionalLight);
// 补光
const fillLight = new THREE.DirectionalLight(0x8888ff, 0.5);
fillLight.position.set(-5, 0, 5);
scene.add(fillLight);
// 6. 物体
// 旋转的环面结
const geometry = new THREE.TorusKnotGeometry(1, 0.3, 100, 16);
const material = new THREE.MeshStandardMaterial({
color: 0x4A90D9,
metalness: 0.7,
roughness: 0.2,
emissive: 0x1a237e,
emissiveIntensity: 0.1
});
const torusKnot = new THREE.Mesh(geometry, material);
torusKnot.castShadow = true;
torusKnot.receiveShadow = true;
torusKnot.position.y = 1;
scene.add(torusKnot);
// 地面
const floorGeometry = new THREE.PlaneGeometry(8, 8);
const floorMaterial = new THREE.MeshStandardMaterial({
color: 0x2a2a3e,
roughness: 0.8,
metalness: 0.2,
transparent: true,
opacity: 0.9
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.5;
floor.receiveShadow = true;
scene.add(floor);
// 网格辅助线
const gridHelper = new THREE.GridHelper(8, 20, 0x666688, 0x444466);
gridHelper.position.y = -0.49;
scene.add(gridHelper);
// 7. 动画循环
function animate() {
requestAnimationFrame(animate);
// 自动旋转
torusKnot.rotation.x += 0.01;
torusKnot.rotation.y += 0.015;
controls.update();
renderer.render(scene, camera);
}
animate();
// 8. 窗口自适应
window.addEventListener('resize', () => {
const width = 800;
const height = 600;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
});
</script>6.4 WebGL 能力检测与回退
function getWebGLSupport() {
const testCanvas = document.createElement('canvas');
const gl = testCanvas.getContext('webgl')
|| testCanvas.getContext('experimental-webgl');
if (!gl) return { supported: false, version: null };
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
return {
supported: true,
version: gl.getParameter(gl.VERSION),
vendor: debugInfo
? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)
: '未知',
renderer: debugInfo
? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
: '未知',
shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
maxVertexAttribs: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
extensions: gl.getSupportedExtensions()
};
}
function createRenderer(canvasId, fallbackType = 'canvas') {
// WebGL 优先
const canvas = document.getElementById(canvasId);
const gl = canvas.getContext('webgl')
|| canvas.getContext('experimental-webgl');
if (gl) {
return { type: 'webgl', context: gl };
}
// 回退到 Canvas 2D
if (fallbackType === 'canvas') {
const ctx = canvas.getContext('2d');
if (ctx) {
return { type: 'canvas2d', context: ctx };
}
}
// 回退到 SVG
if (fallbackType === 'svg') {
// 创建 SVG 替代
return { type: 'svg', context: null };
}
return { type: 'none', context: null };
}七、综合实战:搭建一个多媒体仪表盘
将上述知识点融合——一个包含 Canvas 粒子背景、SVG 矢量图表、视频播放器和音频可视化器的综合示例。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多媒体仪表盘</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f23;
color: #fff;
min-height: 100vh;
overflow-x: hidden;
}
#bgCanvas {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
pointer-events: none;
}
.dashboard {
position: relative;
z-index: 1;
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-top: 30px;
}
.card {
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 24px;
}
.card h2 {
font-size: 18px;
margin-bottom: 16px;
color: #88aaff;
}
.card video,
.card canvas {
width: 100%;
border-radius: 8px;
}
@media (max-width: 768px) {
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- Canvas 粒子背景 -->
<canvas id="bgCanvas"></canvas>
<div class="dashboard">
<h1>🎛️ 多媒体仪表盘</h1>
<p>Canvas 粒子背景 + SVG 图表 + 视频播放 + 音频可视化</p>
<div class="grid">
<!-- SVG 图表 -->
<div class="card">
<h2>📊 SVG 柱状图</h2>
<svg viewBox="0 0 400 200" style="width:100%;height:auto;">
<rect x="30" y="150" width="40" height="30" fill="#4A90D9" rx="4">
<animate attributeName="height" values="30;80;30" dur="2s" repeatCount="indefinite" />
<animate attributeName="y" values="150;100;150" dur="2s" repeatCount="indefinite" />
</rect>
<rect x="100" y="120" width="40" height="60" fill="#E74C3C" rx="4">
<animate attributeName="height" values="60;110;60" dur="2.5s" repeatCount="indefinite" />
<animate attributeName="y" values="120;70;120" dur="2.5s" repeatCount="indefinite" />
</rect>
<rect x="170" y="80" width="40" height="100" fill="#2ECC71" rx="4">
<animate attributeName="height" values="100;140;100" dur="1.8s" repeatCount="indefinite" />
<animate attributeName="y" values="80;40;80" dur="1.8s" repeatCount="indefinite" />
</rect>
<rect x="240" y="100" width="40" height="80" fill="#F39C12" rx="4">
<animate attributeName="height" values="80;120;80" dur="3s" repeatCount="indefinite" />
<animate attributeName="y" values="100;60;100" dur="3s" repeatCount="indefinite" />
</rect>
<rect x="310" y="130" width="40" height="50" fill="#9B59B6" rx="4">
<animate attributeName="height" values="50;90;50" dur="2.2s" repeatCount="indefinite" />
<animate attributeName="y" values="130;90;130" dur="2.2s" repeatCount="indefinite" />
</rect>
</svg>
</div>
<!-- 视频播放器 -->
<div class="card">
<h2>🎬 视频播放器</h2>
<video controls width="100%" poster="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='225'%3E%3Crect width='400' height='225' fill='%23333'/%3E%3Ctext x='200' y='120' text-anchor='middle' fill='%23fff' font-size='18'%3E示例视频%3C/text%3E%3C/svg%3E">
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
您的浏览器不支持视频播放。
</video>
</div>
<!-- 音频可视化 -->
<div class="card">
<h2>🎵 音频可视化</h2>
<canvas id="audioVis" width="350" height="120"></canvas>
<audio id="audioPlayer" controls style="width:100%;margin-top:12px;">
<source src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" type="audio/mpeg">
</audio>
</div>
<!-- Canvas 时钟 -->
<div class="card">
<h2>🕐 Canvas 时钟</h2>
<canvas id="clockCanvas" width="300" height="300" style="display:block;margin:0 auto;"></canvas>
</div>
</div>
</div>
<script>
// ========== 粒子背景 ==========
const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
let particles = [];
function resizeBgCanvas() {
bgCanvas.width = window.innerWidth;
bgCanvas.height = window.innerHeight;
}
resizeBgCanvas();
window.addEventListener('resize', resizeBgCanvas);
class Star {
constructor() {
this.reset();
}
reset() {
this.x = Math.random() * bgCanvas.width;
this.y = Math.random() * bgCanvas.height;
this.size = Math.random() * 2 + 0.5;
this.speedX = (Math.random() - 0.5) * 0.3;
this.speedY = (Math.random() - 0.5) * 0.3;
this.opacity = Math.random() * 0.8 + 0.2;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
if (this.x < 0 || this.x > bgCanvas.width ||
this.y < 0 || this.y > bgCanvas.height) {
this.reset();
}
}
draw() {
bgCtx.beginPath();
bgCtx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
bgCtx.fillStyle = `rgba(200, 220, 255, ${this.opacity})`;
bgCtx.fill();
}
}
for (let i = 0; i < 100; i++) {
particles.push(new Star());
}
function animateBg() {
bgCtx.clearRect(0, 0, bgCanvas.width, bgCanvas.height);
particles.forEach(p => { p.update(); p.draw(); });
// 连线
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 100) {
bgCtx.beginPath();
bgCtx.moveTo(particles[i].x, particles[i].y);
bgCtx.lineTo(particles[j].x, particles[j].y);
bgCtx.strokeStyle = `rgba(100, 150, 255, ${0.1 * (1 - dist / 100)})`;
bgCtx.lineWidth = 0.5;
bgCtx.stroke();
}
}
}
requestAnimationFrame(animateBg);
}
animateBg();
// ========== 音频可视化 ==========
const audioVisCanvas = document.getElementById('audioVis');
const audioVisCtx = audioVisCanvas.getContext('2d');
const audio = document.getElementById('audioPlayer');
let audioCtx, analyser, sourceNode;
let isAudioInitialized = false;
function initAudioVisualizer() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
sourceNode = audioCtx.createMediaElementSource(audio);
analyser = audioCtx.createAnalyser();
analyser.fftSize = 128;
sourceNode.connect(analyser);
analyser.connect(audioCtx.destination);
isAudioInitialized = true;
}
function drawAudioVis() {
requestAnimationFrame(drawAudioVis);
if (!isAudioInitialized || audio.paused) {
audioVisCtx.fillStyle = 'rgba(15, 15, 35, 0.1)';
audioVisCtx.fillRect(0, 0, audioVisCanvas.width, audioVisCanvas.height);
return;
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
audioVisCtx.fillStyle = 'rgba(15, 15, 35, 0.2)';
audioVisCtx.fillRect(0, 0, audioVisCanvas.width, audioVisCanvas.height);
const barWidth = audioVisCanvas.width / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * audioVisCanvas.height;
const hue = (i / bufferLength) * 200 + 200;
audioVisCtx.fillStyle = `hsl(${hue}, 80%, 60%)`;
audioVisCtx.fillRect(x, audioVisCanvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
}
audio.addEventListener('play', () => {
if (!isAudioInitialized) initAudioVisualizer();
audioCtx.resume();
});
drawAudioVis();
// ========== Canvas 时钟 ==========
const clockCanvas = document.getElementById('clockCanvas');
const clockCtx = clockCanvas.getContext('2d');
function drawClock() {
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
clockCtx.clearRect(0, 0, 300, 300);
// 表盘
clockCtx.beginPath();
clockCtx.arc(150, 150, 130, 0, Math.PI * 2);
clockCtx.fillStyle = 'rgba(255,255,255,0.05)';
clockCtx.fill();
clockCtx.strokeStyle = 'rgba(136, 170, 255, 0.6)';
clockCtx.lineWidth = 3;
clockCtx.stroke();
// 刻度
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 2 - Math.PI / 2;
const outerX = 150 + 120 * Math.cos(angle);
const outerY = 150 + 120 * Math.sin(angle);
const innerX = 150 + 105 * Math.cos(angle);
const innerY = 150 + 105 * Math.sin(angle);
clockCtx.beginPath();
clockCtx.moveTo(innerX, innerY);
clockCtx.lineTo(outerX, outerY);
clockCtx.strokeStyle = i % 3 === 0 ? '#88aaff' : 'rgba(136, 170, 255, 0.4)';
clockCtx.lineWidth = i % 3 === 0 ? 3 : 1.5;
clockCtx.stroke();
}
// 时针
const hourAngle = (hours / 12) * Math.PI * 2 + (minutes / 60) * (Math.PI / 6) - Math.PI / 2;
clockCtx.beginPath();
clockCtx.moveTo(150, 150);
clockCtx.lineTo(150 + 60 * Math.cos(hourAngle), 150 + 60 * Math.sin(hourAngle));
clockCtx.strokeStyle = '#4A90D9';
clockCtx.lineWidth = 5;
clockCtx.lineCap = 'round';
clockCtx.stroke();
// 分针
const minAngle = (minutes / 60) * Math.PI * 2 - Math.PI / 2;
clockCtx.beginPath();
clockCtx.moveTo(150, 150);
clockCtx.lineTo(150 + 90 * Math.cos(minAngle), 150 + 90 * Math.sin(minAngle));
clockCtx.strokeStyle = '#E74C3C';
clockCtx.lineWidth = 3;
clockCtx.lineCap = 'round';
clockCtx.stroke();
// 秒针
const secAngle = (seconds / 60) * Math.PI * 2 - Math.PI / 2;
clockCtx.beginPath();
clockCtx.moveTo(150, 150);
clockCtx.lineTo(150 + 100 * Math.cos(secAngle), 150 + 100 * Math.sin(secAngle));
clockCtx.strokeStyle = '#2ECC71';
clockCtx.lineWidth = 1.5;
clockCtx.lineCap = 'round';
clockCtx.stroke();
// 中心点
clockCtx.beginPath();
clockCtx.arc(150, 150, 5, 0, Math.PI * 2);
clockCtx.fillStyle = '#fff';
clockCtx.fill();
requestAnimationFrame(drawClock);
}
drawClock();
</script>
</body>
</html>八、性能最佳实践
8.1 Canvas 性能优化
// 1. 离屏 Canvas(预渲染静态内容)
const offscreen = document.createElement('canvas');
offscreen.width = 800;
offscreen.height = 600;
const offCtx = offscreen.getContext('2d');
// 绘制一次静态内容
offCtx.fillStyle = '#3498DB';
offCtx.fillRect(0, 0, 800, 600);
// 在主画布中快速复制
ctx.drawImage(offscreen, 0, 0);
// 2. 分层 Canvas
// 静态层(少更新) + 动态层(频繁更新)
// 3. 避免频繁设置 strokeStyle/fillStyle
// 坏:
for (let i = 0; i < 1000; i++) {
ctx.fillStyle = `hsl(${i}, 50%, 50%)`;
ctx.fillRect(i, 0, 1, 100);
}
// 好:批量设置
ctx.fillStyle = 'red';
for (let i = 0; i < 1000; i += 2) {
ctx.fillRect(i, 0, 2, 100);
}
// 4. 使用 requestAnimationFrame 而非 setInterval
// 5. 动画时避免 clearRect,用 fillRect(半透明) 实现拖尾
// 6. 关闭 Canvas 抗锯齿(性能需求)
ctx.imageSmoothingEnabled = false;
// 7. 对不需要透明的绘制关闭全局透明度
// ctx.globalAlpha = 1; // 默认8.2 SVG 性能优化
// 1. 精简 SVG(使用 SVGO 等工具)
// svgo icon.svg -o icon.min.svg
// 2. 使用 CSS 动画替代 SMIL(更高效)
// 3. 限制 DOM 节点数(< 5000)
// 4. 复杂图形使用 <canvas> 渲染后转为图片
// 5. SVG 精灵图合并多个 SVG 为一份文件
// 6. 对于不会变化的 SVG,用 <img> 或 CSS background 替代内联8.3 媒体性能
// 1. 预加载关键视频
// <link rel="preload" as="video" href="hero.mp4">
// 2. 使用 poster 占位,延迟视频实际加载
// preload="metadata"
// 3. 视口外暂停
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const video = entry.target;
if (entry.isIntersecting) {
video.play();
} else {
video.pause();
}
});
}, { threshold: 0.3 });
document.querySelectorAll('video[data-autopause]').forEach(v => observer.observe(v));
// 4. 释放资源
video.removeAttribute('src');
video.load(); // 清空九、总结
HTML5 多媒体 API 构成了现代 Web 交互体验的基石:
- 图片:从
srcset/picture到 AVIF / WebP,配合懒加载与 CDN,构成了高性能的图像管道。 - SVG:无限缩放的矢量世界,通过内联方式获得 CSS/JS 完全控制,精灵图技术大幅优化 HTTP 请求。
- Canvas:像素级的绘图引擎,适合游戏、数据可视化、图像处理等高性能场景。
requestAnimationFrame是其动画核心。 - Video & Audio:原生媒体播放从
<video>/<audio>到 MediaRecorder,从 WebVTT 字幕到 HLS / DASH 自适应流。 - WebGL:通过 Three.js 等库,3D 渲染触手可及。
选择策略:
✅ 图标/Logo/简单交互 → SVG
✅ 数据可视化 (<500 元素) → SVG
✅ 数据可视化 (>500 元素) → Canvas
✅ 游戏/滤镜/图像处理 → Canvas
✅ 视频播放/字幕 → Video API
✅ 3D 渲染 → WebGL (Three.js)
✅ 录音/录屏 → MediaRecorder
✅ 需要像素操作 → Canvas getImageData掌握这些技术,你就拥有了构建下一代 Web 应用的完整多媒体工具箱。