JS实现图片转为扇形

226 阅读2分钟

将一张普通的图片变形为扇形图片?

核心原理

  1. UV坐标系转换:将矩形图片的笛卡尔坐标(x, y)转换为极坐标(半径r,角度θ)。
  2. 扇形区域筛选:根据设定的起始角度、结束角度、内外半径筛选有效像素。
  3. 逆向采样:通过目标像素的极坐标,反向计算其在原图中的对应位置,避免图像断裂。

HTML部分

<input type="file" id="uploader" accept="image/jpeg" />
<canvas id="outputCanvas"></canvas>

Javascript部分

// 抗锯齿处理,在边缘添加插值计算,避免锯齿
// 使用双线性插值替代直接取整
const getPixel = (data, x, y, width) => {
  const x1 = Math.floor(x),
    y1 = Math.floor(y)
  const x2 = x1 + 1,
    y2 = y1 + 1
  const dx = x - x1,
    dy = y - y1

  // 边界检查
  if (x1 < 0 || x2 >= width || y1 < 0 || y2 >= width) return [0, 0, 0, 0]

  const idx = (y1 * width + x1) * 4
  const a = data.slice(idx, idx + 4)
  const b = data.slice(idx + 4, idx + 8)
  const c = data.slice((y2 * width + x1) * 4, (y2 * width + x1) * 4 + 4)
  const d = data.slice((y2 * width + x2) * 4, (y2 * width + x2) * 4 + 4)

  // 插值计算
  return [
    (a[0] * (1 - dx) + b[0] * dx) * (1 - dy) +
      (c[0] * (1 - dx) + d[0] * dx) * dy,
    (a[1] * (1 - dx) + b[1] * dx) * (1 - dy) +
      (c[1] * (1 - dx) + d[1] * dx) * dy,
    (a[2] * (1 - dx) + b[2] * dx) * (1 - dy) + c[2] * (1 - dx) * dy,
    (a[3] * (1 - dx) + b[3] * dx) * (1 - dy) +
      (c[3] * (1 - dx) + d[3] * dx) * dy
  ]
}
const canvas = document.getElementById('outputCanvas')
const ctx = canvas.getContext('2d')
const uploader = document.getElementById('uploader')

uploader.addEventListener('change', e => {
const file = e.target.files[0]
const reader = new FileReader()

reader.onload = event => {
  const img = new Image()
  img.onload = () => {
    // 初始化画布
    canvas.width = img.width
    canvas.height = img.height
    ctx.drawImage(img, 0, 0)

    // 获取像素数据
    const imageData = ctx.getImageData(
      0,
      0,
      canvas.width,
      canvas.height
    )
    const data = imageData.data

    // 创建目标像素数组
    const newData = new Uint8ClampedArray(data.length)

    // 2 * Math.PI
    // 扇形参数
    const startAngle = 0 // 起始角度(弧度)
    const endAngle = Math.PI / 5 // 结束角度(90度)
    const minRadius = 0.6 // 内半径比例(0~1)
    const maxRadius = 0.8 // 外半径比例(0~1)

    // 遍历每个像素
    for (let y = 0; y < canvas.height; y++) {
      for (let x = 0; x < canvas.width; x++) {
        // 转换为以中心为原点的坐标系
        const cx = x - canvas.width / 2
        const cy = y - canvas.height / 2

        // 计算极坐标
        const r = Math.sqrt(cx * cx + cy * cy) / (canvas.width / 2) // 归一化半径
        let theta = Math.atan2(cy, cx) // [-π, π]
        if (theta < 0) theta += 2 * Math.PI // 转换为[0, 2π]

        // 判断是否在扇形区域内
        if (
          theta >= startAngle &&
          theta <= endAngle &&
          r >= minRadius &&
          r <= maxRadius
        ) {
          // 计算原图对应位置(逆向采样)
          const u = (theta - startAngle) / (endAngle - startAngle) // 角度映射到[0,1]
          const v = (r - minRadius) / (maxRadius - minRadius) // 半径映射到[0,1]

          // 原图坐标(反向计算)
          const srcX = Math.round(u * canvas.width)
          const srcY = Math.round((1 - v) * canvas.height) // Y轴翻转

          // 边界检查
          if (
            srcX >= 0 &&
            srcX < canvas.width &&
            srcY >= 0 &&
            srcY < canvas.height
          ) {
            const dstIdx = (y * canvas.width + x) * 4
            const [r, g, b, a] = getPixel(
              data,
              srcX,
              srcY,
              canvas.width
            )
            newData[dstIdx] = r // R
            newData[dstIdx + 1] = g // G
            newData[dstIdx + 2] = b // B
            newData[dstIdx + 3] = a // A
          }
        }
      }
    }

// 应用新像素数据
ctx.putImageData(
  new ImageData(newData, canvas.width, canvas.height),
  0,
  0
)
}
img.src = event.target.result

reader.readAsDataURL(file)