将一张普通的图片变形为扇形图片?
核心原理
- UV坐标系转换:将矩形图片的笛卡尔坐标(x, y)转换为极坐标(半径
r,角度θ)。 - 扇形区域筛选:根据设定的起始角度、结束角度、内外半径筛选有效像素。
- 逆向采样:通过目标像素的极坐标,反向计算其在原图中的对应位置,避免图像断裂。
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)