This is a quick implementation based on the visual and interaction design of Stan Yakusevich which I stumbled upon on Dribbble. The playful concept and design made me curious as to how the interaction would feel when using it. Here's the code to make it work:
HourSlider = function( container, width, height )
{
var _knob = { x: 20, y: height - 60, width: 40, height: 40, size: 20 };
var _minK;
var _maxK;
var _value = 0.0;
var _valueOld = 0.0;
var _infoOffset = 90;
var _infoOffsetOld = 0;
var _hoursPart = [ 1, 3, 6, 12, 24 ];
var _hoursFull = [];
var _index = 0;
//...
_minK = _knob.size * 2;
_maxK = _width - ( 2 * _minK );
var hx = _width / 2;
var hy = _height / 3;
for( var i = 0; i < 24; ++i )
{
_hoursFull.push( {
hour: ( i + 1 ),
cx: hx, cy: hy, cs: 100, ca: 0,
tx: hx, ty: hy, ts: 100, ta: 0
} );
}
//...
function render()
{
var kx = _knob.x;
var ky = _knob.y;
// clear all
_context.clearRect( 0, 0, _width, _height );
// background
var gradient = _context.createLinearGradient( _width, 0, _width - ( _width / 4 ), _height );
gradient.addColorStop( 0.00, hsv2rgb( 0.58 + ( ( 1 - _value ) * 0.52 ), 0.82, 1.00 ) );
gradient.addColorStop( 1.00, hsv2rgb( 0.70 + ( ( 1 - _value ) * 0.32 ), 0.65, 1.00 ) );
_context.beginPath();
_context.rect( 0, 0, _width, _height );
_context.fillStyle = gradient;
_context.fill();
// white panel with little wave
_context.beginPath();
_context.rect( 0, _height - 50, _width, 50 );
_context.fillStyle = "#ffffff";
_context.fill();
_context.beginPath();
_context.moveTo( kx - 90, ky + 10 );
_context.bezierCurveTo( kx - 10, ky + 10, kx - 10, ky - 10, kx + 20, ky - 10 );
_context.moveTo( kx + 20, ky - 10 );
_context.bezierCurveTo( kx + 50, ky - 10, kx + 50, ky + 10, kx + 130, ky + 10 );
_context.lineTo( kx - 90, ky + 10 );
_context.fillStyle = "#ffffff";
_context.fill();
// slider track
_context.beginPath();
_context.moveTo( _minK, _knob.y + _knob.size );
_context.lineTo( _minK + _maxK, _knob.y + _knob.size );
_context.strokeStyle = "#dddddd";
_context.stroke();
// slider knob
_context.save();
_context.beginPath();
_context.arc( _knob.x + _knob.size, _knob.y + _knob.size, _knob.size, 0, Math.PI * 2 );
_context.shadowColor = "rgba(0, 0, 0, 0.3)";
_context.shadowBlur = 8;
_context.shadowOffsetX = 0;
_context.shadowOffsetY = 3;
_context.fillStyle = "#ffffff";
_context.fill();
_context.restore();
// slider hours
_context.textBaseline = "bottom";
_context.textAlign = "center";
_context.fillStyle = "#ffffff";
_context.strokeStyle = "#ffffff";
var step = _maxK / 4;
for( var i = 0; i <= 4; ++i )
{
var p = { x: _minK + ( i * step ), y: _knob.y - 15 };
var d = dist( p, _knob, 1.5 );
var s = Math.floor( 12 + d.d * 2 );
_context.beginPath();
_context.moveTo( d.x, d.y + 6 );
_context.lineTo( d.x, d.y );
_context.stroke();
_context.font = "100 " + s + "px sans-serif";
_context.fillText( _hoursPart[ i ], Math.floor( p.x ), Math.floor( p.y - 10 ) );
}
// main hour value
_context.textBaseline = "middle";
_context.textAlign = "center";
for( var j = 0; j < _hoursFull.length; ++j )
{
var h = _hoursFull[ j ];
h.cx += ( h.tx - h.cx ) * 0.152;
h.cy += ( h.ty - h.cy ) * 0.152;
h.cs += ( h.ts - h.cs ) * 0.152;
h.ca += ( h.ta - h.ca ) * 0.1;
if( h.ca > 1 )
{
_context.font = "700 " + Math.floor( h.cs ) + "px sans-serif";
_context.fillStyle = "rgba(255, 255, 255, " + ( h.ca / 100.0 ) + ")";
_context.fillText( h.hour, h.cx, h.cy );
}
}
// main hour info
_context.font = "100 14px sans-serif";
_context.textBaseline = "middle";
_context.fillStyle = "#ffffff";
_context.textAlign = "right";
_context.fillText( "Every", _width * 0.5 - _infoOffsetOld, _height / 3 );
_context.textAlign = "left";
_context.fillText( "hours", _width * 0.5 + _infoOffsetOld, _height / 3 );
// frame
_context.beginPath();
_context.rect( 0, 0, _width, _height );
_context.strokeStyle = "#999";
_context.stroke();
_infoOffsetOld += ( _infoOffset - _infoOffsetOld ) * 0.0652;
requestAnimationFrame( render );
}
function update( v )
{
_value = Math.max( Math.min( ( ( v - _minK ) / _maxK ), 1.0 ), 0.0 );
var h = Math.floor( _value * ( _hoursPart.length - 1) );
var n = _hoursPart.length - 1;
var t = _value * 400;
_knob.x = _minK + ( _value * _maxK ) - _knob.size;
_index = Math.round( larp(
_hoursPart[ Math.min( h, n ) ],
_hoursPart[ Math.min( h + 1, n ) ],
( t % 100 ) / 100
) ) - 1;
var hx = _width / 2;
var hy = _height / 3;
for( var i = 0; i < _hoursFull.length; ++i )
{
var hf = _hoursFull[ i ];
var o = Math.max( Math.min( i - _index, 1 ), -1 );
hf.tx = hx;
hf.ts = 140;
hf.ty = hy;
hf.ta = ( ( i == _index ) ? 100.0 : 0.0 );
if( o < 0 )
{
hf.tx -= o * 50;
hf.ts -= Math.exp( o ) * 100;
}
else if( o > 0 )
{
hf.tx -= o * 200;
hf.ts += Math.exp( o ) * 100;
}
}
}
function hsv2rgb( h, s, v )
{
s = Math.min( 1.0, s );
v = Math.min( 1.0, v );
var r, g, b, i, f, p, q, t;
i = Math.floor( h * 6 );
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch( i % 6 )
{
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
return "rgba(" + ( (r * 255) >> 0 ) + "," + ( (g * 255) >> 0 ) + "," + ( (b * 255) >> 0 ) + ", 1)";
}
function larp( a, b, p )
{
return (b - a) * p + a;
}
function dist( p, center, pow )
{
var radius = 120;
var dx = p.x - center.x;
var dy = p.y - center.y;
var dd = Math.sqrt( dx * dx + dy * dy );
if( dd >= radius )
{
return { x: p.x, y: p.y, d: 0 };
}
var e = Math.exp( pow );
var k = ( e / (e - 1) * radius ) * (1 - Math.exp( -dd * ( pow / radius ) )) / dd * .75 + .25;
var y = center.y + dy * k;
return { x: p.x, y: y, d: Math.abs( p.y - y ) };
}
//...
};