js中自定义渐变函数的应用

js中自定义渐变函数的应用

这个是很久之前做的东西了,今天翻以前的demo的时候突然发现的,觉得还不错,分享出来大家都学习一下。

首先做这个demo的背景:

我们公司制作的webapp中对于图片滤镜,渐变等效果都是基于后端的,也就是说前端不负责效果的渲染而是将滤镜或渐变的参数发送至后端,后端根据参数生成对应的效果图传给前端然后由前端将效果图展示给用户,通过描述的这个过程就可以知道,从前端到后端,再从后端到前端,这一去一来会耗费不少的时间,而且这种处理会给后端服务器造成不小的压力,要想解决这个问题只能前端自己去实现滤镜和渐变等效果,但是前提是前后端渲染效果需要一致。

问题就是这个样子的,既然问题来了就要解决,首先考虑写一个demo先看看效果,由于我们的项目是基于webpack+react+redux的,而渲染选择了konvajs,一个 基于 Canvas 开发的 2d js 框架库, 你可以点击这里进入其官网. 那么我们的demo也应该基于konvajs实现后期才好移植到我们的app中,在查看了konvajs的API后发现其对于滤镜的支持还是挺全的,但是对于渐变的支持还是有点薄弱,一些基本渐变比如线性渐变,径向渐变都不支持,既然不支持我们只能考虑自己实现了,怎么实现呢?这确实难倒了我这个前端汪,在漫查资料无果的情况下我找后端大佬拿到了他们实现渐变的函数,代码如下:

//圆形渐变
private static IMOperation createCycleGradientIMOperation(Integer originalWidth, Integer originalHeight, Double midpoint) throws Exception {
    if (midpoint == null) {
        midpoint = 0.0;
    }
    if (midpoint < 0 || midpoint > 1) {
        throw new IllegalArgumentException("midpoint must be 0 to 1. midpoint:" + midpoint);
    }
    int opacityWidth, opacityHeight;
    opacityWidth = (int)(midpoint * originalWidth / 2);
    opacityHeight = (int)(midpoint * originalHeight / 2);
    IMOperation op = new IMOperation();
    op.openOperation();
    op.size(originalWidth, originalHeight);
    op.addRawArgs("xc:white");
    op.alpha("Set");
    op.channel("A");
    double dd;
    if (midpoint == 1) {
        dd = 1;
    } else {
        dd = 1.0 / (originalWidth / 2 - opacityWidth);
    }
    String expression = String.format("ot=h/w;xi=abs(i-(w/2));yj=abs(j-(h/2));xo=%d;yo=%d;rro=hypot(xo,yo);rr=hypot(w/2,h/2);dd=%f;w&gt;h?1-((sqrt(xi^2*ot^2+xi^2+(yj^2/ot^2)+yj^2))-rro)*dd:1-((sqrt(yj^2*ot^2+xi^2+(xi^2/ot^2)+yj^2))-rro)*dd", opacityWidth, opacityHeight, dd);
    op.fx(expression);
    op.closeOperation();
    op.addImage();
    op.compose("atop");
    op.composite();
    return op;
}
//矩形渐变
private static IMOperation createRectangleGradientIMOperation(Integer originalWidth, Integer originalHeight, Double midpoint) throws Exception {
    if (midpoint == null) {
        midpoint = 0.0;
    }
    if (midpoint < 0 || midpoint > 1) {
        throw new IllegalArgumentException("midpoint must be 0 to 1. midpoint:" + midpoint);
    }
    int opacityWidth, opacityHeight;
    opacityWidth = (int)(midpoint * originalWidth / 2);
    opacityHeight = (int)(midpoint * originalHeight / 2);
    IMOperation op = new IMOperation();
    op.openOperation();
    op.size(originalWidth, originalHeight);
    op.addRawArgs("xc:white");
    op.alpha("Set");
    op.channel("A");
    String expression = String.format("ot=h/w;xi=abs(i-(w/2));yj=abs(j-(h/2));xo=%d;yo=%d;rro=hypot(xo,yo);rr=hypot(w/2,h/2);dd=1/(rr-rro);yj&gt;xi*ot?1-((hypot(yj/ot,yj)-rro)*dd):1-((hypot(ot*xi,xi)-rro)*dd)", opacityWidth, opacityHeight);
    op.fx(expression);
    op.closeOperation();
    op.addImage();
    op.compose("atop");
    op.composite();
    return op;
}
//菱形渐变
private static IMOperation createDiamondGradientIMOperation(Integer originalWidth, Integer originalHeight, Double midpoint) throws Exception {
    if (midpoint == null) {
        midpoint = 0.0;
    }
    if (midpoint < 0 || midpoint > 1) {
        throw new IllegalArgumentException("midpoint must be 0 to 1. midpoint:" + midpoint);
    }
    int opacityWidth, opacityHeight;
    opacityWidth = (int)(midpoint * originalWidth / 2);
    opacityHeight = (int)(midpoint * originalHeight / 2);
    IMOperation op = new IMOperation();
    op.openOperation();
    op.size(originalWidth, originalHeight);
    op.addRawArgs("xc:white");
    op.alpha("Set");
    op.channel("A");
    double dd;
    if (midpoint == 1) {
        dd = 1;
    } else {
        dd = 1.0 / (originalWidth / 2 - opacityWidth);
    }
    String expression = String.format("ot=h/w;xi=abs(i-(w/2));yj=abs(j-(h/2));xo=%d;yo=%d;dd=%f;1-((xi+(yj/ot)-xo)*dd)", opacityWidth, opacityHeight, dd);
    op.fx(expression);
    op.closeOperation();
    op.addImage();
    op.compose("atop");
    op.composite();
    return op;
}

