中秋!还记得西游记里的嫦娥吗?我用10000张图片拼成了儿时女神!

7,525 阅读8分钟

我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛

前言

大家好,我是林三心,中秋即将来临,预祝大家中秋快乐!!!我在想,关于中秋,我能写点什么分享给大家呢?这一天,我在看《西游记》,突然想到了我儿时的女神,是谁呢?就是天蓬元帅苦苦追求的嫦娥仙子,那可是我儿时的女神啊。嫦娥奔月的故事我相信大家都听过

前段时间,我看了荣顶大佬的这篇我用 10000 张图片合成我们美好的瞬间,发现原来图片的主色调是那样计算的,学到了很多。于是我站在荣顶巨人的肩膀上,用王者荣耀里嫦娥这一角色的不同图片,加上王者荣耀里后羿这一角色的不同图片(后羿是嫦娥老公),组成了我儿时女神——西游记嫦娥的图像。

开搞!!!

前置准备

由于需要用到canvas,以及一些图片上传按钮,所以咱们先把HTML的代码写好,fabric是一个非常实用的canvas库,他提供了很多api,方便我们更方便地在canvas上画出可操作性的图像。fabric的代码在这里fabric库代码,创建一个文件,复制过来就行

    <!-- 引入fabric这个库 -->
    <script src="./fabric.js"></script>
    <!-- 用来选主图 -->
    <input type="file" id="mainInput" />
    <!-- 用来选组成图片 多选 -->
    <input type="file" id="composeInput" multiple />
    <!-- 生成效果 -->
    <button id="finishBtn">生成组合图</button>
    <!-- 一块800 * 800 的canvas画布 -->
    <canvas id="canvas" width="800" height="800"></div>
const mainInput = document.getElementById('mainInput') // 获取上传主图按钮的DOM
const composeInput = document.getElementById('composeInput') // 获取多传组合图片按钮的DOM
const finishBtn = document.getElementById('finishBtn') // 获取生成最终结果按钮的DOM
const exportBtn = document.getElementById('exportBtn') // 获取倒出图片按钮的DOM
const canvas = new fabric.Canvas('canvas') // 实例一个fabric的canvas对象,传入的是canvas的id
const ctx = canvas.getContext('2d') // 绘制2d图像

画出嫦娥姐姐

咱们需要先在页面上画出嫦娥姐姐的原始图像,图像如下

image.png

那咱们要怎么把一张图像画到HTML页面中呢?答案是canvas,那咱们就先把这个图像绘制到页面上去吧!

咱们都知道,图片直接上传到浏览器,是不可能直接就给你绘制出来的,比如原生的canvas需要把你这张图片转为base64格式才能绘制到页面,而fabric提供了一个fabric.Image.fromURL(url, img => {}),需要传入一个图片的blob地址,才能生成一张可绘制到页面的图片。那咱们怎么把咱们上传的图片转成blob地址呢?其实JavaScript已经给我们提供了这么一个方法window.URL.createObjectURL,用它就能实现啦。

// 监听上传主图按钮的上传变化
mainInput.onchange = function (e) {
    // 只有一个图片,所以是e.target.files[0]
    const url = window.URL.createObjectURL(e.target.files[0])
    // 将生成的blob地址传入
    drawMainImage(url)
}

function drawMainImage(url) {
    // 接收传进来的url
    fabric.Image.fromURL(url, img => {
        console.log(img)
        // 转换成功后的回调
        // fabric.Image.fromURL会将此url转换成一张图片

        // 需要缩放图片,height > width 就按照 width的缩放比例,反之用height的缩放比例
        // 反过来是为了能充满整张图
        const scale = img.height > img.width ? canvas.width / img.width : canvas.height / img.height

        // 设置这张图像绘制的参数
        img.set({
            left: canvas.width / 2, // 距离canvas画板左边一半宽度
            originX: 'center', // 水平方向居中
            top: 0, // 距离顶部距离为0
            scaleX: scale, // 图像水平缩放比例
            scaleY: scale, // 图像竖直缩放比例
            selectable: false // 不可操作,默认是true
        })

        // 把此图像绘制到canvas画板中
        canvas.add(img)
        
        // 图片绘制完成的回调函数
        img.on('added', e => {
            console.log('图片加载完成了啊')
            setTimeout(() => {
                // 绘制完成后,获取此图像中10000个格子的色彩信息,后面会实现
                getMainRGBA()
            }, 200) // 这里用延时器,是因为图像绘制有延迟
                    // 而这里需要保证图像真的完全绘制完,再去获取色彩信息
        })
    })
}

10000个格子

1630998118(1).jpg 咱们都知道,咱们的canvas画布是800 * 800的,咱们想要分成10000个格子,那么每个格子就是8 * 8。实现之前,咱们现在认识一个canvas获取色彩信息的api——ctx.getImageData(x, y, width, height),他接收4个参数

  • x:获取范围的x坐标
  • y:获取范围的y坐标
  • width:获取范围的宽度
  • height:获取范围的高度 他会返回一个对象,对象里有一个属性data,这个data就是此范围的色彩信息,比如
const { data } = ctx.getImageData(40, 40, 8, 8)

截屏2021-09-07 下午6.41.00.png

那么data就是x为40,y为40,宽度高度都是8,这一个范围内的色彩信息,这个色彩信息是一个数组,比如这个范围是8 * 8,那么这个数组就有8 * 8 * 4 = 256个元素,因为8 * 8就有64个像素,而每一个像素的rgba(r, g, b, a)是4个值,所以这个数组就有8 * 8 * 4 = 256个元素,所以下面咱们要4个4个收集,因为每4个元素就是一个像素的rgba,而一个8 * 8的格子,就会有64个像素,也就是64个rgba数组

let mainColors = [] // 用来收集1000个格子的主色调rgba,后面会实现

function getMainRGBA() {
    const rgbas = [] // 用来收集10000个格子的色彩信息
    for (let y = 0; y < canvas.height; y += 8) {
        for (let x = 0; x < canvas.width; x += 8) {
            // 获取每一块格子的色彩data
            const { data } = ctx.getImageData(x, y, 8, 8)
            rgbas[y / 8 * 100 + x / 8] = []
            for (let i = 0; i < data.length; i += 4) {
                // 4个4个收集,因为每4个就组成一个像素的rgba
                rgbas[y / 8 * 100 + x / 8].push([
                    data[i],
                    data[i + 1],
                    data[i + 2],
                    data[i + 3]
                ])
            }
        }
    }
    // 算出10000个格子,每个格子的主色调,后面实现
    mainColors = getMainColorStyle(rgbas)
}

每个格子主色调

上面咱们已经获取到了10000个格子,他们每个格子都拥有64个像素,也就是64个rgba数组,那每个格子拥有64个rgba,咱们怎么才能得到这个格子的主色调呢?很简单嘛,rgba(r, g, b, a)有4个值,咱们算出这4个值各自的平均值,然后组成一个新的rgba,这个rgba就是每个格子的主色调啦!!!

function getMainColorStyle(rgbas) {
    const mainColors = []
    for (let colors of rgbas) {
        let r = 0, g = 0, b = 0, a = 0
        for (let color of colors) {
            // 累加
            r += color[0]
            g += color[1]
            b += color[2]
            a += color[3]
        }
        mainColors.push([
            Math.round(r / colors.length), // 取平均值
            Math.round(g / colors.length), // 取平均值
            Math.round(b / colors.length), // 取平均值
            Math.round(a / colors.length) // 取平均值
        ])
    }
    return mainColors
}

上传组合图片

主图片的功能都实现了,现在就剩组合图片了,咱们可以多传组合图片。但是我们要算出每一张组合图片的主色调,因为后面咱们要主色调来对比那10000个格子的主色调,决定哪个格子放哪张组合图片

这里有一个问题要强调一下,想要获取图片的颜色信息,就得把图片画到canvas画板上才能获取,但是咱们又不想在这里把图片画到页面上的canvas里,咋办呢?咱们可以创建临时的canvas画板,画完,获取完颜色信息,咱们就把它销毁

let composeColors = [] // 收集组合图片主色调

// 监听多选按钮的上传
composeInput.onchange = async function (e) {
    const promises = [] // promises数组
    for (file of e.target.files) {
        // 将每张图片生成blob地址
        const url = window.URL.createObjectURL(file)
        // 传入blob地址
        promises.push(getComposeColorStyle(url, file.name))
    }
    const res = await Promise.all(promises) // 顺序执行所有promise
    composeColors = res // 将结果赋值给composeColors
}

function getComposeColorStyle(url, name) {
    return new Promise(resolve => {
        // 创建一个 20 * 20的canvas画板
        // 理论上这里宽高可以自己定,但是越大,色彩会越精准
        const composeCanvas = document.createElement('canvas')
        const composeCtx = composeCanvas.getContext('2d')
        composeCanvas.width = 20
        composeCanvas.height = 20

        // 创建img对象
        const img = new Image()
        img.src = url
        img.onload = function () {
            const scale = composeCanvas.height / composeCanvas.height
            img.height *= scale
            img.width *= scale

            // 将img画到临时canvas画板
            composeCtx.drawImage(img, 0, 0, composeCanvas.width, composeCanvas.height)
            // 获取颜色信息data
            const { data } = composeCtx.getImageData(0, 0, composeCanvas.width, composeCanvas.height)

            // 累加  r,g,b,a
            let r = 0, g = 0, b = 0, a = 0
            for (let i = 0; i < data.length; i += 4) {
                r += data[i]
                g += data[i + 1]
                b += data[i + 2]
                a += data[i + 3]
            }
            resolve({
                // 主色调
                rgba: [
                    Math.round(r / (data.length / 4)), // 取平均值
                    Math.round(g / (data.length / 4)), // 取平均值
                    Math.round(b / (data.length / 4)), // 取平均值
                    Math.round(a / (data.length / 4)) // 取平均值
                ],
                url,
                name
            })
        }
    })
}

对比主色调并绘制

  • canvas画板中的嫦娥姐姐有10000个格子,每个格子都有自己的主色调
  • 上传的每张组合图片也有自己的主色调

那咱们要怎么实现最终效果呢?很简单嘛!!!遍历10000个格子,拿着每个格子的主色调,去跟每张组合图片的主色调一一对比,最接近色调的图片,就拿来绘制到这个8 * 8的格子里。

// 监听完成按钮
finishBtn.onclick = finishCompose

function finishCompose() {
    const urls = [] // 收集最终绘制的10000张图片

    for (let main of mainColors) { // 遍历10000个格子主色调

        let closestIndex = 0 // 最接近主色调的图片的index
        let minimumDiff = Infinity // 相差值

        for (let i = 0; i < composeColors.length; i++) {
            const { rgba } = composeColors[i]
            // 格子主色调rgba四个值,减去图片主色调rgba四个值,的平方
            const diff = (rgba[0] - main[0]) ** 2 + (rgba[1] - main[1]) ** 2
                + (rgba[2] - main[2]) ** 2 + (rgba[3] - main[3]) ** 2

            // 然后开跟比较
            if (Math.sqrt(diff) < minimumDiff) {
                minimumDiff = Math.sqrt(diff)
                closestIndex = i
            }
        }

        // 把最小色差的图片url添加进数组urls
        urls.push(composeColors[closestIndex].url)
    }


    // 将urls中10000张图片,分别绘制在对应的10000个格子中
    for (let i = 0; i < urls.length; i++) {
        fabric.Image.fromURL(urls[i], img => {
            const scale = img.height > img.width ? 8 / img.width : 8 / img.height;
            img.set({
                left: i % 100 * 8,
                top: Math.floor(i / 100) * 8,
                originX: "center",
                scaleX: scale,
                scaleY: scale,
            });
            canvas.add(img)
        })
    }
}

导出图片

// 监听导出按钮
exportBtn.onclick = exportCanvas

//导出图片
function exportCanvas() {
    const dataURL = canvas.toDataURL({
        width: canvas.width,
        height: canvas.height,
        left: 0,
        top: 0,
        format: "png",
    });
    const link = document.createElement("a");
    link.download = "嫦娥姐姐.png";
    link.href = dataURL;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}

