Component models are an important part of any programming platform because they provide a standard set of UI elements and infrastructure for building more.
HTML5 is a relatively young technology with a set of even younger specifications that aim to standardize the implementation of HTML5 components and their integration with other components. The HTML5 component model is currently defined by three specifications:
- "Introduction to Web Components"
- "Shadow DOM"
- "HTML Templates"
The functionality in these three specifications involves encapsulating component DOM trees (Shadow DOM), creating custom tags for components that page authors can use in their HTML pages (custom HTML elements), and declaratively specifying Shadow DOMs and insertion points (HTML templates). (See the "HTML5 component specifications" sidebar for more detail.)
But to implement components in the first place, you need a completely different skill set that includes the ability to create appealing graphics, layer HTML elements to support interesting interactions, and support event listeners. This series covers all these aspects of HTML5 component development:
- Ad-hoc components
- Shadow DOM, templates, and custom elements
- Mozilla's X-Tag
In this article and the next in the series, see how to implement ad-hoc HTML5 components from scratch, without any support from the new specifications. Later installments will explore the specifications and show you how to incorporate Shadow DOM, custom elements, and templates into your HTML5 component-building repertoire.
More specifically, in this article, learn how to:
- Implement ad-hoc HTML5 components
- Draw appealing graphics with the
canvaselement - Layer canvas elements to implement special effects
- Implement real-time dragging of an HTML element
Building one HTML5 component out of several
Figure 1 shows an image-processing component that is built with other, more-general components: icons, a toolbar, sliders, and an image-display area.
Figure 1. An image-processing component
Three of the icons in the toolbar — resize, rotate, and brightness — are associated with sliders. When you click on one of those icons, the image-processing component displays a slider and a couple of related buttons underneath the toolbar. The top screenshot in Figure 1 illustrates the use of the resize slider; the bottom screenshot shows the image after it has been rotated with the rotate slider.
Sliders consist of a rail and a knob; you change the slider's value by moving the knob along the rail. The position of the knob determines the slider's value (or vice versa if you programmatically set the value and redraw the slider). That value — a number between 0.0 and 1.0 — represents the percentage of the rail that lies to the left of the knob; for example, when three-quarters of the rail is to the left of the knob, the slider's value is 0.75.
Figure 2 shows a simple application that illustrates how to use a slider. Besides the slider, the application contains two buttons that incrementally move the slider's knob to the right (+) or left (-). The application also provides a readout underneath the slider that displays the slider's current value. See Resources for a link to run the application; you can also download the source code.
Figure 2. A slider
The user can move the slider's knob in three ways:
- Click one of the buttons to increment (+) or decrement (-) the slider's value by 10 percent.
- Drag the knob.
- Click in the rail, outside the knob, to move the center of the knob to the click location.
When the slider's value is set programmatically — which is the case when you activate the plus (+) or minus (-) buttons or enable clicking in the slider's rail outside the knob — the knob's movement is animated by a CSS transition with an ease-out function that slows the knob as it approaches its final destination. The slider component sets the transition's properties in JavaScript.
To use a slider, you instantiate one and append it to an existing DOM element, as shown in Listing 1.
Listing 1. Creating a slider
var slider = new COREHTML5.Slider( // All of the following arguments are optional
'navy', // Stroke color
'cornflowerblue', // Fill color
0.5, // Initial slider value
500); // Knob animation duration in milliseconds
...
slider.appendTo('someDiv'); // Appends the slider to a DOM element with the ID someDiv
|
The slider's appendTo() method resizes the
slider so that it expands or shrinks to fit the element that the slider is
attached to.
The slider's features are:
- Attaches to DOM elements with an
appendTo()method - Automatically resizes to fill the DOM element to which the slider is attached
- Registers change event listeners with an
addChangeListener()method - Fires change events to change listeners when the slider's knob moves
- Animates the knob with a CSS transition when the user clicks in the rail
Sliders are instances of COREHTML5.Slider to
avoid namespace collisions. It's not too far-fetched to imagine someone
implementing a slider with a more obvious name, such as
Slider, which would replace any existing global
objects with an identical name. It's unlikely, however, that anyone would
come up with COREHTML5.Slider on their own.
The COREHTML5.Slider constructor's arguments are
all optional; all the values have reasonable defaults. Table 1 lists key
COREHTML5.Slider methods.
Table 1. Key slider methods
| Method | Description |
|---|---|
appendTo(elementId) | Appends the slider's DOM element to the element whose ID matches the value passed to the method. |
addChangeListener(listenerFunction) | Adds a change listener function to the slider. When the slider's knob changes position, the slider fires an event to all of its change listeners. |
draw() | Draws the slider's rail and knob. |
erase() | Erases the slider. |
redraw() | Erases and then draws the slider. |
Table 1 lists only the
external methods that developers use to manipulate a slider. The
COREHTML5.Slider object also has many methods
that it uses internally, such as
initializeStrokeAndFillStyles() and
createKnobCanvas().
Developers access the slider's value through its
knobPercent attribute.
Listing 2 shows the HTML for the application shown in Figure 2.
Listing 2. The slider example's HTML
<!DOCTYPE html>
<html>
<head>
<title>Ad hoc components</title>
<style>
body {
background: rgb(240,240,240);
}
#title {
font: 18px Arial;
}
#slider-component {
width: 400px;
text-align: center;
}
#buttons {
display: inline;
font: 14px Arial;
}
#readout {
margin-left: 25%;
color: blue;
font: 18px Arial;
text-shadow: 2px 2px 2px rgb(255,255,255);
}
#slider {
width: 75%;
height: 30px;
float: right;
}
.slider-button {
background: rgba(100, 100, 100, 0.2);
font: 24px Arial;
font-weight: 1;
border-radius: 4px;
border: 1px solid rgba(100, 100, 180, 0.7);
background: rgba(255, 255, 0, 0.2);
box-shadow: 1px 1px 2px rgba(0,0,0,0.5);
cursor: pointer;
margin: 0px;
}
</style>
</head>
<body>
<div id='title'>A custom slider</div>
<p>
<div id='slider-component'>
<div id='controls'>
<div id='buttons'>
<input type='button' class='slider-button'
id='minus-button' value='–'/>
<input type='button' class='slider-button'
id='plus-button' value='+'/>
</div>
<div id='slider'></div>
</div>
<div id='readout'>0</div>
</div>
</p>
</body>
<script type="text/javascript" src="lib/slider.js"></script>
<script type="text/javascript" src="sliderExample.js"></script>
</html>
|
The HTML in Listing 2 creates the DOM tree shown in Figure 3.
Figure 3. The slider example's DOM tree
The HTML and CSS in
Listing 2
is straightforward. The HTML references two scripts, one for the slider
and another for the application itself. The application's script is shown
in Listing 3.
Listing 3. The slider example's JavaScript
var slider = new COREHTML5.Slider('black', 'cornflowerblue', 0),
readoutElement = document.getElementById('readout');
document.getElementById('minus-button').onclick = function (e) {
slider.knobPercent -= 0.1;
slider.redraw();
updateReadout();
}
document.getElementById('plus-button').onclick = function (e) {
slider.knobPercent += 0.1;
slider.redraw();
updateReadout();
}
function updateReadout() {
if (readoutElement)
readoutElement.innerHTML = slider.knobPercent.toFixed(2);
}
slider.addChangeListener(updateReadout);
slider.appendTo('slider');
slider.draw();
|
At the top of the JavaScript in Listing 3, the application creates the slider with a
black stroke style, cornflower blue fill style, and an initial value of
zero. At the bottom of the JavaScript, the application appends the slider
to the DOM element whose ID is slider. In
between, the JavaScript defines three event handlers that respond to
button clicks and slider value changes.
The application adds onclick event handlers to
the plus (+) and minus (-) buttons that adjust the slider's value
(knobPercent), redraw the slider, and update
the readout.
The application also adds a change listener to the slider that updates the application's readout to reflect the slider's new value. Components often provide a mechanism for registering event listeners and firing events to those listeners, and the slider component is no exception.
Now that you've seen how to use a slider, let's look at the slider component's implementation.
Creating and initializing the slider
Listing 4 shows the JavaScript code for the slider's constructor.
Listing 4. The slider's constructor
COREHTML5 = COREHTML5 || {};
COREHTML5.Slider = function(strokeStyle, fillStyle,
knobPercent, knobAnimationDuration) {
knobPercent = knobPercent || 0;
knobAnimationDuration = knobAnimationDuration || 1000; // milliseconds
this.railCanvas = document.createElement('canvas');
this.railContext = this.railCanvas.getContext('2d');
this.changeEventListeners = [];
this.initializeConstants();
this.initializeStrokeAndFillStyles(strokeStyle, fillStyle);
this.initializeKnob(knobPercent, knobAnimationDuration);
this.createDOMTree();
this.addMouseListeners();
this.addKnobTransitionListeners();
return this;
}
|
The first line of Listing 4 uses a common JavaScript idiom to ensure that
a global object — in this case,
COREHTML5 — exists. (If it does exist,
it's assigned to itself; otherwise, it's assigned to an empty object.)
The COREHTML5.Slider constructor function takes
four optional arguments: stroke color, fill color, initial slider value,
and knob animation duration in milliseconds. The
knobPercent represents the slider's value.
The constructor creates a
canvas — railCanvas — that contains
the slider's rail. It also creates a second
canvas — knobCanvas — with
createKnobCanvas() (shown in Listing 5), which is invoked in Listing 4 by
initializeKnob(). Finally, the constructor
function creates the slider's DOM tree and adds listeners to the slider.
The first three slider methods invoked by the slider's
constructor — initializeConstants(),
initializeStrokeAndFillStyles(), and
initializeKnob() — are shown in Listing 5.
Listing 5. The slider's initialization methods
COREHTML5.Slider.prototype = {
initializeConstants: function () {
this.SHADOW_COLOR = 'rgba(100, 100, 100, 0.4)';
this.SHADOW_OFFSET_X = 3;
this.SHADOW_OFFSET_Y = 3;
this.SHADOW_BLUR = 4;
this.KNOB_SHADOW_COLOR = 'rgba(255,255,0,0.8)';
this.KNOB_SHADOW_OFFSET_X = 1;
this.KNOB_SHADOW_OFFSET_Y = 1;
this.KNOB_SHADOW_BLUR = 0;
this.KNOB_FILL_STYLE = 'rgba(255, 255, 255, 0.45)';
this.KNOB_STROKE_STYLE = 'rgb(0, 0, 80)';
this.HORIZONTAL_MARGIN = 2.5 * this.SHADOW_OFFSET_X;
this.VERTICAL_MARGIN = 2.5 * this.SHADOW_OFFSET_Y;
this.DEFAULT_STROKE_STYLE = 'gray';
this.DEFAULT_FILL_STYLE = 'skyblue';
},
initializeStrokeAndFillStyles: function(strokeStyle, fillStyle) {
this.strokeStyle = strokeStyle ? strokeStyle : this.DEFAULT_STROKE_STYLE;
this.fillStyle = fillStyle ? fillStyle : this.DEFAULT_FILL_STYLE;
},
initializeKnob: function (knobPercent, knobAnimationDuration) {
this.animatingKnob = false;
this.draggingKnob = false;
this.knobPercent = knobPercent;
this.knobAnimationDuration = knobAnimationDuration;
this.createKnobCanvas();
},
createKnobCanvas: function() {
this.knobCanvas = document.createElement('canvas');
this.knobContext = this.knobCanvas.getContext('2d');
this.knobCanvas.style.position = "absolute";
this.knobCanvas.style.marginLeft = "0px";
this.knobCanvas.style.marginTop = "1px";
this.knobCanvas.style.zIndex = "1";
this.knobCanvas.style.cursor = "crosshair";
...
},
...
};
|
The initializeConstants() method creates
variables for all the slider's constants, including defaults for the
stroke and fill styles that
initializeStrokeAndFillStyles() uses when the
values are unspecified.
The most interesting method in Listing 5 is
initializeKnob(), which sets some variables
before calling createKnobCanvas() to create a
separate canvas for the slider's knob.
createKnobCanvas() creates a canvas element and
sets its style attributes so the canvas is positioned above and to the top
left of its enclosing canvas.
Now that you've seen how the canvases for the rail and knob are initialized, let's look at how they're used to draw the slider.
The slider component's draw() and
erase() methods are shown in Listing 6.
Listing 6. Drawing and erasing the slider
COREHTML5.Slider.prototype = {
...
erase: function() {
this.railContext.clearRect(
this.left - this.knobRadius, 0 - this.knobRadius,
this.railCanvas.width + 4*this.knobRadius,
this.railCanvas.height + 3*this.knobRadius);
this.knobContext.clearRect(0, 0, this.knobCanvas.width,
this.knobCanvas.height);
},
draw: function (percent) {
this.drawRail();
this.drawKnob(percent ? percent : this.knobPercent );
},
};
|
The erase() method erases both slider
canvases — one for the rail and the other for the knob. Conversely,
the draw() method draws the rail and the knob.
You can pass the knob percent — the
slider's value — to the draw() method, or
you can call it with no arguments, in which case it will use the slider's
existing value.
In Listing 7, the slider component draws the rail in two passes.
Listing 7. Drawing the rail
COREHTML5.Slider.prototype = {
...
drawRail: function () {
var radius = (this.bottom - this.top) / 2;
this.railContext.save();
this.railContext.shadowColor = this.SHADOW_COLOR;
this.railContext.shadowOffsetX = this.SHADOW_OFFSET_X;
this.railContext.shadowOffsetY = this.SHADOW_OFFSET_Y;
this.railContext.shadowBlur = this.SHADOW_BLUR;
this.railContext.beginPath();
this.railContext.moveTo(this.left + radius, this.top);
this.railContext.arcTo(this.right, this.top, this.right, this.bottom, radius);
this.railContext.arcTo(this.right, this.bottom, this.left, this.bottom, radius);
this.railContext.arcTo(this.left, this.bottom, this.left, this.top, radius);
this.railContext.arcTo(this.left, this.top, this.right, this.top, radius);
this.railContext.closePath();
this.railContext.fillStyle = this.fillStyle;
this.railContext.fill();
this.railContext.shadowColor = undefined;
this.railContext.restore();
this.overlayRailGradient();
this.railContext.restore();
},
overlayRailGradient: function () {
var gradient =
this.railContext.createLinearGradient(this.left, this.top,
this.left, this.bottom);
gradient.addColorStop(0, 'rgba(255,255,255,0.4)');
gradient.addColorStop(0.2, 'rgba(255,255,255,0.6)');
gradient.addColorStop(0.25, 'rgba(255,255,255,0.7)');
gradient.addColorStop(0.3, 'rgba(255,255,255,0.9)');
gradient.addColorStop(0.40, 'rgba(255,255,255,0.7)');
gradient.addColorStop(0.45, 'rgba(255,255,255,0.6)');
gradient.addColorStop(0.60, 'rgba(255,255,255,0.4)');
gradient.addColorStop(1, 'rgba(255,255,255,0.1)');
this.railContext.fillStyle = gradient;
this.railContext.fill();
this.railContext.lineWidth = 0.4;
this.railContext.strokeStyle = this.strokeStyle;
this.railContext.stroke();
},
...
};
|
First, the slider's drawRail() method fills the
rail with a solid color, as shown in Figure 4.
Figure 4. Slider base
Next, drawRail() overlays a white gradient, as
shown in Figure 5.
Figure 5. Slider overlay
The result, shown in Figure 6, gives the rail some depth and makes it appear as though a light is shining on it from the top.
Figure 6. Slider composite
The slider's overlayRailGradient() method uses
the HTML5 Canvas's createLinearGradient()
method to create the gradient. Subsequently,
overlayRailGradient() adds color stops at
points along the gradient line. Each color stop is pure white with varying
degrees of opacity. Finally,
overlayRailGradient() fills the slider with the
gradient and strokes its outline.
The knob, which the slider draws in a separate canvas, is shown in Figure 7.
Figure 7. The knob canvas
Recall from Listing 5 that
the slider creates the knob's canvas with a call to
document.createElement(). The slider's
fillKnob() and
strokeKnob() methods, shown in Listing 8, draw in that canvas.
Listing 8. Drawing the knob
COREHTML5.Slider.prototype = {
...
drawKnob: function (percent) {
if (percent < 0) percent = 0;
if (percent > 1) percent = 1;
this.knobPercent = percent;
this.moveKnob(this.knobPercentToPosition(percent));
this.fillKnob();
this.strokeKnob();
},
fillKnob: function () {
this.knobContext.shadowColor = this.KNOB_SHADOW_COLOR;
this.knobContext.shadowOffsetX = this.KNOB_SHADOW_OFFSET_X;
this.knobContext.shadowOffsetY = this.KNOB_SHADOW_OFFSET_Y;
this.knobContext.shadowBlur = this.KNOB_SHADOW_BLUR;
this.knobContext.beginPath();
this.knobContext.arc(this.knobCanvas.width/2, this.knobCanvas.height/2,
this.knobCanvas.width/2-2, 0, Math.PI*2, false);
this.knobContext.clip();
this.knobContext.fillStyle = this.KNOB_FILL_STYLE;
this.knobContext.fill();
},
strokeKnob: function () {
this.knobContext.lineWidth = 1;
this.knobContext.strokeStyle = this.KNOB_STROKE_STYLE;
this.knobContext.stroke();
},
...
};
|
The drawKnob() method takes a
percent, which represents the slider's
position. That method sets the slider's value and moves the knob
accordingly, subsequently filling and stroking the knob.
The fillKnob() method fills the knob with a
translucent yellow, which allows the rail underneath to show through and
makes it appear as though the knob is lighted. The
strokeKnob() method strokes the outline of the
knob with a solid color.
The slider's code for dragging the knob is shown in Listing 9.
Listing 9. Dragging the knob
COREHTML5.Slider.prototype = {
...
addMouseListeners: function () {
var slider = this; // Let event handlers access this object
this.knobCanvas.addEventListener('mousedown', function(e) {
slider.draggingKnob = true;
e.preventDefault();
};
this.knobCanvas.addEventListener('mousemove', function(e) {
var mouse = null,
percent = null;
e.preventDefault();
if (slider.draggingKnob) {
mouse = slider.windowToCanvas(e.clientX, e.clientY);
percent = slider.knobPositionToPercent(mouse.x);
if (percent >= 0 && percent <= 1.0) {
slider.erase();
slider.draw(percent);
}
}
}, false);
this.knobCanvas.addEventListener('mouseup', function(e) {
e.preventDefault();
slider.draggingKnob = false;
};
},
windowToCanvas: function(x, y) {
var bbox = this.railCanvas.getBoundingClientRect();
return {
x: x - bbox.left * (this.railCanvas.width / bbox.width),
y: y - bbox.top * (this.railCanvas.height / bbox.height)
};
},
knobPositionToPercent: function(position) {
var railWidth = this.right - this.left - 2*this.knobRadius;
percent = (position - this.left - this.knobRadius) / railWidth;
percent = percent > 1.0 ? 1.0 : percent;
percent = percent < 0 ? 0 : percent;
return percent;
},
...
};
|
Recall from Listing 4 that
the slider's constructor calls the
addMouseListeners() method shown in Listing 9. That method adds
mouse-down, mouse-move, and mouse-up event handlers to the knob canvas
that govern the dragging of the knob canvas. Two helper
methods — windowToCanvas() and
knobPositionToPercent() — make those
event handlers easy to understand.
In this article you've seen how to implement an ad-hoc HTML5 component. The next article in this series continues the exploration of the slider component, showing you how to support change listeners, programmatically use CSS transitions to animate the slider's knob, and add the slider component to any DOM tree. See you next time.
| Description | Name | Size | Download method |
|---|---|---|---|
| Sample code | wa-html5-components-1.zip | 8KB | HTTP |
Information about download methods
Learn
- The W3C HTML5 component specifications:
-
Core HTML5 Canvas (David Geary, Prentice Hall,
2012): David Geary's book offers extensive coverage of the Canvas API and
game development. Also check out the companion website and blog.
-
The
Slider application: Run the slider application discussed in this
article online.
-
Mind-blowing apps with HTML5 Canvas: Watch David Geary's
presentation from Strange Loop 2011.
- "HTML5 fundamentals" (developerworks, October 2011): Learn HTML5
basics with this progressive developerWorks knowledge path.
-
HTML5 2D game development (developerWorks): Check
out David Geary's developerWorks series on developing an HTML5 platform
game.
- developerWorks Web
development zone: Find articles covering various web-based
solutions. See the Web
development technical library for a wide range of technical
articles and tips, tutorials, standards, and IBM Redbooks.
- developerWorks
technical events and webcasts: Stay current with technology in
these sessions.
- developerWorks Live! briefings: Get up to speed quickly on IBM
products and tools as well as IT industry trends.
-
developerWorks on-demand demos: Watch demos ranging from product
installation and setup for beginners, to advanced functionality for
experienced developers.
- developerWorks on
Twitter: Join today to follow developerWorks tweets.
Get products and technologies
-
IBM product
evaluation versions: Download or explore
the online trials in the IBM SOA Sandbox and get your hands on
application development tools and middleware products from DB2, Lotus,
Rational, Tivoli, and WebSphere.
Discuss
- developerWorks
community: Connect with other developerWorks users while exploring
the developer-driven blogs, forums, groups, and wikis.
- Find other developerWorks members interested in web development.

The author of Core HTML5 Canvas, David Geary is also the co-founder of the HTML5 Denver User's Group and the author of eight Java books, including the best-selling books on Swing and JavaServer Faces. David is a frequent speaker at conferences, including JavaOne, Devoxx, Strange Loop, NDC, and OSCON, and is a three-time JavaOne Rock Star. He is the author of the HTML5 2D game development, JSF 2 fu, and GWT fu article series for developerWorks.



