所谓的双面图指的是一张图片有 A / B 两面,某些场景下只能看到 A 面, 某些场景下又只能看到 B 面,虽然是一张图片,但是不同的场景下你会看到完全不同的图片内容。我写的这个生成器就是实现:在黑色背景下,你看到的是图片的 A 面;在白色背景下,你看到的则是图片的 B 面。
其实这个工具很早之前就写了(几年之前),当时做这个的原因是:在 QQ 群里经常有人发一些双面图,没点开的时候明明是一个美女,点开放大查看却变成了一只小狗。觉得挺有趣的,就想着这种图片是如何实现的呢?自己能不能制作出这样的图片?用来恶搞一下应该效果不错?于是就查资料,然后自己写了这么一个图片生成器。
现在 QQ 群里很少有人这么玩了,因为现在新版 QQ 需要点击【查看原图】才能看到双面图效果,不像以前,直接点开图片就能看到双面图效果。虽然可玩性没那么高了,但是这个工具还是有它的价值的。除了 QQ 或许还有其他的应用场景,只是目前还没被发现。
原理讲解:
原理就是查看图片时底色的影响,不同的底色呈现不同的内容,在白底下显示图片黑色像素的那部分,在黑底下则显示图片白色像素的那部分。其实图片的白色像素在白底上也显示了,但是由于颜色相同,我们自动忽略了。
实现步骤:
1- 上传两张图片,一张用于黑色背景时显示内容,一张用于白色背景时显示内容
2- 通过 canvas 拿到两张图片的像素数据
3- 处理像素数据,将两张图片进行去色处理,也就是变成黑/白照片
4- 然后将处理好后的两张图片进行叠加,也就是绘制到同一张 canvas 画布上,从而得到一张新的图片
源码如下
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="telephone=no" name="format-detection" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<title>双面图生成器</title>
<link rel="stylesheet" href="css/style.css" />
<script type="text/javascript">
// 通过控制 rem 处理移动端自适应
(function (win, doc) {
if (!win.addEventListener) return;
let html = document.documentElement;
function setFont() {
let cliWidth = html.clientWidth;
html.style.fontSize = 40 * (cliWidth / 750) + "px";
}
win.addEventListener("resize", setFont, false);
setFont();
})(window, document);
</script>
</head>
<body>
<header>双面图生成器</header>
<div class="img-box">
<div class="img-box-item">
<div class="img-show" id="smallImg">
<input type="file" accept="image/*" id="chooseSmall" class="img-file" />
</div>
<div class="img-txt">缩略图</div>
</div>
<div class="img-box-item">
<div class="img-show" id="largeImg">
<input type="file" accept="image/*" id="chooseLarge" class="img-file" />
</div>
<div class="img-txt">展开图</div>
</div>
</div>
<div>
<input type="button" value="合成图片" class="btn-generate" id="generate" />
</div>
<div class="result" id="result"></div>
<div class="tips">提示:可通过浏览器自带的 【 图片另存为 】 功能来保存生成后的图片</div>
<footer>Power by 鄢云峰! <a href="https://yanyunfeng.com">https://yanyunfeng.com</a></footer>
<script type="text/javascript" src="js/composite.js"></script>
<script>
let $ = (id) => document.getElementById(id);
let chooseSmall = $("chooseSmall");
let chooseLarge = $("chooseLarge");
let smallImg = $("smallImg");
let largeImg = $("largeImg");
let generate = $("generate");
let composite = new Composite("result");
// 处理图片
let handleChoose = function (files, element, mark) {
let file = files[0],
reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
element.style.backgroundImage = `url(${e.target.result})`;
element.style.backgroundSize = "95% 95%";
let img = new Image();
img.src = e.target.result;
mark === "small" ? composite.imgWhite(img) : composite.imgBlack(img);
};
};
// 选择缩略图
chooseSmall.onchange = function (ev) { handleChoose(ev.target.files, smallImg, "small"); };
// 选择展开图
chooseLarge.onchange = function (ev) { handleChoose(ev.target.files, largeImg, "large"); };
// 生成图片
generate.onclick = function () { composite.generate(); };
</script>
</body>
</html>
composite.js 文件
// 构造函数
function Composite (targetId) {
this.whiteUrl = ''
this.blackUrl = ''
this.whiteReady = false
this.blackReady = false
this.maxHeight = 300
this.canvas = document.createElement('canvas')
// target是用来存放合成后的图片的div
// 如果没有指定,则自行创建一个
let target = document.getElementById(targetId)
if (!target) {
let div = document.createElement('div')
div.style.width = '91%'
div.style.height = '14rem'
document.body.appendChild(div)
this.target = div
} else {
this.target = target
}
}
// 生成白底图
Composite.prototype.imgWhite = function (img) {
let that = this
img.onload = function () {
let canvas = that.canvas
let data = that.drawImg(img, canvas)
let pureData = that.processingWhite(canvas, data)
that.whiteUrl = that.draw(pureData, canvas)
that.whiteReady = true
}
}
// 生成黑底图
Composite.prototype.imgBlack = function (img) {
let that = this
img.onload = function () {
let canvas = that.canvas
let data = that.drawImg(img, canvas) // 1. 得到图片数据
let pureData = that.processingBlack(canvas, data) // 2. 图片数据去色
that.blackUrl = that.draw(pureData, canvas) // 3. 将去色后的数据转为图片
that.blackReady = true // 4. 准备就绪
}
}
// 将图片绘制到canvas上,然后得到图片数据
Composite.prototype.drawImg = (img, canvas) => {
let ctx = canvas.getContext("2d")
if (img.height > this.maxHeight) {
// 宽度等比例缩放 *=
img.width *= this.maxHeight / img.height
img.height = this.maxHeight
}
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0, img.width, img.height)
let data = ctx.getImageData(0, 0, img.width, img.height).data
return data
}
// 用imgData绘制
Composite.prototype.draw = (imgData, canvas) => {
let ctx = canvas.getContext("2d")
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.putImageData(imgData, 0, 0)
let imgUrl = canvas.toDataURL("image/png")
return imgUrl
}
// 创建空白canvas画布,得到画布像素数据
Composite.prototype.getBlankCanvasImgData = function (canvas) {
let ctx = canvas.getContext("2d")
return ctx.createImageData(canvas.width, canvas.height)
}
// 处理白底时图片
Composite.prototype.processingWhite = function (canvas, data) {
let blankData = this.getBlankCanvasImgData(canvas)
let blankDataData = blankData.data
for (let i = 0, len = data.length; i < len; i += 4) {
let red = data[i],
green = data[i + 1],
blue = data[i + 2],
alpha = data[i + 3],
light = 0.299 * red + 0.587 * green + 0.114 * blue // 亮度
let k = 130;
blankDataData[i] = light
blankDataData[i + 1] = light
blankDataData[i + 2] = light
blankDataData[i + 3] = alpha * (k - light) / 255
if (light > k) {
blankDataData[i + 3] = 0
}
}
return blankData
}
// 处理黑底时图片
Composite.prototype.processingBlack = function (canvas, data) {
let blankData = this.getBlankCanvasImgData(canvas)
let blankDataData = blankData.data
for (let i = 0, len = data.length; i < len; i += 4) {
let red = data[i],
green = data[i + 1],
blue = data[i + 2],
alpha = data[i + 3],
light = (0.299 * red + 0.587 * green + 0.114 * blue) * 4.5 //亮度
blankDataData[i] = light
blankDataData[i + 1] = light
blankDataData[i + 2] = light
blankDataData[i + 3] = alpha * light / 255 * 0.08
if (light < 150) {
blankDataData[i + 3] = 0
}
}
return blankData
}
// 合成图片
Composite.prototype.generate = function () {
if (!this.whiteReady || !this.blackReady) {
alert("请先上传缩略图和展开图,然后再合成")
return
}
let that = this
let canvas = that.canvas
let img_white = new Image()
let img_black = new Image()
img_white.src = that.whiteUrl
img_black.src = that.blackUrl
img_white.onload = function() {
let ctx = canvas.getContext("2d")
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img_white, 0, 0, canvas.width, canvas.height)
img_black.onload = function() {
let ctx = canvas.getContext("2d")
ctx.drawImage(img_black, 0, 0, canvas.width, canvas.height)
that.appednImage()
}
}
}
// 将合成后的图片添加到页面
Composite.prototype.appednImage = function () {
this.target.innerHTML = ""
this.target.style.backgroundColor = '#FFF'
let img = document.createElement("img")
img.style.width = '100%'
img.style.height = '100%'
img.src = this.canvas.toDataURL("image/png")
this.target.appendChild(img)
}