The code on this page will not work in Internet
Explorer. IE does not support the HTML5 canvas tag.
Someone might be able to adapt this to detect IE and use VML
instead, but that is out of the scope of this post.
The only external code this example relies on is Cake.js,
a library that abstracts and automates a lot of the steps required
in using the HTML5 canvas tag.
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
#canvas {
width: 600px;
height: 400px;
}
</style>
<title>Canvas Experiment</title>
</head>
<body>
<div id="canvas">
<!-- This container will hold our canvas element. -->
</div>
<button type="button">Save Image</button>
<div id="saved">
<!-- We will output our images here so we know they got saved. -->
</div>
<!-- download and host on your own site -->
<script type="text/javascript" src="cake.js"></script>
<!-- main.js includes all of the javascript in this blog post -->
<script type="text/javascript" src="main.js"></script>
</body>
</html>
// you can use your favorite library's CSS getStyle method here...
function getStyle(element, property) {
if (typeof element == 'string')
element = document.getElementById(element);
if (element.style[property]) {
// inline style property
return element.style[property];
} else if (element.currentStyle) {
// external stylesheet for Explorer
return element.currentStyle[property];
} else if (document.defaultView && document.defaultView.getComputedStyle) {
// external stylesheet for Mozilla and Safari 1.3+
property = property.replace(/([A-Z])/g, "-$1");
property = property.toLowerCase();
return document.defaultView.getComputedStyle(element, "").getPropertyValue(property);
} else {
return null;
}
}
// we'll be calling this later to erase drawn images
function resetCanvas() {
canvas.clear = true;
setTimeout(function() {
canvas.clear = false;
}, 100);
}
// get the container's dimensions and create a new Cake.js Canvas
var w = parseInt(getStyle('canvas', 'width'), 10);
var h = parseInt(getStyle('canvas', 'height'), 10);
var canvas = new Canvas(document.getElementById('canvas'), w, h);
// setup our canvas
canvas.frameDuration = 10;
canvas.addEventListener('mousedown', function(e) {
e.preventDefault();
}, false);
resetCanvas();
/**
* A CanvasNode can be used to group other nodes, but essentially
* it is a drawing building block. In this case we will only need
* one node.
*/
var scene = new CanvasNode();
// Polygon is another Cake.js class
var brush = new Polygon([], {
closePath : false
});
// configure the brush size and color
brush.strokeWidth = 2;
brush.stroke = '#f60';
// Frame frequency is based on frameDuration (which we set above)
brush.addFrameListener(function() {
var moves = this.root.mouseEvents.allWith(2).take(0, 1).flatten()
if (moves.length > 0 || this.root.mouseDown) {
// this block draws a series of line segments following the mouse's path
this.segments = (this.segments.length) ? this.segments.slice(-2).concat(moves) : moves;
if (this.segments.length == 2) {
this.segments = this.segments.concat(this.segments);
}
this.visible = (this.segments.length > 0)
} else {
if (this.segments.length > 0) {
this.segments = [];
}
this.strokeStarted = false;
this.visible = false;
}
});
// append elements
scene.append(brush);
canvas.append(scene);
var button = document.getElementsByTagName('button')[0];
button.addEventListener('click', function() {
var data = canvas.canvas.toDataURL('image/png').split(/,/)[1];
var query = 'data=' + encodeURIComponent(data);
// you can use your favorite library's AJAX request method
var http = new XMLHttpRequest();
http.open('POST', 'saveImage.php', true);
http.onreadystatechange = function() {
if (http.readyState === 4) { // XMLHttpRequest code for "Done"
if (http.status === 200) { // HTTP status code for "OK"
// see below for what saveImage.php outputs
var img = document.createElement('img');
img.src = http.responseText;
// append the new image
var saved = document.getElementById('saved');
saved.insertBefore(img, saved.childNodes[0]);
// clear the drawing area
resetCanvas();
}
}
};
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.setRequestHeader("Content-length", query.length);
http.setRequestHeader("Connection", "close");
http.send(query);
}, false);
<?php
$filename = md5($_POST['data']) . '.png';
$data = base64_decode($_POST['data']);
file_put_contents("./saved/$filename", $data);
echo "./saved/$filename";
exit;
?>
In case you're wondering, this section title is paraphrased from a Mitch Hedberg joke from the comedy album Strategic Grill Locations.
(function() {
function getStyle(element, property) {
if (typeof element == 'string')
element = document.getElementById(element);
if (element.style[property]) {
return element.style[property];
} else if (element.currentStyle) {
return element.currentStyle[property];
} else if (document.defaultView && document.defaultView.getComputedStyle) {
property = property.replace(/([A-Z])/g, "-$1");
property = property.toLowerCase();
return document.defaultView.getComputedStyle(element, "").getPropertyValue(property);
} else {
return null;
}
}
function resetCanvas() {
canvas.clear = true;
setTimeout(function() {
canvas.clear = false;
}, 100);
}
var w = parseInt(getStyle('canvas', 'width'), 10);
var h = parseInt(getStyle('canvas', 'height'), 10);
var canvas = new Canvas(document.getElementById('canvas'), w, h);
canvas.frameDuration = 10;
canvas.addEventListener('mousedown', function(e) {
e.preventDefault();
}, false);
resetCanvas();
var scene = new CanvasNode();
var brush = new Polygon([], {
closePath : false
});
brush.strokeWidth = 2;
brush.stroke = '#f60';
brush.addFrameListener(function() {
var moves = this.root.mouseEvents.allWith(2).take(0, 1).flatten()
if (moves.length > 0 || this.root.mouseDown) {
this.segments = (this.segments.length) ? this.segments.slice(-2).concat(moves) : moves;
if (this.segments.length == 2) {
this.segments = this.segments.concat(this.segments);
}
this.visible = (this.segments.length > 0)
} else {
if (this.segments.length > 0) {
this.segments = [];
}
this.strokeStarted = false;
this.visible = false;
}
});
scene.append(brush);
canvas.append(scene);
var button = document.getElementsByTagName('button')[0];
button.addEventListener('click', function() {
var data = canvas.canvas.toDataURL('image/png').split(/,/)[1];
var query = 'data=' + encodeURIComponent(data);
var http = new XMLHttpRequest();
http.open('POST', 'saveImage.php', true);
http.onreadystatechange = function() {
if (http.readyState === 4) {
if (http.status === 200) {
var img = document.createElement('img');
img.src = http.responseText;
var saved = document.getElementById('saved');
saved.insertBefore(img, saved.childNodes[0]);
resetCanvas();
}
}
};
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.setRequestHeader("Content-length", query.length);
http.setRequestHeader("Connection", "close");
http.send(query);
}, false);
})();
If you copy the HTML, JavaScript, and PHP to your server, you should have a fully working image saver. Let me know if you have any trouble getting this set up and I'll see what I can do.
I'm a Front-End Engineer at Yahoo! working on the Mail and Messenger teams. I blog about web design and development topics including accessibility, usability, performance, and developing HTML / CSS / JavaScript applications on Appcelerator Titanium and Adobe AIR.
If you're a web developer, you might enjoy Jelo, my JavaScript library.
All original work on this site is covered by a Creative Commons Attribution 3.0 license unless otherwise specified.
You may share or use any code or images from this site in any manner, for free, so long as reasonable effort has been made to give credit where due.
The views expressed in the posts and comments on this blog do not necessarily reflect the views of Yahoo!