使用Sass实现反相滤镜效果

在理清制作固定背景的反相效果后,我脑海中自然而然地想到了,使用 Sass 让不支持滤镜的浏览器实现这种效果。Sass 本身存在一个 invert 函数,但是只能模拟出 filter: invert(100%) 的效果。我们的目标是适用于任意比例。

下面的代码,是上次用 JavaScript 实现的反相滤镜效果:

var box = document.querySelector('.box'), 
    styles = window.getComputedStyle(box), 
    filter = (styles.webkitFilter || styles.filter),
    invert_arg = filter.match(/(0\.\d+)|\d+/)[0], 
    upper = ((filter.indexOf('%') > -1)?(invert_arg/100):invert_arg)*255, 
    lower = 255 - upper, 
    original = styles.backgroundColor.split('('), 
    channels = original[1].match(/(0\.\d+)|\d+/g), 
    alpha = (channels.length > 3)?(1*channels.splice(3, 1)[0]):1, 
    inverted_channels = channels.map(function(ch) {
      return 255 - Math.round(lower + ch*(upper - lower)/255);
    }), 
    inverted;

if(alpha !== 1) {
  inverted_channels.splice(3, 0, alpha);
}

inverted = original[0] + '(' + inverted_channels.join(', ') + ')';

所以第一步是,设置 red, green, blue 参数(区间为 0 ~ 255),以及反相函数的比例参数,该参数值在 01 之间,或者是百分数 0%100% 之间(允许使用范围之外的值,但表现效果会被限制在规定的区间内)。然后,我们会计算传入的比例参数,得出每个颜色值的限制范围并将其反相。

代码演示如下:

$red: 255;
$green: 165;
$blue: 0;
$percentage: 65%;

$original: rgb($red, $green, $blue);

$upper: ($percentage/100%)*255;
$lower: 255 - $upper;

$inverted-red: 255 - round($lower + $red*($upper - $lower)/255);
$inverted-green: 255 - round($lower + $red*($upper - $lower)/255);
$inverted-blue: 255 - round($lower + $red*($upper - $lower)/255);

$inverted: rgb($inverted-red, $inverted-green, $inverted-blue);

显而易见,上面的代码有重复的方程式,所以可以将其放入一个函数中:

@function invert-channel($channel, $upper, $lower) {
    @return 255 - round($lower + $red*($upper - $lower)/255);
}

接着,我们就可以向下面这样传参了:

$inverted: rgb(invert-channel($red, $upper, $lower), 
               invert-channel(green, $upper, $lower), 
               invert-channel(blue, $upper, $lower));

但我们真的需要多次调用 invert-channel 吗?当然不是,所以我们继续创建另一个函数,用来处理相关计算并调用 invert-channel

@function _invert($red, $green, $blue, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    @return rgb(invert-channel($red, $upper, $lower), 
                invert-channel($green, $upper, $lower), 
                invert-channel($blue, $upper, $lower));
}

干得不错。但是我们并不总是使用 rgb() 值,有时还会使用颜色关键字或者 hsl() 值。当然我们可以使用一些工具,在使用颜色前转换为恰当的格式。但是否存在一种方式,无论我们传入任何值都可以获得正确格式呢?当然有!Sass 自带的 redgreenblue 方法就是为此而生的。

继续简化 _invert 函数:

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    @return rgb(invert-channel(red($original), $upper, $lower), 
                invert-channel(green($original), $upper, $lower), 
                invert-channel(blue($original), $upper, $lower));
}

整体代码:

$original: orange;
$percentage: 65%;

@function invert-channel($channel, $upper, $lower) {
    @return 255 - round($lower + $red*($upper - $lower)/255);
}

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    @return rgb(invert-channel(red($original), $upper, $lower), 
                invert-channel(green($original), $upper, $lower), 
                invert-channel(blue($original), $upper, $lower));
}

$inverted: _invert($original, $percentage);

这看起来好多了!不过,我们可以使用动态调用继续简化 invert-channel。重写后的 _invert 函数如下所示:

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    $inverted-channels: ();

    @each $channel-name in 'red' 'green' 'blue' {
        $channel: call($channel-name, $original);
        $inverted-channel: invert-channel($channel, $upper, $lower);
        $inverted-channels: append($inverted-channels, $inverted-channel);
    }

    @return rgb($inverted-channels...);
}

通过消除重复的地方,代码整体非常优雅简洁。最后还有一件事需要处理:原始值为半透明的处理方式。这其实非常简单,我们可以取出使用alpha 函数取出透明通道,最后将其追加到返回值即可。所以,最后的代码就像下面这样:

@function invert-channel($channel, $upper, $lower) {
    @return 255 - round($lower + $channel*($upper - $lower)/255);
}

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;
    $alpha: alpha($original);

    $inverted-channels: ();

    @each $channel-name in 'red' 'green' 'blue' {
        $channel: call($channel-name, $original);
        $inverted-channel: invert-channel($channel, $upper, $lower);
        $inverted-channels: append($inverted-channels, $inverted-channel);
    }

    $inverted-channels: append($inverted-channels, $alpha);

    @return rgba($inverted-channels...);
}

$inverted: _invert($original, $percentage);

关于 rgba 函数有趣的一点是,只有透明值小于 1,才会在最终的 CSS 中生成 rgba() 值。

本文根据@Ana Tudor的《Emulate the Invert Filter Effect with Sass》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://davidwalsh.name/invert-color-sass

出处:http://www.w3cplus.com/preprocessor/invert-color-sass.html