|
<!DOCTYPE HTML> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>CSS and Canvas Bezier timing function editor</title> |
|
<style type="text/css"> |
|
* { |
|
user-select: none; |
|
-moz-user-select: none; |
|
-webkit-user-select: none; |
|
} |
|
#wrapper { |
|
margin: 0 auto; |
|
width: 550px; |
|
} |
|
#bezierEditor, #clock { |
|
float: left; |
|
} |
|
#clock { |
|
margin-right: 30px; |
|
} |
|
#bezierEditor { |
|
position: relative; |
|
width: 225px; |
|
height: 225px; |
|
} |
|
#yaxis { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 20px; |
|
} |
|
#xaxis { |
|
position: absolute; |
|
bottom: 0; |
|
left: 15px; |
|
height: 20px; |
|
} |
|
#bezier { |
|
position: absolute; |
|
left: 20px; |
|
top: 5px; |
|
} |
|
#info { |
|
margin-top: 10px; |
|
clear: both; |
|
} |
|
p { |
|
padding: 0; |
|
margin: 0; |
|
} |
|
footer { |
|
margin-top: 10px; |
|
padding-top: 10px; |
|
text-align: center; |
|
} |
|
footer a { |
|
color: #09f; |
|
} |
|
dl { |
|
display: block; |
|
margin: 0; |
|
padding: 0; |
|
} |
|
dt { |
|
padding: 0; |
|
margin: 0; |
|
font-weight: bold; |
|
} |
|
dt:after { |
|
content: ': '; |
|
} |
|
</style> |
|
<script type="text/javascript" src="KeySpline.js"></script> |
|
</head> |
|
<body> |
|
<div id="wrapper"> |
|
<canvas id="clock" width="200" height="200"></canvas> |
|
<div id="bezierEditor"> |
|
<canvas id="bezier" width="200" height="200"></canvas> |
|
<canvas id="yaxis" width="20" height="210"></canvas> |
|
<canvas id="xaxis" width="210" height="20"></canvas> |
|
</div> |
|
<div id="info"> |
|
<h2>Usage</h2> |
|
<dl> |
|
<dt>CSS</dt> |
|
<dd><code>transition-timing-function: cubic-bezier(<strong id="transitionTimingFunctionValue"></strong>);</code></dd> |
|
</dl> |
|
<dl> |
|
<dt>JavaScript with KeySpline</dt> |
|
<dd><code>var k = new KeySpline(<strong id="keySplineParams"></strong>); <em style="opacity: 0.5"> ... k.get(time);</em></code></dd> |
|
</dl> |
|
</div> |
|
|
|
<footer id="footer"> |
|
<a href="http://greweb.me/2012/02/bezier-curve-based-easing-functions-from-concept-to-implementation/">Read the article</a> - |
|
<a href="https://gist.github.com/1926947">Gist</a> |
|
</footer> |
|
</div> |
|
|
|
<script type="text/javascript">(function(){ |
|
window.requestAnimFrame = (function(){ |
|
return window.requestAnimationFrame || |
|
window.webkitRequestAnimationFrame || |
|
window.mozRequestAnimationFrame || |
|
window.oRequestAnimationFrame || |
|
window.msRequestAnimationFrame || |
|
function( callback ){ |
|
window.setTimeout(callback, 1000 / 60); |
|
}; |
|
})(); |
|
|
|
if (window.parent != window) { |
|
var node = document.getElementById("footer"); |
|
node.style.display = "none"; |
|
} |
|
|
|
var cumulativeOffset = function(element) { |
|
var top = 0, left = 0; |
|
do { |
|
top += element.offsetTop || 0; |
|
left += element.offsetLeft || 0; |
|
element = element.offsetParent; |
|
} while(element); |
|
|
|
return { |
|
top: top, |
|
left: left |
|
}; |
|
}; |
|
|
|
|
|
var bezier; |
|
var fun; |
|
|
|
var currentStep = [0, 0]; |
|
|
|
var transitionTimingFunctionValue = document.getElementById("transitionTimingFunctionValue"); |
|
var keySplineParams = document.getElementById("keySplineParams"); |
|
|
|
function setBezier (b) { |
|
bezier = b; |
|
fun = new KeySpline(b[0], b[1], b[2], b[3]).get; |
|
for (var i = 0; i < b.length; ++i) |
|
b[i] = Math.floor(b[i]*100)/100; |
|
var str = b+""; |
|
transitionTimingFunctionValue.innerHTML = str; |
|
keySplineParams.innerHTML = str; |
|
} |
|
function getBezierFunction () { |
|
return fun; |
|
} |
|
|
|
(function(){ |
|
var canvas = document.getElementById("xaxis"); |
|
var ctx = canvas.getContext("2d"); |
|
for (var i=0; i<=10; ++i) { |
|
var n = i / 10; |
|
var high = (i % 5 == 0); |
|
var x = 5 + n*(canvas.width-10); |
|
x = Math.floor(x); |
|
ctx.font = "9px monospace"; |
|
ctx.textBaseline = "top"; |
|
ctx.textAlign = "center"; |
|
ctx.beginPath(); |
|
ctx.moveTo(0, 0); |
|
ctx.lineTo(canvas.width, 0); |
|
ctx.stroke(); |
|
ctx.beginPath(); |
|
ctx.moveTo(x, 0); |
|
if (high) { |
|
ctx.lineTo(x, 8); |
|
ctx.stroke(); |
|
ctx.fillText(""+n, x, 10); |
|
} |
|
else { |
|
ctx.lineTo(x, 4); |
|
ctx.stroke(); |
|
} |
|
ctx.textBaseline = "bottom"; |
|
ctx.fillText("time", canvas.width-40, 20); |
|
} |
|
}()); |
|
|
|
|
|
(function(){ |
|
var canvas = document.getElementById("yaxis"); |
|
var ctx = canvas.getContext("2d"); |
|
for (var i=0; i<=10; ++i) { |
|
var n = i / 10; |
|
var high = (i % 5 == 0); |
|
var y = 5 + (1-n)*(canvas.height-10); |
|
y = Math.floor(y); |
|
ctx.font = "9px monospace"; |
|
ctx.textBaseline = "middle"; |
|
ctx.textAlign = "right"; |
|
ctx.beginPath(); |
|
ctx.moveTo(20, 0); |
|
ctx.lineTo(20, canvas.height); |
|
ctx.stroke(); |
|
ctx.beginPath(); |
|
ctx.moveTo(20, y); |
|
ctx.lineTo(16, y); |
|
ctx.stroke(); |
|
if (high) { |
|
ctx.fillText(""+Math.floor(n*100), 15, y); |
|
} |
|
} |
|
ctx.textAlign = "left"; |
|
ctx.textBaseline = "bottom"; |
|
ctx.rotate(Math.PI/2); |
|
ctx.fillText("percentage", 20, 0); |
|
}()); |
|
|
|
(function(){ |
|
var canvas = document.getElementById("clock"); |
|
var ctx = canvas.getContext("2d"); |
|
|
|
var DURATION = 1500; |
|
var LINE_WIDTH = 0.3; |
|
|
|
// state variables |
|
var tStart = +new Date(); |
|
|
|
function setup () { |
|
ctx.scale(canvas.width, canvas.height); |
|
} |
|
|
|
function render () { |
|
var bezierf = getBezierFunction(); |
|
var now = +new Date(); |
|
var t = (now - tStart) % (2*DURATION); |
|
var reverse = t > DURATION; |
|
if (reverse) t -= DURATION; |
|
var x = t / DURATION; |
|
var y = bezierf(x); |
|
currentStep = [ x, y ]; |
|
ctx.clearRect(0,0,1,1); |
|
ctx.strokeStyle = 'red'; |
|
ctx.lineWidth = LINE_WIDTH; |
|
ctx.beginPath(); |
|
ctx.arc(0.5, 0.5, 0.3, 0, y*2*Math.PI, reverse); |
|
ctx.stroke(); |
|
} |
|
|
|
setup(); |
|
(function loop () { |
|
requestAnimFrame(function() { |
|
loop(); |
|
render(); |
|
}, canvas); |
|
}()); |
|
}()); |
|
|
|
|
|
(function(){ |
|
var canvas = document.getElementById("bezier"); |
|
var ctx = canvas.getContext("2d"); |
|
|
|
var HANDLE_RADIUS = 0.03; |
|
|
|
// state variables |
|
// handles positions |
|
var handle = [ null, [0.25, 0.25], [0.75, 0.75] ]; |
|
var draggingHandle = 0; |
|
var hoveringHandle = 0; |
|
|
|
var hovering = false; |
|
var stime = +new Date(); |
|
var oneHandleClicked = false; |
|
|
|
function positionWithE (e) { |
|
var o = cumulativeOffset(canvas); |
|
return { x: relativeX(e.clientX-o.left), y: relativeY(e.clientY-o.top) }; |
|
} |
|
|
|
function setup() { |
|
canvas.addEventListener("mousedown", function (e) { |
|
var p = positionWithE(e); |
|
var hnum = findHandle(p.x, p.y); |
|
if (hnum) { |
|
draggingHandle = hnum; |
|
oneHandleClicked = true; |
|
} |
|
}); |
|
canvas.addEventListener("mouseup", function (e) { |
|
var p = positionWithE(e); |
|
if (draggingHandle) { |
|
setHandle(draggingHandle, p.x, p.y); |
|
draggingHandle = 0; |
|
} |
|
}); |
|
canvas.addEventListener("mousemove", function (e) { |
|
var p = positionWithE(e); |
|
if (draggingHandle) { |
|
setHandle(draggingHandle, p.x, p.y); |
|
} |
|
hoveringHandle = draggingHandle || findHandle(p.x, p.y); |
|
}); |
|
canvas.addEventListener("mouseover", function (e) { |
|
hovering = true; |
|
hasChanged = true; |
|
}); |
|
canvas.addEventListener("mouseout", function (e) { |
|
hovering = false; |
|
hasChanged = true; |
|
draggingHandle = 0; |
|
hoveringHandle = 0; |
|
}); |
|
|
|
syncBezier(); |
|
} |
|
|
|
function render () { |
|
var now = +new Date(); |
|
|
|
ctx.save(); |
|
ctx.fillStyle = 'white'; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
// Draw grid |
|
|
|
ctx.translate(0, canvas.height); |
|
ctx.scale(canvas.width, -canvas.height); |
|
|
|
// Draw projections |
|
ctx.lineWidth = 0.01; |
|
ctx.strokeStyle = "rgba(0,0,0,0.5)"; |
|
ctx.beginPath(); |
|
ctx.moveTo(currentStep[0], 0); |
|
ctx.lineTo(currentStep[0], currentStep[1]); |
|
ctx.lineTo(0, currentStep[1]); |
|
ctx.stroke(); |
|
|
|
// Draw bezier |
|
ctx.lineWidth = 0.02; |
|
ctx.strokeStyle = "black"; |
|
ctx.beginPath(); |
|
ctx.moveTo(0, 0); |
|
ctx.bezierCurveTo(handle[1][0], handle[1][1], handle[2][0], handle[2][1], 1, 1); |
|
ctx.stroke(); |
|
|
|
// Draw handle |
|
ctx.strokeStyle = "red"; |
|
ctx.fillStyle = "white"; |
|
ctx.lineWidth = 0.01; |
|
|
|
ctx.beginPath(); |
|
ctx.moveTo(0, 0); |
|
ctx.lineTo(handle[1][0], handle[1][1]); |
|
ctx.stroke(); |
|
|
|
ctx.beginPath(); |
|
ctx.moveTo(1, 1); |
|
ctx.lineTo(handle[2][0], handle[2][1]); |
|
ctx.stroke(); |
|
|
|
var r = HANDLE_RADIUS; |
|
if (!oneHandleClicked) { |
|
r += 0.2*HANDLE_RADIUS*Math.cos((stime-now)/150); |
|
} |
|
for (var i=1; i<handle.length; ++i) { |
|
var h = handle[i]; |
|
ctx.beginPath(); |
|
ctx.arc(h[0], h[1], r, 0, Math.PI*2); |
|
ctx.fillStyle = (hoveringHandle == i) ? "red" : "white"; |
|
ctx.fill(); |
|
ctx.stroke(); |
|
} |
|
ctx.restore(); |
|
} |
|
|
|
function syncBezier () { |
|
setBezier([ handle[1][0], handle[1][1], handle[2][0], handle[2][1] ]); |
|
} |
|
|
|
|
|
function findHandle (x, y) { |
|
for (var i=1; i<handle.length; ++i) { |
|
var h = handle[i]; |
|
var radius = HANDLE_RADIUS; |
|
var dx = x - h[0]; |
|
var dy = y - h[1]; |
|
if (dx*dx+dy*dy < HANDLE_RADIUS*HANDLE_RADIUS) |
|
return i; |
|
} |
|
return 0; |
|
} |
|
|
|
function setHandle (num, x, y) { |
|
handle [num] = [x, y]; |
|
syncBezier(); |
|
render(); |
|
} |
|
|
|
function relativeX (x) { |
|
return x/canvas.width; |
|
} |
|
function relativeY (y) { |
|
return 1-y/canvas.height; |
|
} |
|
|
|
setup(); |
|
(function loop () { |
|
requestAnimFrame(function() { |
|
loop(); |
|
render(); |
|
}, canvas); |
|
}()); |
|
|
|
}()); |
|
|
|
}()); |
|
</script> |
|
|
|
<script type="text/javascript"> |
|
var _gaq = _gaq || []; |
|
_gaq.push(['_setAccount', 'UA-9919624-4']); |
|
_gaq.push(['_trackPageview']); |
|
|
|
(function() { |
|
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; |
|
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; |
|
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); |
|
})(); |
|
</script> |
|
</body> |
|
</html> |
This comment has been minimized.
I think I found a bug. Drag the left handle to (1, 90). Drag the right handle to (0, 10). You can see the circular graph go wild.