所谓的双面图指的是一张图片有 A / B 两面,某些场景下只能看到 A 面, 某些场景下又只能看到 B 面,虽然是一张图片,但是不同的场景下你会看到完全不同的图片内容。我写的这个生成器就是实现:在黑色背景下,你看到的是图片的 A 面;在白色背景下,你看到的则是图片的 B 面。


iShot_2024-08-13_16.52.03.png


其实这个工具很早之前就写了(几年之前),当时做这个的原因是:在 QQ 群里经常有人发一些双面图,没点开的时候明明是一个美女,点开放大查看却变成了一只小狗。觉得挺有趣的,就想着这种图片是如何实现的呢?自己能不能制作出这样的图片?用来恶搞一下应该效果不错?于是就查资料,然后自己写了这么一个图片生成器。


iShot_2024-08-13_16.53.07.png


现在 QQ 群里很少有人这么玩了,因为现在新版 QQ 需要点击【查看原图】才能看到双面图效果,不像以前,直接点开图片就能看到双面图效果。虽然可玩性没那么高了,但是这个工具还是有它的价值的。除了 QQ 或许还有其他的应用场景,只是目前还没被发现。


iShot_2024-08-14_10.38.06.png


原理讲解:

原理就是查看图片时底色的影响,不同的底色呈现不同的内容,在白底下显示图片黑色像素的那部分,在黑底下则显示图片白色像素的那部分。其实图片的白色像素在白底上也显示了,但是由于颜色相同,我们自动忽略了。


实现步骤:

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)
}

本文最后更新于 2024-08-14 17:59:31JAVASCRIPT
天生我材必有用,千金散尽还复来~~
作者:鄢云峰 YYF声明:转载请注明文章出处地址:https://yanyunfeng.com/article/58
评论
提交
Comments | 7 条评论
小贺2024-08-14 16:30:39
#1 回复
现在皮皮虾好多人引流就是用的这种图片当头像,不过真的不理解为什么点开看到的和原本的不同
鄢云峰站长2024-08-14 17:49:19
#2 回复
@小贺 原理就是查看图片时底色的影响,不同的底色呈现不同的内容,在白底下显示图片黑色像素的那部分,在黑底下则显示图片白色像素的那部分。其实图片的白色像素在白底上也显示了,但是由于颜色相同,我们自动忽略了。 看文章的最后一张截图,聊天框背景是白色的,而点开后查看背景是黑色的,不同的底色呈现不同的内容。
晨岩2024-08-15 09:21:22
#3 回复
挺有趣的一个小玩意,你这边填写姓名和邮箱的input控件没有name,没办法自动填充😵
鄢云峰站长2024-08-15 09:25:00
#4 回复
@晨岩 收到,收到,晚点我更新下代码!谢谢反馈~
晨岩2024-08-15 09:27:45
#5 回复
@鄢云峰 var lauthor = ["#author","input[name='comname']","#inpName","input[name='author']","#ds-dialog-name","#name","input[name='nick']","#comment_author"], lmail =["#mail","#email","input[name='commail']","#inpEmail","input[name='email']","#ds-dialog-email","input[name='mail']","#comment_email"], lurl =["#url","input[name='comurl']","#inpHomePage","#ds-dialog-url","input[name='url']","input[name='website']","#website","input[name='link']","#comment_url"];这些常见的
鄢云峰站长2024-08-15 09:48:38
#6 回复
@晨岩 好的! 你这条评论帮我发现了一个 Bug ,首页左边的【最新评论】样式乱了😂
晨岩2024-08-15 09:49:50
#7 回复
@鄢云峰 难道我就是传说中的bug圣体?[手动滑稽]