百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

使用Node.js和PNG.js处理PNG图片,提取精灵图并生成CSS

myzbx 2025-02-26 13:02 16 浏览

在前端开发中,精灵图(Sprite)是一种常见的优化技术,通过将多个小图标合并为一张大图片,减少HTTP请求,从而提升页面加载速度。然而,手动处理精灵图往往耗时且容易出错。今天,我们将通过Node.js和pngjs库,实现自动提取精灵图并生成对应的CSS代码,大大提高开发效率。

一、什么是精灵图?

精灵图是一种将多个小图片合并为一张大图片的技术。通过CSS的background-imagebackground-position属性,可以在网页中显示大图片的某一部分,从而实现多个小图标的显示。精灵图的优点是可以减少HTTP请求次数,优化网页性能。

二、技术栈介绍

  1. Node.js:一个基于Chrome V8引擎的JavaScript运行环境,适用于服务器端开发。
  2. PNG.js:一个用于处理PNG图片的Node.js库,支持读取、解析和修改PNG图片。

三、代码实现

以下是完整的代码实现,它将读取一个PNG图片文件,提取其中的精灵图,并生成对应的CSS文件和分割后的图片。

1. 安装依赖

在开始之前,确保你已经安装了pngjs库。可以通过以下命令安装:

Bash
npm install pngjs

2. 完整代码

Bash
var fs = require('fs');
var PNG = require('pngjs').PNG;

// 获取命令行参数中的PNG文件路径
var pngFile = process.argv[2];
var outDir = pngFile.replace(".png", "");

// 读取PNG文件并解析
fs.createReadStream(pngFile)
    .pipe(new PNG({ filterType: 4 }))
    .on('parsed', function () {
        // 复制图片数据
        var tempData = new Buffer(4 * this.width * this.height);
        this.data.copy(tempData);

        // 创建输出目录
        if (!fs.existsSync('export')) {
            fs.mkdirSync('export');
        }
        if (!fs.existsSync('export/' + outDir)) {
            fs.mkdirSync('export/' + outDir);
        }

        // 获取精灵图数组
        var spritesArray = getSprites(tempData, this.height, this.width);

        // 生成CSS代码
        var css = getCss(spritesArray, pngFile);
        fs.writeFile(outDir + '.css', css, function (err) {
            if (err) throw err;
            console.log('CSS文件已保存!');
        });

        // 保存每个精灵图为单独的PNG文件
        for (var i = 0; i < spritesArray.length; i++) {
            var rect = spritesArray[i];
            var newData = {
                data: null,
                height: (rect.rb.y - rect.rt.y),
                width: (rect.rt.x - rect.lt.x)
            };
            var newPng = new PNG({
                filterType: 4,
                width: newData.width,
                height: newData.height
            });

            // 截取精灵图区域
            this.bitblt(newPng, rect.lt.x, rect.lt.y, newData.width, newData.height, 0, 0);

            // 保存为PNG文件
            var dst = fs.createWriteStream('export/' + outDir + '/' + outDir + '' + i + '.png');
            newPng.pack().pipe(dst);
        }
    });

// 生成CSS代码
function getCss(spritesArray, pngname) {
    var css = `.sprite {display:inline-block; overflow:hidden; background-repeat: no-repeat;background-image:url(${pngname});}`;
    for (var i = 0; i < spritesArray.length; i++) {
        var rect = spritesArray[i];
        css += getSpriteCss('sprite' + i, rect);
    }
    return css;
}

// 生成单个精灵图的CSS代码
function getSpriteCss(spritename, rect) {
    return `.${spritename} {width:${rect.rt.x - rect.lt.x}px; height:${rect.rb.y - rect.rt.y}px; background-position: ${0 - rect.lt.x}px ${0 - rect.lt.y}px;}`;
}

// 提取精灵图数组
function getSprites(data, height, width) {
    var spritesArray = [];
    var contourVector = marchingSquares(data, height, width);

    while (contourVector.length > 3) {
        var rect = getRect(contourVector);
        if ((rect.rt.x - rect.lt.x > 3) && (rect.lb.y - rect.lt.y > 3)) {
            spritesArray.push(rect);
        }

        // 清空已提取的区域
        for (var y = rect.rt.y; y < rect.rb.y; y++) {
            for (var x = rect.lb.x; x < rect.rb.x; x++) {
                var idx = (width * y + x) << 2;
                data[idx] = 0;
                data[idx + 1] = 0;
                data[idx + 2] = 0;
                data[idx + 3] = 0;
            }
        }

        contourVector = marchingSquares(data, height, width);
    }
    return spritesArray;
}

