Making Web Icons Smarter

This article is the first in a three-part series showing the new approaches to iconography Iconic will be delivering. If you like what you see in this article, please consider backing Iconic on Kickstarter.
This content was commissioned by Iconic and was written and/or edited by the Tuts+ team. Our aim with sponsored content is to publish relevant and objective tutorials, case studies, and inspirational interviews that offer genuine educational value to our readers and enable us to fund the creation of more useful content.
What Exactly is a Smart Icon?
A smart icon is designed so that elements within it to adjust to input, interaction or associated data. In short, it's dynamic. Smart icons are SVG-based and controlled with a combination of Javascript and CSS.
Why This is Relevant
Web icons up to this point have been static—primarily due to limitations in technology. Different states or variations of an icon would be created simply through providing individual files of each permutation. For example, a battery icon would come in four different versions: charging, full-charge, half-charge and no-charge. Not exactly an optimal approach.
A whole new set of possibilities are now viable with mainstream browser adoption of SVG 1.1. Due to SVG's semantic structure, a smart icon can display its full range of states and variations. This removes the need for image swapping and allows transitions between states to occur fluidly.
Smart icons also give designers the option to display relevant contextual information within an icon. A well designed icon is already a relatively informationally dense object. By adding contextual information to an icon, its informational density is even greater without considerable increased cognitive load. In theory, these types of icons will be able to take on the heavy-lifting of communication, thus reducing the amount of other elements on the screen.
Use-cases for Smart Icons
There are many different directions smart icons can go—some being easier to implement than others. We're still in the discovery process, but so far we've come up with three primary use cases:
Providing contextual information
There are plenty of icons which could provide another layer of information, but simply have not up to this point, due to their static display method. Examples include icons such as the clock, thermometer, aperture, WIFI signal and battery charge.



Acting as simple data-vis elements (when d3.js is too much)
One of the best use-cases for smart icons is simple data visualization. Icons that fit in this category are the audio spectrum, gauge/meter and loading indicator. Smart icons could dramatically simplify the process of building dashboard displays—think of simply adding four or five icons to your HTML and adjusting the gauge value with a data attribute.



Displaying various states
Many icons often come in a series of variations to convey all their different states. Examples include the battery, WIFI, media playback (e.g., play, pause, etc.) and power (e.g., on, off, standby). Another potential application for smart icons is to roll all of an icon's states into a single SVG. So instead of swapping out image assets when a subject's state changes, you simply change a data attribute to the appropriate state.



