svg之viewBox

什么是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后可以学到更多有意思的知识了,学无止境。

相关推荐
福兮说4 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理4 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆6665 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
Dovis(誓平步青云)5 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan6 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_7 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒7 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技7 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu8 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
默_笙8 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript