乐趣区

关于前端:还记得西游记里的嫦娥吗我用10000张图片拼成了儿时女神

本文写于 2021 中秋节前夕

前言

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

前段时间,我看了荣顶大佬的这篇我用 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 图像

画出嫦娥姐姐

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

那咱们要怎么把一张图像画到 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 个格子


咱们都晓得,咱们的 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)

那么 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);
}

最终成果

彩蛋

如果你感觉此文对你有一丁点帮忙,点个赞,激励一下林三心哈哈。或者能够退出我的摸鱼群
想进学习群,摸鱼群,请点击这里[摸鱼](
https://juejin.cn/pin/6969565…)

哈哈我用王者光荣 猪八戒 的图片,组成了我本人

残缺代码

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

参考文章

  • 荣顶大佬的这篇我用 10000 张图片合成咱们美妙的霎时

结语

我是林三心,一个热心的前端菜鸟程序员。如果你上进,喜爱前端,想学习前端,那咱们能够交朋友,一起摸鱼哈哈,摸鱼群,加我请备注【思否】

退出移动版