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.
A few panoramic shots I took at SDCC 2010. #geek http://bit.ly/bwX6GB
JS version of Regex prime number checker:
function isPrime(n) {
return Array(n + 1).join("1")
.search(/^1?$|^(11+?)\1+$/) == -1;
}
Погрузился в пучину EcmaScript5, местами увлекательно, местами нудно =)
Modernizr http://ow.ly/18njQ1
A Collection of 20 HTML5 Video Players - a round-up of JavaScript and html5 alternatives to Flash-based media player... http://ow.ly/18njQ2
jQuery TOOLS - The missing UI library for the Web http://ow.ly/18njQ3
Contactable - A jQuery Plugin | the odin http://ow.ly/18njQ4
Giants vs Dodgers, sweet seats. http://twitpic.com/2ag9pa
@snookca That'll be fixed next week. I promise.
@snookca I was tryna not name names ;) But really that was just par for the course today, pretty hectic day. As I'm sure you know.
Who breaks major stuff after 4pm on Friday? On the last day of the sprint, no less. Tsk. (wasn't me)
Awesome live git tracker for teams: http://www.utsup.com/
RT @DerrenBrown: Blog post: Camera Software Lets You See Into the Past http://bit.ly/9kjVg5
10 invites to the new version of Digg: http://bit.ly/dqM8EV
Threaded vs Evented Servers, great look at the whats and whys. http://bit.ly/bDUEjn #geek
Nav, Context menus, "app-style" toolbars in sample chapter http://bit.ly/csTRY8 of new YUI book http://bit.ly/cJINoV
Add a side-mounted End Call button to your iPhone 4: http://bit.ly/cGxPBD #funny #geekAll 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!