盒子
盒子
文章目录
  1. 前言
  2. 点九图
  3. 分析
  4. 新问题
  5. 遗留的问题

使用JavaScript处理点九图

前言

在前端开发中,常会将图片作为某个元素的背景图,但是背景图的大小和比例和元素有偏差,所以一般要使元素有全背景的话,只能将图片拉伸。这里不考虑background-repeat。最好的办法还是将图片修改为比例和元素相同以等比缩放。
使用微信或者QQ的人应该会发现聊天气泡,气泡会随着内容多少的改变而去适应它,但是并没有使气泡图片有拉伸的效果,这里就用到了点九图

点九图

关于点九图这里不做过多介绍,简单来说,它是andriod平台的应用软件开发里的一种特殊的图片形式,扩展名为.9.png。它有两个重要的特点是:四周必须要有四条一像素纯黑的线或点;左上两条线控制拉伸区,右下两条线控制内容区。
这里我们需要将上传的点九图片拉伸成指定的或者自适应的比例,在没有接触点九图之前根本没有任何想法,于是上github上找到一个在web端处理点九图的,将代码拉取到本地即可看到demo

分析

阅读源码发现主要使用border-image和用canvas绘制两种方式实现。首先先取出点九图左边和上边1px,这里以水平方向为例:

let tempCtx, tempCanvas;
tempCanvas = document.createElement('canvas');
tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(this.bgImage, 0, 0);
let data = tempCtx.getImageData(0, 0, this.bgImage.width, 1).data;

上面的data存放的为只读的ImageData.data属性,返回Uint8ClampedArray,描述一个一维数组,包含以 RGBA 顺序的数据,数据使用 0255(包含)的整数表示。然后遍历这个一维数组,每4位一个step,找到可拉伸的区间数量和区域。

NinePatch.prototype.getPieces = function(data, staticColor, repeatColor) {
    var tempDS, tempPosition, tempWidth, tempColor, tempType;
    var tempArray = new Array();

    tempColor = data[4] + ',' + data[5] + ',' + data[6] + ',' + data[7];
    tempDS = (tempColor == staticColor ? 's' : (tempColor == repeatColor ? 'r' : 'd'));
    tempPosition = 1;

    for (var i = 4, n = data.length - 4; i < n; i += 4) {
        tempColor = data[i] + ',' + data[i + 1] + ',' + data[i + 2] + ',' + data[i + 3];
        tempType = (tempColor == staticColor ? 's' : (tempColor == repeatColor ? 'r' : 'd'));
        if (tempDS != tempType) {
            // box changed colors
            tempWidth = (i / 4) - tempPosition;
            tempArray.push(new Array(tempDS, tempPosition, tempWidth));

            tempDS = tempType;
            tempPosition = i / 4;
            tempWidth = 1
        }
    }

    // push end
    tempWidth = (i / 4) - tempPosition;
    tempArray.push(new Array(tempDS, tempPosition, tempWidth));

    return tempArray;
}

上面的getPieces方法存放了可用于判断拉伸区间数量和可拉伸范围的数组。在将其传入绘制函数中。

for (var i = 0, n = this.horizontalPieces.length; i < n; i++) {
    if (this.horizontalPieces[i][0] == 's') {
        tempStaticWidth += this.horizontalPieces[i][2];
    } else {
        tempDynamicCount++; // 拉伸区间数量
    }
}

fillWidth = (dWidth - tempStaticWidth) / tempDynamicCount;  // 可拉伸区间

再将取得的水平和垂直的1px获取到的数组进行嵌套循环,去填充拉伸图片,这里就不贴代码了,可以查阅源码理解。

新问题

找到的这种方式只能将图片进行放大,如果点九图比需要预览的图大,那就不适用了,还有个新问题是,点九图的宽或高跟预览图相比,有个的值大,有一个的值小,如:W点九 > W预览,H点九 < H预览。这种情景也不适用,所以考虑处理点九图。
这里只说最终的解决办法,当点九图的宽或高其中一个大于预览图的对应值时,将对应边缩小到预览图的值,再将另一边等比缩小,产生新的点九图片,这样新的点九图肯定比预览图小,可以正常拉伸了。

if (this.div.offsetWidth < this.bgImage.width && this.div.offsetHeight > this.bgImage.height) {
    tmpCanvas.width = this.div.offsetWidth;
    tmpCanvas.height = Math.floor(this.bgImage.height * this.div.offsetWidth / this.bgImage.width);
    tmpCtx.drawImage(this.bgImage, 0, 0, this.div.offsetWidth, Math.floor(this.bgImage.height * this.div.offsetWidth / this.bgImage.width));
    let tmpImage = new Image();
    tmpImage.src = tmpCanvas.toDataURL("image/png");
    this.bakImage = this.bgImage;
    this.bgImage = tmpImage;
}

遗留的问题

按照上面的缩放方式,不论是宽还是高缩小,都会影响原点九图左边或者上面的1px的边界,导致在 getPieces方法中误取可拉伸区间值,这种情况一般发生在边界线离点九图非透明色边界距离较近时发生,暂时没有想到解决方案。
想到其实这也是种模拟实现的方式,在实际的产品中不可能多用。不过这个过程收获也是挺大。
有好的解决方案欢迎轻敲~~