// 获取矩形边界
function getRect(squareArray) {
    var rectXY = {};
    rectXY.maxX = squareArray[0].x;
    rectXY.minX = squareArray[0].x;
    rectXY.maxY = squareArray[0].y;
    rectXY.minY = squareArray[0].y;

    for (var i = 0; i < squareArray.length; i++) {
        var p = squareArray[i];
        rectXY.maxX = p.x > rectXY.maxX ? p.x : rectXY.maxX;
        rectXY.maxY = p.y > rectXY.maxY ? p.y : rectXY.maxY;
        rectXY.minX = p.x < rectXY.minX ? p.x : rectXY.minX;
        rectXY.minY = p.y < rectXY.minY ? p.y : rectXY.minY;
    }

    return {
        lt: { x: rectXY.minX, y: rectXY.minY },
        lb: { x: rectXY.minX, y: rectXY.maxY },
        rt: { x: rectXY.maxX, y: rectXY.minY },
        rb: { x: rectXY.maxX, y: rectXY.maxY }
    };
}

// Marching Squares算法实现
function marchingSquares(data, height, width) {
    var contourVector = [];
    var startPoint = getStartingPixel(data, height, width);

    if (startPoint != null && startPoint.x >= 0) {
        var pX = startPoint.x;
        var pY = startPoint.y;
        var stepX = 0;
        var stepY = 0;
        var prevX = 0;
        var prevY = 0;
        var closedLoop = false;

        while (!closedLoop) {
            var squareValue = getSquareValue(data, pX, pY, width);

            switch (squareValue) {
                case 1:
                case 5:
                case 13:
                    stepX = 0;
                    stepY = -1;
                    break;
                case 8:
                case 10:
                case 11:
                    stepX = 0;
                    stepY = 1;
                    break;
                case 4:
                case 12:
                case 14:
                    stepX = -1;
                    stepY = 0;
                    break;
                case 2:
                case 3:
                case 7:
                    stepX = 1;
                    stepY = 0;
                    break;
                case 6:
                    if (prevX == 0 && prevY == -1) {
                        stepX = -1;
                        stepY = 0;
                    } else {
                        stepX = 1;
                        stepY = 0;
                    }
                    break;
                case 9:
                    if (prevX == 1 && prevY == 0) {
                        stepX = 0;
                        stepY = -1;
                    } else {
                        stepX = 0;
                        stepY = 1;
                    }
                    break;
            }

            pX += stepX;
            pY += stepY;
            contourVector.push(new Point(pX, pY));
            prevX = stepX;
            prevY = stepY;

            if (pX == startPoint.x && pY == startPoint.y) {
                closedLoop = true;
            }
        }
    }
    return contourVector;
}

// 获取2x2像素网格的值
function getSquareValue(data, pX, pY, width) {
    var squareValue = 0;
    if (!isAlpha(data, pX - 1, pY - 1, width)) {
        squareValue += 1;
    }
    if (!isAlpha(data, pX, pY - 1, width)) {
        squareValue += 2;
    }
    if (!isAlpha(data, pX - 1, pY, width)) {
        squareValue += 4;
    }
    if (!isAlpha(data, pX, pY, width)) {
        squareValue += 8;
    }
    return squareValue;
}

// 找到第一个非透明像素作为起始点
function getStartingPixel(data, height, width) {
    var offsetPoint = new Point(-1, -1);
    for (var y = 0; y < height; y++) {
        for (var x = 0; x < width; x++) {
            var idx = (width * y + x) << 2;
            var alpha = data[idx + 3];
            if (alpha > 0) {
                offsetPoint.x = x;
                offsetPoint.y = y;
                return offsetPoint;
            }
        }
    }
    return offsetPoint;
}

// 检查像素是否透明
function isAlpha(data, x, y, width) {
    if (x < 0 || y < 0) {
        return true;
    }
    var idx = (width * y + x) << 2;
    var alpha = data[idx + 3];
    return alpha == 0;
}

// 定义点类
var Point = function (_x, _y) {
    this.x = _x;
    this.y = _y;
};

3. 运行代码

将上述代码保存为sprite.js,然后通过以下命令运行:

node sprite.js your-image.png

运行完成后,你将在export目录中找到提取的精灵图和对应的CSS文件。

四、代码解析

  1. PNG.js:用于读取和解析PNG图片,支持像素级操作。
  2. Marching Squares算法:用于提取图片中的轮廓,从而确定每个精灵图的边界。
  3. CSS生成:根据提取的精灵图边界,生成对应的CSS代码,方便在网页中使用。
  4. 文件输出:将提取的精灵图保存为单独的PNG文件,并生成一个CSS文件用于样式定义。

相关推荐

为什么钟表的指针是从左向右顺时针转?