最终效果

嫦娥2.gif

彩蛋

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼

哈哈我用王者荣耀猪八戒的图片,组成了我自己

截屏2021-09-07 下午11.17.26.png

完整代码

截屏2021-09-07 下午7.37.34.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 引入flare这个库 -->
    <script src="./flare.js"></script>
</head>
<body>
    <!-- 用来选主图 -->
    <input type="file" id="mainInput" />
    <!-- 用来选组成图片 多选 -->
    <input type="file" id="composeInput" multiple />
    <!-- 生成效果 -->
    <button id="finishBtn">生成组合图</button>
    <!-- 导出图片 -->
    <button id="exportBtn">导出图片</button>
    <!-- 一块800 * 800 的canvas画布 -->
    <canvas id="canvas" width="800" height="800"></div>
</body>
<script src="./index2.js"></script>
</html>
const mainInput = document.getElementById('mainInput') // 获取上传主图按钮的DOM
const composeInput = document.getElementById('composeInput') // 获取多传组合图片按钮的DOM
const finishBtn = document.getElementById('finishBtn') // 获取生成最终结果按钮的DOM
const exportBtn = document.getElementById('exportBtn') // 获取倒出图片按钮的DOM
const canvas = new fabric.Canvas('canvas') // 实例一个flare的canvas对象,传入的是canvas的id
const ctx = canvas.getContext('2d') // 绘制2d图像

let mainColors = []
let composeColors = []

// 监听上传主图按钮的上传变化
mainInput.onchange = function (e) {
    // 只有一个图片,所以是e.target.files[0]
    const url = window.URL.createObjectURL(e.target.files[0])
    // 将生成的blob地址传入
    drawMainImage(url)
}

composeInput.onchange = async function (e) {
    const promises = [] // promises数组
    for (file of e.target.files) {
        // 将每张图片生成blob地址
        const url = window.URL.createObjectURL(file)
        // 传入blob地址
        promises.push(getComposeColorStyle(url, file.name))
    }
    const res = await Promise.all(promises) // 顺序执行所有promise
    composeColors = res // 将结果赋值给composeColors
}

// 监听完成按钮
finishBtn.onclick = finishCompose

// 监听导出按钮
exportBtn.onclick = exportCanvas

function drawMainImage(url) {
    // 接收传进来的url
    fabric.Image.fromURL(url, img => {
        console.log(img)
        // 转换成功后的回调
        // fabric.Image.fromURL会将此url转换成一张图片

        // 需要缩放图片,height > width 就按照 width的缩放比例,反之用height的缩放比例
        // 反过来是为了能充满整张图
        const scale = img.height > img.width ? canvas.width / img.width : canvas.height / img.height

        // 设置这张图像绘制的参数
        img.set({
            left: canvas.width / 2, // 距离canvas画板左边一半宽度
            originX: 'center', // 水平方向居中
            top: 0, // 距离顶部距离为0
            scaleX: scale, // 图像水平缩放比例
            scaleY: scale, // 图像竖直缩放比例
            selectable: false // 不可操作,默认是true
        })

        // 图片绘制完成的回调函数
        img.on('added', e => {
            console.log('图片加载完成了啊')
            setTimeout(() => {
                // 绘制完成后,获取此图像中10000个格子的色彩信息
                getMainRGBA()
            }, 200) // 这里用延时器,是因为图像绘制有延迟
            // 而这里需要保证图像真的完全绘制完,再去获取色彩信息
        })

        // 把此图像绘制到canvas画板中
        canvas.add(img)
    })
}


function getMainRGBA() {
    const rgbas = [] // 用来收集10000个格子的色彩信息
    for (let y = 0; y < canvas.height; y += 8) {
        for (let x = 0; x < canvas.width; x += 8) {
            // 获取每一块格子的色彩data
            const { data } = ctx.getImageData(x, y, 8, 8)
            rgbas[y / 8 * 100 + x / 8] = []
            for (let i = 0; i < data.length; i += 4) {
                // 4个4个收集,因为每4个就组成一个像素的rgba
                rgbas[y / 8 * 100 + x / 8].push([
                    data[i],
                    data[i + 1],
                    data[i + 2],
                    data[i + 3]
                ])
            }
        }
    }
    // 算出10000个格子,每个格子的主色调
    mainColors = getMainColorStyle(rgbas)
}

