什么是viewBox
先看MDN官方的解释:
viewBox 属性允许指定一个给定的一组图形伸展以适应特定的容器元素。
viewBox 属性的值是一个包含 4 个参数的列表 min-x, min-y, width and height,以空格或者逗号分隔开,在用户空间中指定一个矩形区域映射到给定的元素,查看属性preserveAspectRatio不允许宽度和高度为负值,0 则禁用元素的呈现。
个人觉得官方的解释其实挺难理解的。
实现一个viewBox就彻底理解了
以下代码借助AI实现: 简单来说就是放大/缩小 + 平移, perserveAspectRatio是用来控制宽高比 不一致的时候的对齐方式
js
class SVGViewBoxRenderer {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.viewBox = null;
this.preserveAspectRatio = 'xMidYMid meet';
this.transformMatrix = null;
}
/**
* 设置viewBox
* @param {string} viewBox - SVG viewBox属性值,如 "0 0 100 100"
*/
setViewBox(viewBox) {
const parts = viewBox.trim().split(/[\s,]+/);
if (parts.length !== 4) {
throw new Error('Invalid viewBox format');
}
this.viewBox = {
x: parseFloat(parts[0]),
y: parseFloat(parts[1]),
width: parseFloat(parts[2]),
height: parseFloat(parts[3])
};
this.updateTransform();
}
/**
* 设置preserveAspectRatio
* @param {string} value - preserveAspectRatio值,如 "xMidYMid meet"
*/
setPreserveAspectRatio(value) {
this.preserveAspectRatio = value.trim();
this.updateTransform();
}
/**
* 更新变换矩阵
*/
updateTransform() {
if (!this.viewBox) return;
const canvasWidth = this.canvas.width;
const canvasHeight = this.canvas.height;
const vb = this.viewBox;
// 计算缩放比例
const scaleX = canvasWidth / vb.width;
const scaleY = canvasHeight / vb.height;
// 解析preserveAspectRatio
const [align, meetOrSlice] = this.preserveAspectRatio.split(/\s+/);
const useMeet = meetOrSlice === 'meet' || !meetOrSlice;
let scale, translateX, translateY;
if (useMeet) {
// meet: 保持宽高比,内容完全可见
scale = Math.min(scaleX, scaleY);
} else {
// slice: 保持宽高比,填满视口
scale = Math.max(scaleX, scaleY);
}
// 计算缩放后的viewBox尺寸
const scaledWidth = vb.width * scale;
const scaledHeight = vb.height * scale;
// 解析align部分
const xAlign = align.substring(1, 4).toLowerCase(); // xMin, xMid, xMax
const yAlign = align.substring(4, 7).toLowerCase(); // YMin, YMid, YMax
// 计算平移
switch (xAlign) {
case 'min':
translateX = 0;
break;
case 'mid':
translateX = (canvasWidth - scaledWidth) / 2;
break;
case 'max':
translateX = canvasWidth - scaledWidth;
break;
default:
translateX = 0;
}
switch (yAlign) {
case 'min':
translateY = 0;
break;
case 'mid':
translateY = (canvasHeight - scaledHeight) / 2;
break;
case 'max':
translateY = canvasHeight - scaledHeight;
break;
default:
translateY = 0;
}
// 存储变换矩阵
this.transformMatrix = {
scale,
translateX,
translateY,
viewBox: vb
};
}
/**
* 应用viewBox变换
*/
applyViewBoxTransform() {
if (!this.transformMatrix) return;
const { scale, translateX, translateY, viewBox } = this.transformMatrix;
// 保存当前状态
this.ctx.save();
// 应用变换
this.ctx.translate(translateX, translateY);
this.ctx.scale(scale, scale);
this.ctx.translate(-viewBox.x, -viewBox.y);
}
/**
* 恢复变换
*/
restoreViewBoxTransform() {
if (this.transformMatrix) {
this.ctx.restore();
}
}
/**
* 将Canvas坐标转换为SVG用户坐标
* @param {number} canvasX - Canvas X坐标
* @param {number} canvasY - Canvas Y坐标
* @returns {{x: number, y: number}}
*/
canvasToSVG(canvasX, canvasY) {
if (!this.transformMatrix) {
return { x: canvasX, y: canvasY };
}
const { scale, translateX, translateY, viewBox } = this.transformMatrix;
// 逆向变换
const svgX = (canvasX - translateX) / scale + viewBox.x;
const svgY = (canvasY - translateY) / scale + viewBox.y;
return { x: svgX, y: svgY };
}
/**
* 将SVG用户坐标转换为Canvas坐标
* @param {number} svgX - SVG X坐标
* @param {number} svgY - SVG Y坐标
* @returns {{x: number, y: number}}
*/
svgToCanvas(svgX, svgY) {
if (!this.transformMatrix) {
return { x: svgX, y: svgY };
}
const { scale, translateX, translateY, viewBox } = this.transformMatrix;
const canvasX = (svgX - viewBox.x) * scale + translateX;
const canvasY = (svgY - viewBox.y) * scale + translateY;
return { x: canvasX, y: canvasY };
}
/**
* 渲染示例内容
*/
renderExample() {
this.applyViewBoxTransform();
// 绘制一个矩形(使用SVG用户坐标)
this.ctx.fillStyle = '#3498db';
this.ctx.fillRect(10, 10, 80, 80);
// 绘制文字
this.ctx.fillStyle = '#2c3e50';
this.ctx.font = '12px Arial';
this.ctx.fillText('SVG Content', 20, 50);
// 绘制一个圆形
this.ctx.beginPath();
this.ctx.arc(150, 50, 30, 0, Math.PI * 2);
this.ctx.fillStyle = '#e74c3c';
this.ctx.fill();
this.restoreViewBoxTransform();
}
/**
* 渲染viewBox边界(调试用)
*/
renderViewBoxBounds() {
if (!this.viewBox) return;
this.ctx.save();
this.ctx.strokeStyle = '#27ae60';
this.ctx.lineWidth = 2 / this.transformMatrix.scale; // 保持线宽一致
this.ctx.setLineDash([5 / this.transformMatrix.scale, 5 / this.transformMatrix.scale]);
// 在SVG用户坐标系中绘制viewBox边界
this.ctx.strokeRect(
this.viewBox.x,
this.viewBox.y,
this.viewBox.width,
this.viewBox.height
);
this.ctx.restore();
}
}
// 使用示例
const canvas = document.getElementById('myCanvas');
const renderer = new SVGViewBoxRenderer(canvas);
// 设置viewBox(例如:SVG viewBox="0 0 200 200")
renderer.setViewBox('0 0 200 200');
// 设置preserveAspectRatio(默认值:xMidYMid meet)
renderer.setPreserveAspectRatio('xMidYMid meet');
// 渲染内容
renderer.renderExample();
renderer.renderViewBoxBounds();
// 坐标转换示例
const canvasPoint = { x: 100, y: 100 };
const svgPoint = renderer.canvasToSVG(canvasPoint.x, canvasPoint.y);
console.log('Canvas坐标:', canvasPoint, '-> SVG坐标:', svgPoint);
最后
本文主要是为了记录一下,以后想不起来可以随时翻阅。
有了AI后可以学到更多有意思的知识了,学无止境。