Applying color tints to web pages with SVG filters and JavaScript
Introduction
In this article I will present to you a new experiment of mine—an application of SVG and JavaScript that allows you to re-color any web page you navigate to. This has great uses—not only can it be used to make pages more accessible, but you can also use it to test different color schemes on a page on the fly. The re-coloring functions are actually packaged up bookmarklets, contained in the article below—feel free to install these in your browser as buttons in the UI, or as bookmarks.
While I explain you how it works, you’ll learn about bookmarklets, linearGradient
, feColorMatrix
and foreignObject
.
Note on browser support: The examples in this article will work in Opera 9.5+. Please report any bugs you find; i’ve found the following:
- Can’t drag the window scrollbar (#332880)
svg:foreignObject
displays badly when window is resized (#336280)
Gradients
I once had a map with black, white and all shades of grey in between on it. The “colors” represented the different temperature values, and as I wanted to drive to the warmest place possible, it was therefore important to be able to distinguish between the grey shades. This is all well and good when you’ve only got 7 shades of grey to distinguish between, but the combination of my eyes and my screen however doesn’t allow me to tell apart 256 colors on a web page. Does yours? The SVG object below shows a grey gradient containing 256 shades of grey.
The code to create this is as follows:
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg viewBox="0 0 256 25" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="MyGradient">
<stop offset="0%" stop-color="black"/>
<stop offset="100%" stop-color="white"/>
</linearGradient>
</defs>
<rect fill="url(#MyGradient)" stroke-width="0" width="256" height="25"/>
</svg>
I thought maybe it would be easier to distinguish between shades of red, green or blue. Testing with only my own eyes shows that distinguishability between neighboring colors is highest in a gradient depending on where in the minimum/maximum range the colors fall (sometimes I prefer red, sometimes green, sometimes blue). What do you think? The below SVG objects show different red, green, and blue gradients.
feColorMatrix
In the above 3 color gradients I am actually still showing the grey gradient you saw earlier, but I am using feColorMatrix
to re-color it.
The code to do this looks like so (this only shows the code for the red above example—the code for each is the same except for differing color matrix values):
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="100%" height="100%">
<defs>
<filter id="myFilter"color-interpolation-filters="sRGB">
<!-- without the above interpolation setting halving a color with the filter doesn't mean halving its numerical value,
if this doesn't make sense, try the SVG spec. -->
<feColorMatrix id="myFilterElement" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"
x="0" y="0" width="100%" height="100%" in="sourceGraphic" type="matrix" result="ColorMatrix"/>
</filter>
</defs>
<g filter="url(#myFilter)" image-rendering="optimizeSpeed">
<!-- the attributes of the group could in theory be put on the foreignObject directly,
but not with the Opera implementation I tested it on -->
<foreignObject xlink:href="shadesOfGrey.svg" id="myForeignObject" width="100%" height="100%"/>
</g>
</svg>
(instead of using foreignObject
,
I could have instead used a different one from the collection of external references*, but my choice will become clear as you read on.)
Using feColorMatrix
filtering basically results in every component of the resulting color being a linear function of all the components of the original color.
Here both the input and output color components are taken as fractions of the maximum. Full opaque pure red for example is (r=1,g=0,b=0,a=1)
.
The formula for one component is as follows:
Red_result= Rr*Red_original + Rg*Green_original + Rb*Blue_original + Ra*Alpha_original + R1.
This stated, you can define feColorMatrix
’s “values” attribute like so:
Rr Rg Rb Ra R1 Gr Gg Gb Ga G1 Br Bg Bb Ba B1 Ar Ag Ab Aa A1
What applications does this technique have? One is fun: imagine shooting a picture or video with your mobile and instantly showing an alien version to your friends, with the colors all shifted?
Or you could make a photobook with the photos made black-and-white for a nostalgic feel (there is a Photo gallery example available on dev.opera.com that you could modify). Instead, you could put this technique to more serious uses. I’ll look at one now.
Accessibility
I got interested in color contrast and, after some Googlin’, read about color blindness and learned that:
- About 10% of men (and a lot less women), have some sort of color blindness.
- There are many kinds of color blindness, including insensitivities to different (but rarely all) color components.
On pages with bad contrast (for example yellow text on white background) I often select all text using the browser’s own select All
(which you can usually call with Ctrl + A on Windows and Command + A on Mac). This however doesn’t help with . feColorMatrix
is a nice alternative that can help improve contrast on the whole page, not just text.
I imposed the following constraints on my design:
- The number of colors should stay the same, so nothing disappears.
- color values stay the same relative distance apart on the color spectrum, so you can recognize the original picture in the result.
This leaves the following subset of feColorMatrix
operations available to use (giving, including the original, 8 x 6 = 48 color schemes):
- swap color channels (3*2*1=6 options: rgb, rbg, grb, gbr, brg, bgr)
- flip the minimum/maximum for a channel (2 x 2 x 2 = 8 options: rgb, rgB, rGb, rGB, Rgb, RgB, RGb, RGB)—this would be equivalent to a left/right flip on the gradients I started the article with.
For those that are curious about the mathematics, I’ll explain how flipping the minimum/maximum for a color component works—basically it’s the multi-dimensional version of x becomes 1-x :
Red_result_new = (-1)*Rr_old*Red_original + (-1)*Rg_old*Green_original + (-1)*Rb_old*Blue_original + (-1)*Ra_old*Alpha_original + (-1)*R1+1
But surely it is too difficult to use? Instead of hitting a keyboard shortcut, this requires creation of an SVG file, and some more coding besides, so how can we implement this in a way that is usable by non-technical people? After pondering this for a while, I decided on using bookmarklets.
Bookmarklets
Bookmarklets are bookmarks containing a piece of script—the script is executed when you click on the link. For example, try putting the following code inside the href
attribute of a link, click it, and see what the effect is:
javascript:alert('the%20address%20of%20this%20page%20is:\n\n'+location.href);
Because of URL-encoding, the spaces in the above code are escaped as “%20”—before encoding you need to bring back your script to one line and also prevent a return value.
If you want you can learn specifics, but I just use the Sub Simple bookmarklet builder form to do it for me. Simply copy your JavaScript (with the javascript:
protocol that prefixes it) into the form, press “format” (this also checks for errors), then “compress” and finally “function”.
If you bookmark this link, it will become a so-called bookmarklet, and you can then call it to run on any page form your bookmarks menu. If you place it on a browser bar, it effectively becomes a browser button.
Now let’s look at how to use feColorMatrix
inside a bookmarklet.
How it works
First let's look at the example in action. Put the script below into a link, like you did before with the previous bookmarket example, change the feColorMatrix
values that appear in the resulting alert box, and press OK/Enter (Try this 2nd example live here).The script basically redirects you to an SVG file and sends the document title and address of the original file, plus the feColorMatrix
values along as variables in the URL.
var baseURL="http://steltenpower.com/feColorMatrix.svg";
var values;
if (location.href.substr(0,baseURL.length)==baseURL){
values=document.getElementById("myFilterElement").getAttribute("values");
}else{
values="";
}
var defaultValues;
if (values===""){
defaultValues="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0";
}else{
defaultValues=values;
}
var newValues = prompt("matrix values for feColorMatrix please",defaultValues);
if (newValues!=defaultValues){
if (values===""){
location.assign(baseURL+"?uri="+escape(location.href)+"&values="+escape(newValues)+"&title="+escape(document.title));
}else{
document.getElementById("myFilterElement").setAttribute("values",newValues);
}
}
The SVG file that the script redirects to contains JavaScript that takes the variables from the URL and uses them in setting the color parameters. This SVG file looks like so:
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg onload="setValues()" width="100%" height="100%" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>title as page that is recolored</title>
<script type="text/ecmascript"> <![CDATA[
function setValues(){
var here=location.href;
var getString=here.substr(here.indexOf("?")+1);
var pairs=getString.split("&");
var pair;
var value;
var uri;
for (var i=0;i<3;i++){
pair=pairs[i].split("=");
value=unescape(pair[1]); // reverse of previous code (URL encoding: e.g. " " is "%20" in URL)
switch(pair[0]){
case "title":
document.getElementsByTagNameNS("http://www.w3.org/2000/svg","title")[0].firstChild.nodeValue=value;
break;
case "values":
document.getElementById("myFilterElement").setAttributeNS(null,"values",value);
break;
case "uri":
uri=value;
break;
}
}
document.getElementById("myForeignObject").setAttributeNS("http://www.w3.org/1999/xlink","href",uri);
// this is done last to prevent seeing the original un-re-colored page first for a bit
}
]]> </script>
<defs>
<filter id="myFilter" color-interpolation-filters="sRGB">
<!-- without the above interpolation setting halving a color with the filter doesn't mean halving its numerical value in RGB space. -->
<feColorMatrix id="myFilterElement" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
x="0" y="0" width="100%" height="100%" in="sourceGraphic" type="matrix" result="ColorMatrix"/>
</filter>
</defs>
<g filter="url(#myFilter)" image-rendering="optimizeSpeed">
<!-- the attributes of the group could in theory be put on the foreignObject directly, but not with the Opera implementation I tested it on -->
<foreignObject id="myForeignObject" xlink:href="" width="100%" height="100%"/>
</g>
</svg>
Some usability thoughts
So this is working quite nicely, but there’s a usabillity issue here—the feColorMatrix
values you have to calculate and type to choose a color value aren’t exactly easy to comprehend for most people.
Therefore I have also coded the flip and swap operations into bookmarklets. Try bookmarklet example 3.
flip red, green, or blue
The full code for this is as follows:
var baseURL="http://steltenpower.com/feColorMatrix.svg";
var values;
if (location.href.substr(0,baseURL.length)==baseURL){
values=document.getElementById("myFilterElement").getAttribute("values");
}else{
values="";
}
var defaultValues;
if (values===""){
defaultValues="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0";
}else{
defaultValues=values;
}
var valuesComponents = defaultValues.split(" ");
for(var i=0;i<5;i++){
valuesComponents[5+10+i]=-valuesComponents[5+10+i];
}
valuesComponents[4914]+=1;
var newValues=valuesComponents[0];
for(var j=2;j<=20;j++){
newValues=newValues+" "+valuesComponents[j-1];
}
if (values===""){
location.href=baseURL+"?uri="+escape(location.href)+"&values="+escape(newValues)+"&title="+escape(document.title);
}else{
document.getElementById("myFilterElement").setAttribute("values",newValues);
}
swap red and green, or green and blue, or blue and red.
The full code for this is as follows:
var baseURL="http://steltenpower.com/feColorMatrix.svg";
var values;
if (location.href.substr(0,baseURL.length)==baseURL){
values=document.getElementById("myFilterElement").getAttribute("values");
}else{
values="";
}
var defaultValues;
if (values===""){
defaultValues="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0";
}else{
defaultValues=values;
}
var valuesComponents = defaultValues.split(" ");
var temp;
for(var i=0;i<=4;i++){
temp=valuesComponents[5+10+i];
valuesComponents[5+10+i]=valuesComponents[5+10+i];
valuesComponents[5+10+i]=temp;
}
var newValues=valuesComponents[0];
for(i=2;i<=20;i++){
newValues=newValues+" "+valuesComponents[i-1];
}
if (values===""){
location.href=baseURL+"?uri="+escape(location.href)+"&values="+escape(newValues)+"&title="+escape(document.title);
}else{
document.getElementById("myFilterElement").setAttribute("values",newValues);
}
Now try them out and see what happens to the colors below.
black = 0, 0, 0 | 0, 0, 51 | 0, 0, 102 | 0, 0, 153 | 0, 0, 204 | blue = 0, 0, 255 |
0, 51, 0 | 0, 51, 51 | 0, 51, 102 | 0, 51, 153 | 0, 51, 204 | 0, 51, 255 |
0, 102, 0 | 0, 102, 51 | 0, 102, 102 | 0, 102, 153 | 0, 102, 204 | 0, 102, 255 |
0, 153, 0 | 0, 153, 51 | 0, 153, 102 | 0, 153, 153 | 0, 153, 204 | 0, 153, 255 |
0, 204, 0 | 0, 204, 51 | 0, 204, 102 | 0, 204, 153 | 0, 204, 204 | 0, 204, 255 |
green = 0, 255, 0 | 0, 255, 51 | 0, 255, 102 | 0, 255, 153 | 0, 255, 204 | aqua = 0, 255, 255 |
51, 0, 0 | 51, 0, 51 | 51, 0, 102 | 51, 0, 153 | 51, 0, 204 | 51, 0, 255 |
51, 51, 0 | 51, 51, 51 | 51, 51, 102 | 51, 51, 153 | 51, 51, 204 | 51, 51, 255 |
51, 102, 0 | 51, 102, 51 | 51, 102, 102 | 51, 102, 153 | 51, 102, 204 | 51, 102, 255 |
51, 153, 0 | 51, 153, 51 | 51, 153, 102 | 51, 153, 153 | 51, 153, 204 | 51, 153, 255 |
51, 204, 0 | 51, 204, 51 | 51, 204, 102 | 51, 204, 153 | 51, 204, 204 | 51, 204, 255 |
51, 255, 0 | 51, 255, 51 | 51, 255, 102 | 51, 255, 153 | 51, 255, 204 | 51, 255, 255 |
102, 0, 0 | 102, 0, 51 | 102, 0, 102 | 102, 0, 153 | 102, 0, 204 | 102, 0, 255 |
102, 51, 0 | 102, 51, 51 | 102, 51, 102 | 102, 51, 153 | 102, 51, 204 | 102, 51, 255 |
102, 102, 0 | 102, 102, 51 | 102, 102, 102 | 102, 102, 153 | 102, 102, 204 | 102, 102, 255 |
102, 153, 0 | 102, 153, 51 | 102, 153, 102 | 102, 153, 153 | 102, 153, 204 | 102, 153, 255 |
102, 204, 0 | 102, 204, 51 | 102, 204, 102 | 102, 204, 153 | 102, 204, 204 | 102, 204, 255 |
102, 255, 0 | 102, 255, 51 | 102, 255, 102 | 102, 255, 153 | 102, 255, 204 | 102, 255, 255 |
153, 0, 0 | 153, 0, 51 | 153, 0, 102 | 153, 0, 153 | 153, 0, 204 | 153, 0, 255 |
153, 51, 0 | 153, 51, 51 | 153, 51, 102 | 153, 51, 153 | 153, 51, 204 | 153, 51, 255 |
153, 102, 0 | 153, 102, 51 | 153, 102, 102 | 153, 102, 153 | 153, 102, 204 | 153, 102, 255 |
153, 153, 0 | 153, 153, 51 | 153, 153, 102 | 153, 153, 153 | 153, 153, 204 | 153, 153, 255 |
153, 204, 0 | 153, 204, 51 | 153, 204, 102 | 153, 204, 153 | 153, 204, 204 | 153, 204, 255 |
153, 255, 0 | 153, 255, 51 | 153, 255, 102 | 153, 255, 153 | 153, 255, 204 | 153, 255, 255 |
204, 0, 0 | 204, 0, 51 | 204, 0, 102 | 204, 0, 153 | 204, 0, 204 | 204, 0, 255 |
204, 51, 0 | 204, 51, 51 | 204, 51, 102 | 204, 51, 153 | 204, 51, 204 | 204, 51, 255 |
204, 102, 0 | 204, 102, 51 | 204, 102, 102 | 204, 102, 153 | 204, 102, 204 | 204, 102, 255 |
204, 153, 0 | 204, 153, 51 | 204, 153, 102 | 204, 153, 153 | 204, 153, 204 | 204, 153, 255 |
204, 204, 0 | 204, 204, 51 | 204, 204, 102 | 204, 204, 153 | 204, 204, 204 | 204, 204, 255 |
204, 255, 0 | 204, 255, 51 | 204, 255, 102 | 204, 255, 153 | 204, 255, 204 | 204, 255, 255 |
red = 255, 0, 0 | 255, 0, 51 | 255, 0, 102 | 255, 0, 153 | 255, 0, 204 | fuchsia = 255, 0, 255 |
255, 51, 0 | 255, 51, 51 | 255, 51, 102 | 255, 51, 153 | 255, 51, 204 | 255, 51, 255 |
255, 102, 0 | 255, 102, 51 | 255, 102, 102 | 255, 102, 153 | 255, 102, 204 | 255, 102, 255 |
255, 153, 0 | 255, 153, 51 | 255, 153, 102 | 255, 153, 153 | 255, 153, 204 | 255, 153, 255 |
255, 204, 0 | 255, 204, 51 | 255, 204, 102 | 255, 204, 153 | 255, 204, 204 | 255, 204, 255 |
yellow = 255, 255, 0 | 255, 255, 51 | 255, 255, 102 | 255, 255, 153 | 255, 255, 204 | white = 255, 255, 255 |
Conclusion
One of the advantages of SVG is that it seamlessly cooperates with other open webstandards—this fact alone made this experiment far easier. I hope I’ve inspired you to create many more applications and experiments using SVG. Ideas, examples and funny recolorings are welcome in the comments for this article, as are possible bug reports and other comments. I’d like to leave you with the following image—a vision of an alternate reality perhaps?
Reader challenges
Now it’s time to set you some challenges:
- Who can code a bookmarklet that loops through all 48 color schemes, first? (I wonder who will go bananas, you or your browser?) Every n=prompt seconds or every link-click?
- Do the same as the above request, but now show all color schemes simultaneously instead of looping: a massive time saver for web designers.
- Code a (GreaseMonkey kind of?) script that flips color channels for a different version of background color-based energy saving
*: This is part of the same SVG Open presentation in which Erik Dahlström inspired me to write this article by showing SVG filters applied to video. You can also get inspired at this year’s edition of SVG Open (re-coloring applied :-) )
This article is licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license.
Comments
The forum archive of this article is still available on My Opera.