function getMainColorStyle(rgbas) {
    const mainColors = [] // 用来收集1000个格子的主色调rgba
    for (let colors of rgbas) {
        let r = 0, g = 0, b = 0, a = 0
        for (let color of colors) {
            // 累加
            r += color[0]
            g += color[1]
            b += color[2]
            a += color[3]
        }
        mainColors.push([
            Math.round(r / colors.length), // 取平均值
            Math.round(g / colors.length), // 取平均值
            Math.round(b / colors.length), // 取平均值
            Math.round(a / colors.length) // 取平均值
        ])
    }
    return mainColors
}

function getComposeColorStyle(url, name) {
    return new Promise(resolve => {
        // 创建一个 20 * 20的canvas画板
        // 理论上这里宽高可以自己定,但是越大,色彩会越精准
        const composeCanvas = document.createElement('canvas')
        const composeCtx = composeCanvas.getContext('2d')
        composeCanvas.width = 20
        composeCanvas.height = 20

        // 创建img对象
        const img = new Image()
        img.src = url
        img.onload = function () {
            const scale = composeCanvas.height / composeCanvas.height
            img.height *= scale
            img.width *= scale

            // 将img画到临时canvas画板
            composeCtx.drawImage(img, 0, 0, composeCanvas.width, composeCanvas.height)
            // 获取颜色信息data
            const { data } = composeCtx.getImageData(0, 0, composeCanvas.width, composeCanvas.height)

            // 累加  r,g,b,a
            let r = 0, g = 0, b = 0, a = 0
            for (let i = 0; i < data.length; i += 4) {
                r += data[i]
                g += data[i + 1]
                b += data[i + 2]
                a += data[i + 3]
            }
            resolve({
                // 主色调
                rgba: [
                    Math.round(r / (data.length / 4)), // 取平均值
                    Math.round(g / (data.length / 4)), // 取平均值
                    Math.round(b / (data.length / 4)), // 取平均值
                    Math.round(a / (data.length / 4)) // 取平均值
                ],
                url,
                name
            })
        }
    })
}

function finishCompose() {
    const urls = [] // 收集最终绘制的10000张图片

    for (let main of mainColors) { // 遍历10000个格子主色调

        let closestIndex = 0 // 最接近主色调的图片的index
        let minimumDiff = Infinity // 相差值

        for (let i = 0; i < composeColors.length; i++) {
            const { rgba } = composeColors[i]
            // 格子主色调rgba四个值,减去图片主色调rgba四个值,的平方
            const diff = (rgba[0] - main[0]) ** 2 + (rgba[1] - main[1]) ** 2
                + (rgba[2] - main[2]) ** 2 + (rgba[3] - main[3]) ** 2

            // 然后开跟比较
            if (Math.sqrt(diff) < minimumDiff) {
                minimumDiff = Math.sqrt(diff)
                closestIndex = i
            }
        }

        // 把最小色差的图片url添加进数组urls
        urls.push(composeColors[closestIndex].url)
    }


    // 将urls中10000张图片,分别绘制在对应的10000个格子中
    for (let i = 0; i < urls.length; i++) {
        fabric.Image.fromURL(urls[i], img => {
            const scale = img.height > img.width ? 8 / img.width : 8 / img.height;
            img.set({
                left: i % 100 * 8,
                top: Math.floor(i / 100) * 8,
                originX: "center",
                scaleX: scale,
                scaleY: scale,
            });
            canvas.add(img)
        })
    }
}


//导出图片
function exportCanvas() {
    const dataURL = canvas.toDataURL({
        width: canvas.width,
        height: canvas.height,
        left: 0,
        top: 0,
        format: "png",
    });
    const link = document.createElement("a");
    link.download = "嫦娥姐姐.png";
    link.href = dataURL;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}

参考文章

结语

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者加入我的群哈哈,咱们一起摸鱼一起学习 : meron857287645