前提简介
幻影坦克图流传度高得益于使用简单 无需额外软件即可使用
1.png图有rgba四个参数 要用a与背景色混合计算后得出最终rgb 显示屏才能渲染
2.常用软件中png图略缩图背景色为白色 点开后详情图背景色为黑色
原理就是利用a混合背景色渲染出表图和里图
png图渲染原理
png图中的每个像素由red(红) green(绿) blue(蓝) alpha(透明值) 这四个参数组成 也就是rgba
rgb对应显示屏的三原色 显示屏不能直接渲染a
需要一次预渲染 用a与原图rgb以及背景色rgb混合计算后得出最终rgb
- 举个例子
原图rgba=[50, 100, 150, 26] 这里透明值26相当于透明度10%
背景rgb=[255,255,255]
原图预渲染rgb=[50, 100, 150][0.1]=[5, 10, 15]
背景预渲染rgb=[255,255,255][0.9]=[230, 230, 230]
最终渲染rgb=[5, 10, 15]+[230, 230, 230]=[235, 240, 245]
公式为(0=最终图 1=原图 2=背景图)
这里的a为了计算方便 表示的是透明度而不是透明值 透明度=透明值/255
幻影坦克图渲染原理
-
幻影坦克图是两张图混合
白色背景rgb(255,255,255)下显示的是表图
黑色背景rgb(0,0,0)下显示的是里图 -
公式推导
-
由png渲染公式得出他们的关系如下(0=幻影图 1=表图 2=里图)
-
由上述公式继续推导
-
由此可推导出透明度
-
再得出结论
-
-
通过推理公式得到了如下
- 两个大括号内计算rgb和a的公式
- 里图和表图对应位置的像素必须满足里图比表图大
关于彩色图
由于渲染时只有一个透明度 这就导致了必须满足 r2-r1 = g2-g1 = b2-b1
意思是里图与表图的rgb差值必须相等
而彩色是千奇百怪 相等的话显然不够彩色
大部分全彩图不能做到完美的分开表图和里图 非常影响观感
并非不可以彩色 而是只有特定情况下才能实现彩色
比如同一张图 只是颜色不一样 那么这两张图之间的rgb差值是固定的 刚好满足上述推导结论 \
- 结论: 彩色幻影图对表图和里图限制大 适用性小 无需考虑
具体实现思路
-
原图
-
去色
为了让表图和里图rgb的差值相同
一个简单的办法是让r1=g1=b1 r2=g2=b2 直接取三个数的平均值即可
这个过程就是去色变成灰度图
灰度图的rgb相等 下面使用x变量用于表示新像素(1=表图 2=里图) -
色阶
由于里图像素必须比表图的大 所以图片需要经过处理才能计算出合法的a
且图片自身色阶不能大幅度改变 否则人眼就观察不出原图
所以只是简单的改变里图和表图的像素值是不可取的
rgb的取值范围都是0到255 那么一个简单的压缩想法就诞生了
把表图所有像素的最大值都限定到127到255 里图所有像素都限定到0到127但这个方式具有局限性相当于固定压缩力度为50%
比如如果里图原本所有像素都比表图像素大 那么其实不用压缩也可以
更好的方式是遍历一次里图和表图 找到两张图之间同一像素最大的差值 然后以此决定压缩力度
不过实际上大部分场景下 里图和表图同一像素位置的差值几乎都很大
所以也就不折腾 直接设定为255的一半 也就是50%的压缩力度
-
输出
色阶调整完毕后就可以计算最终幻影图的rgba了 -
至此计算过程已全部明朗
代码实现
/**
* 入参 rgba扁平数组 [r, g, b, a, r, g, b, a, ...]
* 出参 同上
*/
const core = (front, back) => {
// 4个一组切割
return Array.from({length: front.length / 4}, (_, i) => i)
.map(i => {
const j = i * 4
const r1 = front[j]
const g1 = front[j + 1]
const b1 = front[j + 2]
const r2 = back[j]
const g2 = back[j + 1]
const b2 = back[j + 2]
// 去色
const x1 = (r1 + g1 + b1) / 3
const x2 = (r2 + g2 + b2) / 3
// 变亮
const x3 = x1 / 2 + 127
// 变暗
const x4 = x2 / 2
// 透明值
const a = Math.max(x4 - x3 + 255, 1)
// 最终值
const x5 = x4 * 255 / a
return [x5, x5, x5, a]
})
.flat()
}
void function(core){
// 元素声明
const style = document.createElement('style')
const canvas = document.createElement('canvas')
const file = document.createElement('input')
const img2 = document.createElement('img')
const img1 = document.createElement('img')
const div3 = document.createElement('div')
const div2 = document.createElement('div')
const div1 = document.createElement('div')
const div0 = document.createElement('div')
// 元素结构
div3.append(style, canvas)
div2.append(img1, img2)
div1.append(file)
div0.append(div1, div2, div3)
document.body.append(div0)
// 元素设置
style.textContent = 'canvas:hover { background: black }'
file.type = 'file'
file.multiple = true
file.accept = 'image/*'
file.onchange = ({target: {files: [f1, f2]}}) => {
if(!f1 || !f2){
alert('请上传两张图')
return
}
const ctx = canvas.getContext('2d', { willReadFrequently: true })
// 并行处理两张图
Promise.all([
new Promise(resolve => {
const reader = new FileReader()
reader.onload = ({target: {result: url}}) => {
img1.src = url
img1.onload = () => resolve()
}
reader.readAsDataURL(f1)
}),
new Promise(resolve => {
const reader = new FileReader()
reader.onload = ({target: {result: url}}) => {
img2.src = url
img2.onload = () => resolve()
}
reader.readAsDataURL(f2)
})
])
// 将结果合并渲染进画布
.then(() => {
const {width: w1, height: h1} = img1
const {width: w2, height: h2} = img2
const w = Math.min(w1, w2)
const h = Math.min(h1, h2)
canvas.width = w
canvas.height = h
ctx.clearRect(0, 0, w, h)
ctx.drawImage(img1, 0, 0)
const front = ctx.getImageData(0, 0, w, h).data
ctx.clearRect(0, 0, w, h)
ctx.drawImage(img2, 0, 0)
const back = ctx.getImageData(0, 0, w, h).data
const data = new ImageData(new Uint8ClampedArray(core(front, back)), w, h)
ctx.putImageData(data, 0, 0)
})
}
}(core)