虽然代码量并不多而且看起来也挺简单,但是一开始就是没整明白,后来静下心来细想,才明白,其实这几段代码最关键的地方就是那几个expression,那么这个expression是用来做什么的呢?其实就是处理每一个像素点的,也就是说通过一定的算法去改变每一个像素点的值进入实现渐变,其实这个明白了后面就简单多了,我们只需要通过konvajs取出图片的像素数据,然后通过上面expression中的公式对每一个像素点的数据进行处理,然后用处理好的新像素数据替换掉老的像素数据就好了,具体使用什么API有兴趣的可以去查查官网的API,也可以查看文末贴的demo,下面是我根据自己的理解转换的javascript代码并进行了简单的包装:

const gradientFixer = {
    radial: function(w, h, i, j, ow, oh, dd) {
        const ot = h / w;
        const xi = Math.abs(i - (w / 2));
        const yj = Math.abs(j - (h / 2));
        const xo = ow;
        const yo = oh;
        const rro = Math.hypot(xo, yo);
        const rr = Math.hypot(w / 2, h / 2);
        if (w > h) {
            return Math.round(255 * (1 - (Math.sqrt(Math.pow(xi, 2) * Math.pow(ot, 2) + Math.pow(xi, 2) + (Math.pow(yj, 2) / Math.pow(ot, 2)) + Math.pow(yj, 2)) - rro) * dd));
        } else {
            return Math.round(255 * (1 - ((Math.sqrt(Math.pow(yj, 2) * Math.pow(ot, 2) + Math.pow(xi, 2) + (Math.pow(xi, 2) / Math.pow(ot, 2)) + Math.pow(yj, 2))), rro) * dd));
        }
    },
    rect: function(w, h, i, j, ow, oh, dd) {
        const ot = h / w;
        const xi = Math.abs(i - (w / 2));
        const yj = Math.abs(j - (h / 2));
        const xo = ow;
        const yo = oh;
        const rro = Math.hypot(xo, yo);
        const rr = Math.hypot(w / 2, h / 2);
        if (yj > xi * ot) {
            return Math.round(255 * (1 - (Math.hypot(yj / ot, yj) - rro) * dd));
        } else {
            return Math.round(255 * (1 - (Math.hypot(ot * xi, xi) - rro) * dd));
        }
    },
    diamond: function(w, h, i, j, ow, oh, dd) {
        const ot = h / w;
        const xi = Math.abs(i - (w / 2));
        const yj = Math.abs(j - (h / 2));
        const xo = ow;
        const yo = oh;
        const rro = Math.hypot(xo, yo);
        const rr = Math.hypot(w / 2, h / 2);
        return Math.round(255 * (1 - ((xi + (yj / ot) - xo) * dd)));
    }
};
/**
 * 生成渐变效果
 * @param {[type]} imageData 像素数据
 * @param {[type]} midPoint 中心点
 * @param {String} type 渐变类型
 */
function gradient(imageData, midPoint, type = 'radial') {
    const { width, height } = imageData;

    const midpoint = midPoint || 0;

    if (midpoint < 0 || midpoint > 1) {
        throw new Error('midpoint must be 0 to 1. current midpoint is:' + midpoint);
    }

    const opacityWidth = Math.floor(midpoint * width / 2);
    const opacityHeight = Math.floor(midpoint * height / 2);

    const newImageData = new ImageData(width, height);

    let dd;
    if (midpoint === 1) {
        dd = 1;
    } else {
        dd = 1 / (width / 2 - opacityWidth);
    }

    const fixFunction = gradientFixer[type];

    if (fixFunction) {
        for (let i = 0, data = imageData.data; i < height; i++) {
            for (let j = 0; j < width; j++) {
                const index = i * width * 4 + j * 4,
                    r = data[index],
                    g = data[index + 1],
                    b = data[index + 2];
                const a = fixFunction(width, height, j, i, opacityWidth, opacityHeight, dd);
                imageData.data[index] = r;
                imageData.data[index + 1] = g;
                imageData.data[index + 2] = b;
                imageData.data[index + 3] = a;
            }
        }
    }
}

看懂了上面的代码,后面的实现就很简单了,这里就不做赘述了。

最后的最后,附上一个集滤镜、渐变、裁剪一体的综合案例,你可以猛戳这里穿越过去。

  • 支付宝二维码 支付宝
  • 微信二维码 微信
相关文章