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

相关推荐
吹牛不交税1 小时前
tree-transfer-vue3 前端插件安装问题解决(--legacy-peer-deps)(其他插件可考虑)适用
前端·javascript·vue.js
ricardo19731 小时前
Chrome DevTools + Lighthouse + Performance API:前端性能调优三件套实操指南
前端
Appoint_x1 小时前
设计稿自己会说话:我用 Claude 给 Figma 做了个 AI 上下文插件
前端·javascript
豹哥学前端1 小时前
浏览器console里的双中括号 `[[ ]]`
前端·javascript·ecmascript 6
菜泡泡@1 小时前
npm 安装pnpm之后运行pnpm -v查询报错
前端·npm·node.js
贫民窟的勇敢爷们1 小时前
React跨平台能力,打破前端开发的平台边界
前端·react.js·前端框架
lifejump2 小时前
Dede(织梦)CMS渗透测试(all)
前端·网络·安全·web安全
扬帆破浪2 小时前
sidecar崩溃后前端怎么续命 重启策略与状态保留
前端·人工智能·架构·开源·知识图谱