Created
March 11, 2012 04:46
-
-
Save caryfitzhugh/2015072 to your computer and use it in GitHub Desktop.
Simple Proof Of Concept For SVG image annotation in browser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<script src="http://code.jquery.com/jquery-1.7.1.min.js"></script> | |
<script src="http://github.com/DmitryBaranovskiy/raphael/raw/master/raphael-min.js"></script> | |
<script> | |
/** | |
* A Simple Vector Shape Drawing App with RaphaelJS and jQuery | |
* copyright 2010 Kayla Rose Martin - Licensed under the MIT license | |
* Inspired by http://stackoverflow.com/questions/3582344/draw-a-connection-line-in-raphaeljs | |
**/ | |
/* THen edited by Cary FitzHugh to add an example of exporting the SVG and adding polygons | |
*/ | |
/** | |
* Raphael.Export https://github.com/ElbertF/Raphael.Export | |
* | |
* Licensed under the MIT license: | |
* http://www.opensource.org/licenses/mit-license.php | |
* | |
*/ | |
(function(R) { | |
/** | |
* Escapes string for XML interpolation | |
* @param value string or number value to escape | |
* @returns string escaped | |
*/ | |
function escapeXML(s) { | |
if ( typeof s === 'number' ) return s.toString(); | |
var replace = { '&': 'amp', '<': 'lt', '>': 'gt', '"': 'quot', '\'': 'apos' }; | |
for ( var entity in replace ) { | |
s = s.replace(new RegExp(entity, 'g'), '&' + replace[entity] + ';'); | |
} | |
return s; | |
} | |
/** | |
* Generic map function | |
* @param iterable the array or object to be mapped | |
* @param callback the callback function(element, key) | |
* @returns array | |
*/ | |
function map(iterable, callback) { | |
var mapped = new Array; | |
for ( var i in iterable ) { | |
if ( iterable.hasOwnProperty(i) ) { | |
var value = callback.call(this, iterable[i], i); | |
if ( value !== null ) mapped.push(value); | |
} | |
} | |
return mapped; | |
} | |
/** | |
* Generic reduce function | |
* @param iterable array or object to be reduced | |
* @param callback the callback function(initial, element, i) | |
* @param initial the initial value | |
* @return the reduced value | |
*/ | |
function reduce(iterable, callback, initial) { | |
for ( var i in iterable ) { | |
if ( iterable.hasOwnProperty(i) ) { | |
initial = callback.call(this, initial, iterable[i], i); | |
} | |
} | |
return initial; | |
} | |
/** | |
* Utility method for creating a tag | |
* @param name the tag name, e.g., 'text' | |
* @param attrs the attribute string, e.g., name1="val1" name2="val2" | |
* or attribute map, e.g., { name1 : 'val1', name2 : 'val2' } | |
* @param content the content string inside the tag | |
* @returns string of the tag | |
*/ | |
function tag(name, attrs, matrix, content) { | |
if ( typeof content === 'undefined' || content === null ) { | |
content = ''; | |
} | |
if ( typeof attrs === 'object' ) { | |
attrs = map(attrs, function(element, name) { | |
return name + '="' + escapeXML(element) + '"'; | |
}).join(' '); | |
} | |
return '<' + name + ( matrix ? ' transform="matrix(' + matrix.toString().replace(/^matrix\(|\)$/g, '') + ')" ' : ' ' ) + attrs + '>' + content + '</' + name + '>'; | |
} | |
/** | |
* @return object the style object | |
*/ | |
function extractStyle(node) { | |
return { | |
font: { | |
family: node.attrs.font.replace(/^.*?"(\w+)".*$/, '$1'), | |
size: typeof node.attrs['font-size'] === 'undefined' ? null : node.attrs['font-size'] | |
} | |
}; | |
} | |
/** | |
* @param style object from style() | |
* @return string | |
*/ | |
function styleToString(style) { | |
// TODO figure out what is 'normal' | |
return 'font: normal normal normal 10px/normal ' + style.font.family + ( style.font.size === null ? '' : '; font-size: ' + style.font.size + 'px' ); | |
} | |
/** | |
* Computes tspan dy using font size. This formula was empircally determined | |
* using a best-fit line. Works well in both VML and SVG browsers. | |
* @param fontSize number | |
* @return number | |
*/ | |
function computeTSpanDy(fontSize, line, lines) { | |
if ( fontSize === null ) fontSize = 10; | |
//return fontSize * 4.5 / 13 | |
return fontSize * 4.5 / 13 * ( line - .2 - lines / 2 ) * 3.5; | |
} | |
var serializer = { | |
'text': function(node) { | |
style = extractStyle(node); | |
var tags = new Array; | |
node.attrs['text'].split('\n').map(function(text, line) { | |
tags.push(tag( | |
'text', | |
reduce( | |
node.attrs, | |
function(initial, value, name) { | |
if ( name !== 'text' && name !== 'w' && name !== 'h' ) { | |
if ( name === 'font-size') value = value + 'px'; | |
initial[name] = escapeXML(value.toString()); | |
} | |
return initial; | |
}, | |
{ style: 'text-anchor: middle; ' + styleToString(style) + ';' } | |
), | |
node.matrix, | |
tag('tspan', { dy: computeTSpanDy(style.font.size, line + 1, node.attrs['text'].split('\n').length) }, null, text) | |
)); | |
}); | |
return tags; | |
}, | |
'path' : function(node) { | |
var initial = ( node.matrix.a === 1 && node.matrix.d === 1 ) ? {} : { 'transform' : node.matrix.toString() }; | |
return tag( | |
'path', | |
reduce( | |
node.attrs, | |
function(initial, value, name) { | |
if ( name === 'path' ) name = 'd'; | |
initial[name] = value.toString(); | |
return initial; | |
}, | |
{} | |
), | |
node.matrix | |
); | |
} | |
// Other serializers should go here | |
}; | |
R.fn.toSVG = function() { | |
var | |
paper = this, | |
restore = { svg: R.svg, vml: R.vml }, | |
svg = '<svg style="overflow: hidden; position: relative;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="' + paper.width + '" version="1.1" height="' + paper.height + '">' | |
; | |
R.svg = true; | |
R.vml = false; | |
for ( var node = paper.bottom; node != null; node = node.next ) { | |
var attrs = ''; | |
// Use serializer | |
if ( typeof serializer[node.type] === 'function' ) { | |
svg += serializer[node.type](node); | |
continue; | |
} | |
switch ( node.type ) { | |
case 'image': | |
attrs += ' preserveAspectRatio="none"'; | |
break; | |
} | |
for ( i in node.attrs ) { | |
var name = i; | |
switch ( i ) { | |
case 'src': | |
name = 'xlink:href'; | |
break; | |
case 'transform': | |
name = ''; | |
break; | |
} | |
if ( name ) { | |
attrs += ' ' + name + '="' + escapeXML(node.attrs[i].toString()) + '"'; | |
} | |
} | |
svg += '<' + node.type + ' transform="matrix(' + node.matrix.toString().replace(/^matrix\(|\)$/g, '') + ')"' + attrs + '></' + node.type + '>'; | |
} | |
svg += '</svg>'; | |
R.svg = restore.svg; | |
R.vml = restore.vml; | |
return svg; | |
}; | |
})(window.Raphael); | |
/*! | |
* raphaeljs.serialize | |
* | |
* Copyright (c) 2010 Jonathan Spies | |
* Licensed under the MIT license: | |
* (http://www.opensource.org/licenses/mit-license.php) | |
* | |
*/ | |
var RaphaelSerialize = { | |
json: function(paper) { | |
var svgdata = []; | |
paper.forEach(function(node) { | |
if (node && node.type) { | |
switch(node.type) { | |
case "image": | |
var object = { | |
type: node.type, | |
width: node.attrs['width'], | |
height: node.attrs['height'], | |
x: node.attrs['x'], | |
y: node.attrs['y'], | |
src: node.attrs['src'], | |
transform: node.transformations ? node.transformations.join(' ') : '' | |
} | |
break; | |
case "circle": | |
var object = { | |
type: node.type, | |
r: node.attrs['r'], | |
cx: node.attrs['cx'], | |
cy: node.attrs['cy'], | |
stroke: node.attrs['stroke'] === 0 ? 'none': node.attrs['stroke'], | |
'stroke-width': node.attrs['stroke-width'], | |
fill: node.attrs['fill'] | |
} | |
break; | |
case "ellipse": | |
var object = { | |
type: node.type, | |
rx: node.attrs['rx'], | |
ry: node.attrs['ry'], | |
cx: node.attrs['cx'], | |
cy: node.attrs['cy'], | |
stroke: node.attrs['stroke'] === 0 ? 'none': node.attrs['stroke'], | |
'stroke-width': node.attrs['stroke-width'], | |
fill: node.attrs['fill'] | |
} | |
break; | |
case "rect": | |
var object = { | |
type: node.type, | |
x: node.attrs['x'], | |
y: node.attrs['y'], | |
width: node.attrs['width'], | |
height: node.attrs['height'], | |
stroke: node.attrs['stroke'] === 0 ? 'none': node.attrs['stroke'], | |
'stroke-width': node.attrs['stroke-width'], | |
fill: node.attrs['fill'] | |
} | |
break; | |
case "text": | |
var object = { | |
type: node.type, | |
font: node.attrs['font'], | |
'font-family': node.attrs['font-family'], | |
'font-size': node.attrs['font-size'], | |
stroke: node.attrs['stroke'] === 0 ? 'none': node.attrs['stroke'], | |
fill: node.attrs['fill'] === 0 ? 'none' : node.attrs['fill'], | |
'stroke-width': node.attrs['stroke-width'], | |
x: node.attrs['x'], | |
y: node.attrs['y'], | |
text: node.attrs['text'], | |
'text-anchor': node.attrs['text-anchor'] | |
} | |
break; | |
case "path": | |
var path = ""; | |
if(node.attrs['path'].constructor != Array){ | |
path += node.attrs['path']; | |
} | |
else{ | |
$.each(node.attrs['path'], function(i, group) { | |
$.each(group, | |
function(index, value) { | |
if (index < 1) { | |
path += value; | |
} else { | |
if (index == (group.length - 1)) { | |
path += value; | |
} else { | |
path += value + ','; | |
} | |
} | |
}); | |
}); | |
} | |
var object = { | |
type: node.type, | |
fill: node.attrs['fill'], | |
opacity: node.attrs['opacity'], | |
translation: node.attrs['translation'], | |
scale: node.attrs['scale'], | |
path: path, | |
stroke: node.attrs['stroke'] === 0 ? 'none': node.attrs['stroke'], | |
'stroke-width': node.attrs['stroke-width'], | |
transform: node.transformations ? node.transformations.join(' ') : '' | |
} | |
} | |
if (object) { | |
svgdata.push(object); | |
} | |
} | |
}); | |
return(JSON.stringify(svgdata)); | |
}, | |
load_json : function(paper, json) { | |
if (typeof(json) == "string") { json = JSON.parse(json); } // allow stringified or object input | |
var set = paper.set(); | |
$.each(json, function(index, node) { | |
try { | |
var el = paper[node.type]().attr(node); | |
set.push(el); | |
} catch(e) {} | |
}); | |
return set; | |
} | |
}; | |
function Circle(startX, startY, width, raphael) { | |
var start = { | |
x: startX, | |
y: startY, | |
w: width | |
}; | |
var end = { | |
w: width | |
}; | |
var getWidth = function() { | |
return end.w; | |
}; | |
var redraw = function() { | |
node.attr({r: getWidth()}); | |
} | |
var node = raphael.circle(start.x, start.y, getWidth()); | |
node.attr({'fill': 'yellow', 'fill-opacity':0.3}); | |
return { | |
updateStart: function(x, y) { | |
start.x = x; | |
start.y = y; | |
redraw(); | |
return this; | |
}, | |
updateEnd: function(x, y) { | |
var v = { | |
x: Math.abs(x - start.x), | |
y: Math.abs(y - start.y) | |
}; | |
//Radius | |
end.w = Math.sqrt((Math.pow(v.x, 2) + Math.pow(v.y, 2))); | |
redraw(); | |
return this; | |
}, | |
clear: function() { | |
node.remove(); | |
} | |
}; | |
}; | |
function Rect(startX, startY, width, height, raphael) { | |
var start = { | |
x: startX, | |
y: startY, | |
w: width, | |
h: height | |
}; | |
var end = { | |
w: width, | |
h: height | |
}; | |
var getWidth = function() { | |
return end.w; | |
}; | |
var getHeight = function() { | |
return end.h; | |
}; | |
var redraw = function() { | |
node.attr({width: getWidth(), height: getHeight()}); | |
} | |
var node = raphael.rect(start.x, start.y, getWidth(), getHeight()); | |
node.attr({'fill': 'red', 'fill-opacity':0.3}); | |
return { | |
updateStart: function(x, y) { | |
start.x = x; | |
start.y = y; | |
redraw(); | |
return this; | |
}, | |
updateEnd: function(x, y) { | |
var v = { | |
x: Math.abs(x - start.x), | |
y: Math.abs(y - start.y) | |
}; | |
//Width | |
var width = Math.sqrt((Math.pow(v.x, 2) + Math.pow(v.y, 2))); | |
end.h = width; | |
end.w = width; | |
redraw(); | |
return this; | |
}, | |
clear: function() { | |
node.remove(); | |
} | |
}; | |
}; | |
$(function() { | |
var $paper = $("#paper"); | |
var $controls = $('.control'); | |
var paper = Raphael("paper", $("#container img").width(), $("#container img").height()); | |
var painter = {}; | |
var shapes = []; | |
painter.brush = function(){}; | |
var path_func = function(e){ | |
$controls.removeClass('active'); | |
$(this).addClass('active'); | |
painter.brush = function(e) { | |
var startx = e.clientX; | |
var starty = e.clientY; | |
var shape = paper.path("M"+startx+ " " +starty); | |
var saved_path = shape.attr('path'); | |
shapes.push(shape); | |
shape.attr('stroke-width', 2); | |
shape.attr('fill', '#dedede'); | |
shape.attr('fill-opacity', 0.4); | |
// Turn off the painter click event | |
painter.brush = function() {}; | |
// Clicking makes this point be saved | |
$paper.bind('click', function(e) { | |
//var proposed = " L"+startx+" "+starty; | |
var path = saved_path; | |
var added_path = []; | |
added_path.push("L"); | |
added_path.push(e.clientX); | |
added_path.push(e.clientY); | |
path.push(added_path); | |
shape.attr('path', path); | |
saved_path = path; | |
}); | |
//// We show where we'll drop the line... | |
$paper.mousemove( function(e) { | |
var proposed = saved_path.slice(0); | |
var added_path = []; | |
added_path.push("L"); | |
added_path.push(e.clientX); | |
added_path.push(e.clientY); | |
proposed.push(added_path); | |
shape.attr('path', proposed); | |
}); | |
// Finish up! | |
$paper.bind('dblclick', function(e) { | |
var path = saved_path; | |
var added_path = []; | |
added_path.push("Z"); | |
path.push(added_path); | |
shape.attr('path', path); | |
// Clean up our bindings. | |
$paper.unbind('click'); | |
$paper.unbind('dblclick'); | |
$paper.unbind('mousemove'); | |
e.preventDefault(); | |
}); | |
} | |
}; | |
$('.path').bind('click', path_func); | |
$('.rect').bind('click', function(e){ | |
$controls.removeClass('active'); | |
$(this).addClass('active'); | |
painter.brush = function(e) { | |
var startx = e.clientX; | |
var starty = e.clientY; | |
var shape = paper.rect(startx, starty, 1, 1); | |
shapes.push(shape); | |
shape.attr('stroke-width', 4); | |
$paper.bind('mouseup', function(e) { | |
$paper.unbind('mousemove'); | |
$paper.unbind('mouseup'); | |
}); | |
$paper.bind('mousemove', function(e) { | |
var x = [startx, e.clientX].sort(function(a, b) { return a - b; }); | |
var y = [starty, e.clientY].sort(function(a, b) { return a - b; }); | |
shape.attr('x', x[0]); | |
shape.attr('y', y[0]); | |
shape.attr('width', x[1] - x[0]); | |
shape.attr('height', y[1] - y[0]); | |
}); | |
}; | |
}); | |
$('.circ').bind('click', function(){ | |
$controls.removeClass('active'); | |
$(this).addClass('active'); | |
painter.brush = function(e) { | |
var startx = e.clientX; | |
var starty = e.clientY; | |
var shape = paper.circle(startx, starty, 0.1); | |
shape.attr('stroke-width', 4); | |
shapes.push(shape); | |
$paper.bind('mouseup', function(e) { | |
$paper.unbind('mousemove'); | |
$paper.unbind('mouseup'); | |
}); | |
$paper.bind('mousemove', function(e) { | |
var rad = Math.sqrt( | |
Math.pow(startx - e.clientX, 2) + | |
Math.pow(starty - e.clientY, 2) | |
); | |
shape.attr('r', rad); | |
var center = {cx: shape.attr('cx'), cy: shape.attr('cy')}; | |
center.cx = startx + (e.clientX - startx); | |
center.cy = starty + (e.clientY - starty); | |
shape.attr(center); | |
}); | |
}; | |
}); | |
$('.toggle').bind('click', function(e){ | |
$paper.toggle(); | |
}); | |
$('.clear').bind('click', function(e){ | |
while(shapes.length > 0) | |
{ | |
var shape = shapes.pop(); | |
shape.remove(); | |
} | |
//$(shapes).each(function(i){ | |
//this.remove(); | |
//}).toggle(); | |
}); | |
$paper.bind('mousedown', function(e){ | |
painter.brush.call(this, e); | |
}); | |
$('.export-json').bind('click', function(e) { | |
var json = RaphaelSerialize.json(paper); | |
$("#json_output").val(json) | |
}); | |
$('.export-svg').bind('click', function(e) { | |
$("#svg_output").val(paper.toSVG()); | |
}); | |
$(".import").bind('click', function(e) { | |
RaphaelSerialize.load_json(paper, $("#json_output").val()); | |
}); | |
}); | |
</script> | |
<style> | |
html, body { | |
overflow: hidden; | |
} | |
svg { | |
border:solid 1px #000; | |
} | |
.controls { | |
width: 514px; | |
margin-bottom: 10px; | |
} | |
.container { | |
position:relative; | |
width: 514px; | |
height: 350px; | |
overflow:hidden; | |
} | |
.control.active:before { | |
content: " X"; | |
} | |
.container img { | |
position: absolute; | |
top: 0; | |
left: 0; | |
z-index:-1; | |
} | |
.controls span{ | |
float: right; | |
padding-left: 10px; | |
} | |
textarea { | |
height: 300px; | |
width: 100%; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="controls"> | |
<a href="#circle" class="circ control" title="Click to Activate Circle Shape">Circle</a> | |
<a href="#square" class="rect control" title="Click to Activate Square Shape">Square</a> | |
<a href="#path" class="path control" title="Click to Activate Path Shape">Path</a> | |
<span class="toggle">Toggle</span> | |
<span class="clear">Clear</span> | |
</div> | |
<div style='float: left' class="container"> | |
<div id="paper"></div> | |
<img src="http://science.nationalgeographic.com/staticfiles/NGS/Shared/StaticFiles/Science/Images/Content/mackerel-sky-a4jag8-sw.jpg" /> | |
</div> | |
<p> | |
Circles and rectangles are self-explanitory. | |
<br/> | |
THe path - click the path tool, then start clicking about. Your last point should be doubleclicked - and | |
then it will finish the path. This will be handled better later - but for now - that's just the way it is implemented. | |
</p> | |
<div style='clear: both'> </div> | |
<fieldset> | |
<legend> JSON Output </legend> | |
<textarea id='json_output'> </textarea> | |
<input type='submit' class='export-json' value='Export SVG-JSON'></input> | |
<input type='submit' class='import' value='Import SVG-JSON'></input> | |
<textarea id='svg_output'> </textarea> | |
<input type='submit' class='export-svg' value='Export SVG'></input> | |
</fieldset> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment