Skip to content

Instantly share code, notes, and snippets.

@caryfitzhugh
Created March 11, 2012 04:46
Show Gist options
  • Save caryfitzhugh/2015072 to your computer and use it in GitHub Desktop.
Save caryfitzhugh/2015072 to your computer and use it in GitHub Desktop.
Simple Proof Of Concept For SVG image annotation in browser
<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