所有的钟表指针都是从左向右转的,所以我们就用它来表示旋转方向了。那么,为什么钟表都是从左向右转呢?正着转也好,反着转也好,一圈不都是12小时吗?这就要从钟表的前身说起了。在钟表出现之前,人们使用过一种...

牛人将电子钟改造高精度时钟,日误差0.26秒!解决走时不准通病

家里有好多个电子钟,精度各种参差不齐,然后走时就是各种混乱,是可忍孰不可忍……自打发现8025这个好玩意儿之后,就决定不忍了。第一个上场的聪明钟,为啥叫聪明钟然后还走的不准。三节电池供电,其中3V给主...

篮球裁判手势图解之计时钟、得分替换和暂停手势

▋篮球裁判手势图解之计时钟手势停止计时钟手势,伸开手掌,垂直举过头部。犯规停止计时钟手势,一拳握紧,垂直举过头部。计时开始手势,用手做劈柴动作,将垂直举过头部的手放下。▋篮球裁判手势图解之得分手势1...

罗马数字的起源与用途

一、罗马数字的诞生与进化罗马数字起源于古罗马帝国,拥有一个漫长而复杂的历史,始于公元前8世纪至9世纪,与古罗马帝国在帕兰丁山(PalantineHill)周围建立的时间大致相同。不过,罗马数...

基于 Arduino Nano R3 的红外遥控数字时钟

由于在ArduinoNano上没有足够的引脚来编写代码,该项目只有有限的功能(即使没有设置时间的设施)。通过添加红外线遥控器,我可以灵活地整合所有需要的功能(如果需要,可能会更多),不需要额外的...

大班必备33首数字歌,轻松学数学

适合大班宝贝的33首数学歌,让孩子们在玩中学,通过好玩、好记的的儿歌来了解数学的知识点,轻松学数学!以上所有有关数学概念的知识点,其中包含了钟表、点数、分解组成、加减、单双数、倒数正数、凑十、方位...

11的寓意和象征

在数字的王国里,每个数字都有其独特的内涵和象征意义。今天,我们将一起探索数字11的奥秘和象征意义。这个奇特的数字,不仅在我们的日常生活中扮演着重要的角色,而且在神秘主义和宗教中也占有的一席之地。首先,...

基于TM1637的数字时钟

方案介绍这个项目是一个原型,我将在我正在进行的其他数字时钟项目中使用。这是我计划在我的下一个数字时钟项目中使用的时间和闹钟设置机制的原型。我希望能给你提供到帮助。如果你想到任何改进,请告诉我。我会更乐...

【金龟子讲睡前故事】数字不见啦

“快做数学题!”妈妈大声吼邦邦。“啊,好烦呀!”邦邦回到屋里,对着数学练习册大声嚷嚷。考拉熊博士在邦邦的屋外听到邦邦的声音,自言自语说:“好像又在发脾气,我得去看看他。”考拉熊博士推门进去,只见邦邦大...

SE 最终幻想 35 周年,《FF7 重制版》破坏剑数字时钟 9 月发售

IT之家3月9日消息,SE今日正式开设了《最终幻想》35周年纪念网站,天野喜孝绘制官方LOGO公布!值得一提的是,索尼PlayStation游戏发布会即将于北京时间3月10...

谁说数字钟就是黑白状?他们让你改变看法

如果我们没有了钟表,你会用什么衡量时间?是利用太阳的变化还是凭猜测?之前设计癖也介绍一些有趣的钟表,像是Edelkrone设计的无表针的Oqloq钟表,也有淡化了表针概念的轨道钟表,今天再给大...

杭州元宵游玩大赏|“人体时钟”亮相文三数字生活街区,还有元宵巡游活动等你嗨

钱江晚报·小时新闻记者方力通讯员冯晨晨刘静滴答滴答,在这个时钟里面有一位虚拟的“小姐姐”。她的工作内容就是不断地把分针擦掉,然后再画上新的分针,她每画一次分针擦干净后,再画上一条新的分针,就刚...

来用PPT做一只数字时钟动画

“什么是可见性?“可见性”即指PPT动画元素中的一种。在我们之前的图文教程《动画基础扫盲课,必修!》中提到过一些常用的PPT动画元素。分别为可见性,X,Y坐标,旋转,高度和宽度。而其中所谓的“可见性”...

4060+4013+74ls161数字电子时钟仿真电路图

苹果手机桌面时钟怎么显示 苹果手机桌面时钟显示操作

苹果手机系统流畅,系统使用起来很舒适,是很多人的首选。苹果时钟可以在桌面上显示数字时钟,如果在编辑主屏幕时,不小心把时钟删掉了,要怎么恢复呢?或者想要设置时钟显示,操作是什么样的呢?苹果手机桌面时钟怎...