Nuts and Bolts
Note: Before going into details, it's important to note that the examples we're showing are simply proof-of-concept prototypes. These prototypes are intended to communicate the functionality we'll be building. None of the following code is final, let alone beta. We're still in the R&D phase of this method and we know there are many issues that still need to be addressed. We will be working on a more concrete direction for this method after the Kickstarter campaign is complete.
Smart icons consist of SVG, Javascript and CSS. Our current thinking is to treat each icon like a small self-contained app with a simple API to adjust elements within the icon. To achieve this reliably, this approach requires the SVG markup to be included in the DOM.
Keep in mind that the SVG mark up needs to be appropriately structured for this approach to work. This is what we feel makes Iconic unique. The icons are being designed and crafted with new concepts in mind. These concepts rely on clear semantics and a well thought out markup structure to work correctly. This is no different from appropriately structured HTML—if your markup is gobbleygoop, it's going to be difficult to do anything sophisticated.
A lot of compelling things can happen once well structured SVG markup is in the DOM. The problem is that adding SVG markup into HTML is a pain. The SVG markup can add a considerable amount of bloat to your code and it becomes harder to distinguish between structural HTML and vector imagery. In order to remove this friction, we suggest injecting SVG markup in the DOM at runtime.
We've made a simple prototype SVG injector which replaces all specified img
tags with the markup from the referenced SVG file. So this...
1 |
|
2 |
<div class="image-container"> |
3 |
<img src="images/circle.svg" class="svg-inject" width="300" /> |
4 |
</div>
|
Turns into this:
1 |
|
2 |
<div class="image-container"> |
3 |
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100px" height="100px"> |
4 |
<circle cx="50" cy="50" r="50"/> |
5 |
</svg>
|
6 |
</div>
|
Note: Keep in mind, this injector approach certainly feels like a stopgap measure and we're hoping our work will help push a browser-native standard. Until then, our current thinking is that this is the best approach.
Once the SVG is injected in the DOM, the JavaScript encapsulated within it is executed and it's ready to be used. Some icons will run on their own (like a clock) whereas others will need input to adjust.
Self-running Icon
The clock is a perfect example of an icon that runs on its own. Once injected, it will just go. See it in action
HTML
1 |
|
2 |
<img src="clock.svg" class="svg-inject" alt="clock" /> |
JS
1 |
|
2 |
$('.svg-inject').svgInject(); |
SVG: clock.svg
1 |
|
2 |
<svg version="1.1" class="iconic-clock" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="384px" height="384px" viewBox="0 0 384 384" enable-background="new 0 0 384 384" xml:space="preserve"> |
3 |
<path class="iconic-clock-frame" d="M192,0C85.961,0,0,85.961,0,192s85.961,192,192,192s192-85.961,192-192S298.039,0,192,0zM315.037,315.037c-9.454,9.454-19.809,17.679-30.864,24.609l-14.976-25.939l-10.396,6l14.989,25.964c-23.156,12.363-48.947,19.312-75.792,20.216V336h-12v29.887c-26.845-0.903-52.636-7.854-75.793-20.217l14.989-25.963l-10.393-6l-14.976,25.938c-11.055-6.931-21.41-15.154-30.864-24.608s-17.679-19.809-24.61-30.864l25.939-14.976l-6-10.396l-25.961,14.99C25.966,250.637,19.017,224.846,18.113,198H48v-12H18.113c0.904-26.844,7.853-52.634,20.216-75.791l25.96,14.988l6.004-10.395L44.354,99.827c6.931-11.055,15.156-21.41,24.61-30.864s19.809-17.679,30.864-24.61l14.976,25.939l10.395-6L110.208,38.33C133.365,25.966,159.155,19.017,186,18.113V48h12V18.113c26.846,0.904,52.635,7.853,75.792,20.216l-14.991,25.965l10.395,6l14.978-25.942c11.056,6.931,21.41,15.156,30.865,24.611c9.454,9.454,17.679,19.808,24.608,30.863l-25.94,14.976l6,10.396l25.965-14.99c12.363,23.157,19.312,48.948,20.218,75.792H336v12h29.887c-0.904,26.845-7.853,52.636-20.216,75.792l-25.964-14.989l-6.002,10.396l25.941,14.978C332.715,295.229,324.491,305.583,315.037,315.037z" /> |
4 |
<line class="iconic-clock-hour-hand" id="foo" fill="none" stroke="#000000" stroke-width="18" stroke-miterlimit="10" x1="192" y1="192" x2="192" y2="87.5"/> |
5 |
<line class="iconic-clock-minute-hand" id="iconic-anim-clock-minute-hand" fill="none" stroke="#000000" stroke-width="12" stroke-miterlimit="10" x1="192" y1="192" x2="192" y2="54"/> |
6 |
<circle class="iconic-clock-axis" cx="192" cy="192" r="9"/> |
7 |
<g class="iconic-clock-second-hand" id="iconic-anim-clock-second-hand"> |
8 |
<line class="iconic-clock-second-hand-arm" fill="none" stroke="#D53A1F" stroke-width="4" stroke-miterlimit="10" x1="192" y1="192" x2="192" y2="28.5"/> |
9 |
<circle class="iconic-clock-second-hand-axis" fill="#D53A1F" cx="192" cy="192" r="4.5"/> |
10 |
</g>
|
11 |
<defs>
|
12 |
<animateTransform
|
13 |
type="rotate" |
14 |
fill="remove" |
15 |
restart="always" |
16 |
calcMode="linear" |
17 |
accumulate="none" |
18 |
additive="sum" |
19 |
xlink:href="#iconic-anim-clock-hour-hand" |
20 |
repeatCount="indefinite" |
21 |
dur="43200s" |
22 |
to="360 192 192" |
23 |
from="0 192 192" |
24 |
attributeName="transform" |
25 |
attributeType="xml"> |
26 |
</animateTransform>
|
27 |
|
28 |
<animateTransform
|
29 |
type="rotate" |
30 |
fill="remove" |
31 |
restart="always" |
32 |
calcMode="linear" |
33 |
accumulate="none" |
34 |
additive="sum" |
35 |
xlink:href="#iconic-anim-clock-minute-hand" |
36 |
repeatCount="indefinite" |
37 |
dur="3600s" |
38 |
to="360 192 192" |
39 |
from="0 192 192" |
40 |
attributeName="transform" |
41 |
attributeType="xml"> |
42 |
</animateTransform>
|
43 |
|
44 |
<animateTransform
|
45 |
type="rotate" |
46 |
fill="remove" |
47 |
restart="always" |
48 |
calcMode="linear" |
49 |
accumulate="none" |
50 |
additive="sum" |
51 |
xlink:href="#iconic-anim-clock-second-hand" |
52 |
repeatCount="indefinite" |
53 |
dur="60s" |
54 |
to="360 192 192" |
55 |
from="0 192 192" |
56 |
attributeName="transform" |
57 |
attributeType="xml"> |
58 |
</animateTransform>
|
59 |
</defs>
|
60 |
<script type="text/javascript"> |
61 |
<![CDATA[
|
62 |
var date = new Date;
|
63 |
var seconds = date.getSeconds();
|
64 |
var minutes = date.getMinutes();
|
65 |
var hours = date.getHours();
|
66 |
hours = (hours > 12) ? hours - 12 : hours;
|
67 |
|
68 |
minutes = (minutes * 60) + seconds; |
69 |
hours = (hours * 3600) + minutes; |
70 |
|
71 |
document.querySelector('.iconic-clock-second-hand').setAttribute('transform', 'rotate('+360*(seconds/60)+',192,192)'); |
72 |
document.querySelector('.iconic-clock-minute-hand').setAttribute('transform', 'rotate('+360*(minutes/3600)+',192,192)'); |
73 |
document.querySelector('.iconic-clock-hour-hand').setAttribute('transform', 'rotate('+360*(hours/43200)+',192,192)'); |
74 |
]]> |
75 |
</script>
|
76 |
</svg>
|
Input-based Icon
When an icon is responding to input or data, it requires a little more work, but the basics are unchanged. See it in action
HTML
1 |
|
2 |
<img src="audio-spectrum-analyzer.svg" class="svg-inject" alt="audio spectrum analyzer" /> |
JS
1 |
|
2 |
$('.svg-inject').svgInject(); |
SVG: audio-spectrum-analyzer.svg
1 |
|
2 |
<svg id="audio-spectrum-analyzer" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="320px" width="210px" version="1.1" y="0px" x="0px" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 210 320" onclick="toggleAudio()"> |
3 |
<g id="eq-bars" height="320px" width="210px" fill="#010101" transform=""> |
4 |
<rect x="00" y="150" height="20" width="10" /> |
5 |
<rect x="20" y="140" height="40" width="10" /> |
6 |
<rect x="40" y="100" height="120" width="10" /> |
7 |
<rect x="60" y="120" height="80" width="10" /> |
8 |
<rect x="80" y="60" height="200" width="10" /> |
9 |
<rect x="100" y="20" height="280" width="10" /> |
10 |
<rect x="120" y="70" height="180" width="10" /> |
11 |
<rect x="140" y="120" height="80" width="10" /> |
12 |
<rect x="160" y="140" height="40" width="10" /> |
13 |
<rect x="180" y="150" height="20" width="10" /> |
14 |
<rect x="200" y="155" height="10" width="10" /> |
15 |
</g>
|
16 |
|
17 |
<defs>
|
18 |
<style type="text/css"><![CDATA[ |
19 |
svg#audio-spectrum-analyzer {
|
20 |
margin: 0 auto;
|
21 |
}
|
22 |
]]></style> |
23 |
</defs>
|
24 |
|
25 |
<script type="application/javascript"><![CDATA[ |
26 |
var context;
|
27 |
if (typeof AudioContext !== "undefined") {
|
28 |
context = new AudioContext();
|
29 |
}
|
30 |
else if (typeof webkitAudioContext !== "undefined") {
|
31 |
context = new webkitAudioContext();
|
32 |
}
|
33 |
else {
|
34 |
throw new Error('AudioContext not supported. :(');
|
35 |
}
|
36 |
|
37 |
var eqHeight = document.querySelector('svg#audio-spectrum-analyzer > g#eq-bars').getAttribute('height').replace('px', '');
|
38 |
var bars = document.querySelectorAll('svg#audio-spectrum-analyzer rect'); |
39 |
|
40 |
var playing = false; |
41 |
|
42 |
var audioFileUrl = document.querySelector('svg#audio-spectrum-analyzer').getAttribute('data-audiofile'); |
43 |
if (audioFileUrl === undefined) { |
44 |
throw new Error('Audio File not defined'); |
45 |
} |
46 |
|
47 |
var soundSource; |
48 |
var fft; |
49 |
var fftSmoothing = 0.6; |
50 |
var fftMaxValue = 256; |
51 |
var samples = 128; |
52 |
var sampleIntervalID; |
53 |
var ampFactor = 1.25; |
54 |
var numBars = bars.length; |
55 |
var soundBuffer; |
56 |
|
57 |
|
58 |
var request = new XMLHttpRequest(); |
59 |
request.open("GET", audioFileUrl, true); |
60 |
request.responseType = "arraybuffer"; |
61 |
|
62 |
// Our asynchronous callback |
63 |
request.onload = function () { |
64 |
var audioData = request.response; |
65 |
|
66 |
// The Audio Context handles creating source |
67 |
// buffers from raw binary data |
68 |
soundBuffer = context.createBuffer(audioData, true /*make mono*/ ); |
69 |
|
70 |
}; |
71 |
request.send(); |
72 |
|
73 |
function sampleAudio() { |
74 |
var data = new Uint8Array(fft.frequencyBinCount); |
75 |
fft.getByteFrequencyData(data); |
76 |
|
77 |
// Calc bin size to sum freqs into. |
78 |
// Carve off some of the high-end, lower energy bars (+2) |
79 |
var bin_size = Math.floor(data.length / (numBars + 2)); |
80 |
|
81 |
// Sum up and average the samples into their bins |
82 |
for (var i = 0; i < numBars; ++i) { |
83 |
|
84 |
// Sum this bin |
85 |
var sum = 0; |
86 |
for (var j = 0; j < bin_size; ++j) { |
87 |
sum += data[(i * bin_size) + j]; |
88 |
}
|
89 |
|
90 |
// Duck some of the low-end power |
91 |
if (i === 0) { |
92 |
sum = sum * 0.75; |
93 |
}
|
94 |
|
95 |
// Calculate the average frequency of the samples in the bin |
96 |
var average = sum / bin_size; |
97 |
var scaled_average = Math.max(10, ((average / fftMaxValue) * eqHeight) * ampFactor); |
98 |
|
99 |
// Update eq bar height |
100 |
bars[i].setAttribute('height', scaled_average); |
101 |
|
102 |
// Center bar |
103 |
bars[i].setAttribute('y', (eqHeight - scaled_average) / 2); |
104 |
}
|
105 |
}
|
106 |
|
107 |
function playSound() { |
108 |
// create a sound source |
109 |
soundSource = context.createBufferSource(); |
110 |
|
111 |
// Add the buffered data to our object |
112 |
soundSource.buffer = soundBuffer; |
113 |
|
114 |
// Create the FFT |
115 |
fft = context.createAnalyser(); |
116 |
fft.smoothingTimeConstant = fftSmoothing; |
117 |
fft.fftSize = samples; |
118 |
|
119 |
soundSource.connect(fft);
|
120 |
fft.connect(context.destination);
|
121 |
|
122 |
soundSource.noteOn(context.currentTime);
|
123 |
|
124 |
// Start the FFT sampler |
125 |
sampleIntervalID = setInterval(sampleAudio, 30); |
126 |
|
127 |
playing = true; |
128 |
}
|
129 |
|
130 |
function stopSound() { |
131 |
// Stop the FFT sampler |
132 |
clearInterval(sampleIntervalID);
|
133 |
|
134 |
if (soundSource) { |
135 |
soundSource.noteOff(context.currentTime);
|
136 |
}
|
137 |
playing = false; |
138 |
}
|
139 |
|
140 |
var toggleAudio = function () { |
141 |
if (!playing) { |
142 |
playing = true; |
143 |
playSound();
|
144 |
}
|
145 |
else { |
146 |
stopSound();
|
147 |
playing = false; |
148 |
}
|
149 |
}
|
150 |
|
151 |
window.addEventListener('load', function () { |
152 |
window.toggleAudio = toggleAudio; |
153 |
}, false); |
154 |
|
155 |
]]></script> |
156 |
|
157 |
</svg>
|
Adding motion (the icing on the cake)
A smart icon becomes even better with motion. There are many ways to do this. We're currently using SVG animation elements as this allows considerable functionality built right into the browser—meaning less code in the SVG. Support is still a little wonky (we ran into issues in Safari 6), but it's getting better by the day. See it in action
HTML
1 |
|
2 |
<img src="thermometer.svg" class="svg-inject" alt="thermometer" /> |
3 |
<ul class="menu"> |
4 |
<li><a href="#1">Hot</a></li> |
5 |
<li><a href="#0.66">Warm</a></li> |
6 |
<li><a href="#0.33">Chilly</a></li> |
7 |
<li><a href="#0">Cold</a></li> |
8 |
</ul>
|
JS
1 |
|
2 |
$('.svg-inject').svgInject(); |
SVG: themometer.svg
1 |
|
2 |
<svg version="1.0" class="iconic-thermometer" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="40px" |
3 |
height="128px" viewBox="0 0 40 128" enable-background="new 0 0 40 128" xml:space="preserve"> |
4 |
<path class="iconic-thermometer-container" d="M20,6c2.757,0,5,2.243,5,5v80.305v3.234l2.701,1.777C31.646,98.912,34,103.279,34,108c0,7.72-6.28,14-14,14 |
5 |
s-14-6.28-14-14c0-4.721,2.354-9.088,6.298-11.684L15,94.539v-3.234V11C15,8.243,17.243,6,20,6 M20,0C13.935,0,9,4.935,9,11
|
6 |
v80.305c-5.501,3.62-9,9.829-9,16.695c0,11.028,8.972,20,20,20c11.028,0,20-8.972,20-20c0-6.865-3.499-13.075-9-16.695V11
|
7 |
C31,4.935,26.065,0,20,0L20,0z"/> |
8 |
<line class="iconic-thermometer-shaft iconic-thermometer-hot" id="iconic-anim-thermometer-shaft" fill="none" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-miterlimit="10" x1="20" y1="108" x2="20" y2="11" /> |
9 |
<circle class="iconic-thermometer-well iconic-thermometer-hot" cx="20" cy="108" r="12" /> |
10 |
|
11 |
<animate
|
12 |
id="shaft-animate" |
13 |
attributeName= "y2" |
14 |
begin= "indefinite" |
15 |
dur="1s" |
16 |
xlink:href="#iconic-anim-thermometer-shaft" |
17 |
fill="freeze" |
18 |
calcMode="spline" |
19 |
keySplines="0.42 0 0.58 1" |
20 |
keyTimes="0;1" |
21 |
restart="whenNotActive" |
22 |
/>
|
23 |
|
24 |
<script type="text/ecmascript"> |
25 |
<![CDATA[
|
26 |
|
27 |
var shaft = document.querySelector('#iconic-anim-thermometer-shaft');
|
28 |
var well = document.querySelector('.iconic-thermometer-well');
|
29 |
var yOrigin2 = parseFloat(shaft.getAttribute('y2'));
|
30 |
var yOrigin1 = parseFloat(shaft.getAttribute('y1'));
|
31 |
var yPos = yOrigin2;
|
32 |
var tempClass;
|
33 |
|
34 |
window.addEventListener('hashchange', function() {
|
35 |
var hash = window.location.hash.substr(1);
|
36 |
goto(hash);
|
37 |
}, false);
|
38 |
|
39 |
function goto(amount) {
|
40 |
var shaftAnim = document.querySelector('#shaft-animate');
|
41 |
|
42 |
shaft.setAttribute('y2', yPos)
|
43 |
|
44 |
amount = parseFloat(amount)
|
45 |
if( isNaN( amount ) ) return;
|
46 |
|
47 |
if(amount>.9) {
|
48 |
tempClass="iconic-thermometer-hot"; |
49 |
} else if(amount>.5) { |
50 |
tempClass="iconic-thermometer-warm"; |
51 |
} else if(amount>.2) { |
52 |
tempClass="iconic-thermometer-chilly"; |
53 |
} else { |
54 |
tempClass="iconic-thermometer-cold"; |
55 |
} |
56 |
|
57 |
amount = 1 - amount; |
58 |
amount = Math.min(Math.max(0, amount), 1); |
59 |
|
60 |
var ry = ( amount * ( yOrigin1-yOrigin2 ) ) + yOrigin2; |
61 |
|
62 |
/* |
63 |
* Unfortunately, Safari doesn't make life easy on us. We need to remove and re-initialize |
64 |
* the animation element for animations to start from the last end point. |
65 |
*/ |
66 |
var ns = shaftAnim.cloneNode(true); |
67 |
ns.setAttribute( 'from', yPos ) |
68 |
ns.setAttribute( 'to', ry ); |
69 |
|
70 |
shaftAnim.parentNode.replaceChild(ns, shaftAnim); |
71 |
|
72 |
well.setAttribute('class', 'iconic-thermometer-well ' + tempClass); |
73 |
shaft.setAttribute('class', 'iconic-thermometer-shaft ' + tempClass); |
74 |
ns.beginElement(); |
75 |
yPos = ry; |
76 |
|
77 |
} |
78 |
]]> |
79 |
</script>
|
80 |
</svg>
|
Conclusion
Iconography has a significant role to play in interface design. The more relevant information our icons can provide, the more powerful they become. We truly believe that smart iconography can be a compelling tool for designers to add another layer of meaning to their icons. Not every icon is suited for this approach—like all good things it requires moderation. However, when used appropriately, it can be a tremendous new tool.
Back Iconic on Kickstarter
The goal of Iconic is to help provide new approaches to iconography. There's a lot more to Iconic than just smart icons and we're looking forward to sharing another interesting feature with you next week.



Please consider backing Iconic on Kickstarter.