Cropping works!

This commit is contained in:
2016-01-17 17:21:55 +08:00
parent 1f91c71657
commit ebb0d790ae
7 changed files with 377 additions and 224 deletions
Executable
+92
View File
@@ -0,0 +1,92 @@
{
// JSHint Default Configuration File (as on JSHint website)
// See http://jshint.com/docs/ for more details
"maxerr" : 50, // {int} Maximum error before stopping
// Enforcing
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
"camelcase" : false, // true: Identifiers must be in camelCase
"curly" : true, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc.
"immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
"latedef" : false, // true: Require variables/functions to be defined before being used
"newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()`
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
"noempty" : true, // true: Prohibit use of empty blocks
"nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters.
"nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment)
"plusplus" : false, // true: Prohibit use of `++` and `--`
"quotmark" : false, // Quotation mark consistency:
// false : do nothing (default)
// true : ensure whatever is used is consistent
// "single" : require single quotes
// "double" : require double quotes
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
"unused" : true, // Unused variables:
// true : all variables, last function parameter
// "vars" : all variables only
// "strict" : all variables, all function parameters
"strict" : true, // true: Requires all functions run in ES5 Strict Mode
"maxparams" : false, // {int} Max number of formal params allowed per function
"maxdepth" : false, // {int} Max depth of nested blocks (within functions)
"maxstatements" : false, // {int} Max number statements per function
"maxcomplexity" : false, // {int} Max cyclomatic complexity per function
"maxlen" : false, // {int} Max number of characters per line
"varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed.
// Relaxing
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
"boss" : false, // true: Tolerate assignments where comparisons would be expected
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"es5" : true, // true: Allow ES5 syntax (ex: getters and setters)
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
// (ex: `for each`, multiple try/catch, function expression…)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
"funcscope" : false, // true: Tolerate defining variables inside control statements
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
"iterator" : false, // true: Tolerate using the `__iterator__` property
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
"laxcomma" : false, // true: Tolerate comma-first style coding
"loopfunc" : false, // true: Tolerate functions being defined in loops
"multistr" : false, // true: Tolerate multi-line strings
"noyield" : false, // true: Tolerate generator functions with no yield statement in them.
"notypeof" : false, // true: Tolerate invalid typeof operator values
"proto" : false, // true: Tolerate using the `__proto__` property
"scripturl" : false, // true: Tolerate script-targeted URLs
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
"validthis" : false, // true: Tolerate using this in a non-constructor function
// Environments
"browser" : true, // Web Browser (window, document, etc)
"browserify" : false, // Browserify (node.js code in the browser)
"couch" : false, // CouchDB
"devel" : true, // Development/debugging (alert, confirm, etc)
"dojo" : false, // Dojo Toolkit
"jasmine" : false, // Jasmine
"jquery" : true, // jQuery
"mocha" : true, // Mocha
"mootools" : false, // MooTools
"node" : true, // Node.js
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
"phantom" : true, // PhantomJS
"prototypejs" : false, // Prototype and Scriptaculous
"qunit" : false, // QUnit
"rhino" : false, // Rhino
"shelljs" : false, // ShellJS
"typed" : false, // Globals for typed array constructions
"worker" : false, // Web Workers
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface
// Custom Globals
"globals" : {} // additional predefined global variables
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 30 KiB

+6 -2
View File
@@ -5,17 +5,21 @@
"main": "render.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node render.js"
"start": "node render.js svgCube.html"
},
"repository": "git@gitlab.com:wassname/svg2Cube.git",
"author": "wassname@wassname.org",
"private": true,
"license": "ISC",
"dependencies": {
"chromedriver": "^2.20.0",
"fs": "0.0.2",
"globby": "^4.0.0",
"jquery": "^2.2.0",
"mustache": "^2.2.1",
"path": "^0.12.7",
"snapsvg": "^0.4.0",
"webdriverio": "^3.4.0"
"system": "^1.0.4",
"webdriverio": "^3"
}
}
+207 -166
View File
@@ -3,9 +3,35 @@
* Frontend js file to generate an cube of svg's
*/
// http://cssdeck.com/labs/pure-css-animated-isometric-boxes
try {
var Snap = require('snapsvg');
} catch (e) {};
// polyfill for bind https://github.com/ariya/phantomjs/issues/11281
if (!Function.prototype.bind) {
console.log("Using polyfill of Function.prototype.bind");
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
var SvgCube = function (options) {
@@ -48,7 +74,7 @@ var SvgCube = function (options) {
frontUrl: '',
frontRot: 0,
frontShad: 0.0,
}
};
// add defaults, 2 levels deep
options = options || {};
@@ -62,14 +88,14 @@ var SvgCube = function (options) {
}
}
}
this.options = options
this.options = options;
// legacy compat
if (options.flatten!==undefined){
options.scaleY=1-options.flatten;
if (options.flatten !== undefined) {
options.scaleY = 1 - options.flatten;
}
if (options.angle!==undefined){
options.rotateX=options.angle;
if (options.angle !== undefined) {
options.rotateX = options.angle;
}
if (!options.drawOutline) {
@@ -78,161 +104,149 @@ var SvgCube = function (options) {
this.init();
}
};
SvgCube.prototype.init = function () {
this.rotateX = this.options.rotateX
this.rotateX = this.options.rotateX;
this.w = this.options.size; // input image width
this.h = this.options.size;
this.f = 1-this.options.scaleY
this.f = 1 - this.options.scaleY;
this.rot = this.rotateX * Math.PI / 180
this.padding = this.options.padding||0; // pading fraction
this.rot = this.rotateX * Math.PI / 180;
this.padding = this.options.padding || 0; // pading fraction
this.cw = this.w; // we will keep same width but change height
this.ch = this.h*5//(this.h + Math.sqrt(2)*this.h * Math.tan(this.rot)) - this.h / 2 * (this.f); //canvas height full
this.ch = this.h * 5; //(this.h + Math.sqrt(2)*this.h * Math.tan(this.rot)) - this.h / 2 * (this.f); //canvas height full
// create SVG element
var o = this.options;
//var style = document.createElement('style');
//this.paper.defs.appendChild(style)
var styleStr = ` <style id="projection">
.cube {
height: ${this.ch}px;
width: ${this.cw}px;
}
.face {
height: ${o.size}px;
width: ${o.size}px;
}
.cube {
position: absolute;
-webkit-transform: translate(${this.cw/2}px,${0*this.ch/2}px) perspective(-${o.perspective}px) rotateX(-${o.rotateX}deg) rotateY(${o.rotateY}deg) rotateZ(${o.rotateZ}deg) scaleX(${o.scaleX}) scaleY(${o.scaleY}) scaleZ(${o.scaleZ});
-webkit-transform-style: preserve-3d;
-webkit-transition: 0.25s;
}
.back {
transform: translateZ(-${o.size/2}px) rotateY(180deg);
}
.right {
transform: rotateY(-270deg) translateX(${o.size/2}px);
transform-origin: center right;
}
.left {
transform: rotateY(270deg) translateX(-${o.size/2}px);
transform-origin: center left;
}
.top {
transform: rotateX(-90deg) translateY(-${o.size/2}px);
transform-origin: top center;
}
.bottom {
transform: rotateX(90deg) translateY(${o.size/2}px);
transform-origin: bottom center;
}
.front {
transform: translateZ(${o.size/2}px);
}
/* custom orientation/rotation settings for each image */
.back>embed{
transform: rotate(${o.backRot}deg);
}
.front>embed{
transform: rotate(${o.frontRot}deg);
}
.right>embed{
transform: rotate(${o.rightRot}deg);
}
.left>embed{
transform: rotate(${o.leftRot}deg);
}
.top>embed{
transform: rotate(${180+o.topRot}deg);
}
.bottom>embed{
transform: rotate(${o.bottomRot}deg);
}
b{
position:absolute;
transition: all 1s linear;
}
/* outline */
.face {
box-sizing: border-box;
border: ${o.stroke['stroke-width']}px solid ${o.stroke.stroke};
}
/* shade in sides */
.wrap {
overflow: hidden;
width: ${o.size}px;
margin: 0 auto;
}
.tint {
position: absolute;
}
.tint:before {
content: "";
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.tint:hover:before { background: none; }
.tint.top:before { background: rgba(0,0,0, ${o.topShad});}
.tint.left:before { background: rgba(0,0,0, ${o.leftShad});}
.tint.right:before { background: rgba(0,0,0, ${o.rightShad});}
.tint.back:before { background: rgba(0,0,0, ${o.backShad}); }
.tint.front:before { background: rgba(0,0,0, ${o.frontShad}); }
.tint.bottom:before { background: rgba(0,0,0, ${o.bottomShad}); }
/* curved cube. Need something block seeing through, and also extra faces, 1 px in, so inside looks uniform color. Only work for one main color */
/*.face {
border-radius: ${o.borderRadius}px;
}
.mid {
background-color: #e3e2db; /* make stroke color TODO */
border-radius: 0px;
}*/
</style>`
// Now we generate some css to put everything in good positions
var transitions = 0;
var styleStr = '' +
'<style id="projection"> ' +
' .cube { ' +
' height: ' + this.ch + 'px; ' +
' width: ' + this.cw + 'px; ' +
' } ' +
' .face { ' +
' height: ' + o.size + 'px; ' +
' width: ' + o.size + 'px; ' +
' } ' +
' .cube { ' +
' position: absolute; ' +
' -webkit-transform: translate(' + this.cw / 2 + 'px,' + 0 * this.ch / 2 + 'px) perspective(-' + o.perspective + 'px) rotateX(-' + o.rotateX + 'deg) rotateY(' + o.rotateY + 'deg) rotateZ(' + o.rotateZ + 'deg) scaleX(' + o.scaleX + ') scaleY(' + o.scaleY + ') scaleZ(' + o.scaleZ + '); ' +
' -webkit-transform-style: preserve-3d; ' +
' -webkit-transition: '+transitions+'s; ' +
' } ' +
' .back { ' +
' transform: translateZ(-' + o.size / 2 + 'px) rotateY(180deg); ' +
' } ' +
' .right { ' +
' transform: rotateY(-270deg) translateX(' + o.size / 2 + 'px); ' +
' transform-origin: center right; ' +
' } ' +
' .left { ' +
' transform: rotateY(270deg) translateX(-' + o.size / 2 + 'px); ' +
' transform-origin: center left; ' +
' } ' +
' .top { ' +
' transform: rotateX(-90deg) translateY(-' + o.size / 2 + 'px); ' +
' transform-origin: top center; ' +
' } ' +
' .bottom { ' +
' transform: rotateX(90deg) translateY(' + o.size / 2 + 'px); ' +
' transform-origin: bottom center; ' +
' } ' +
' .front { ' +
' transform: translateZ(' + o.size / 2 + 'px); ' +
' } ' +
' /* custom orientation/rotation settings for each image */ ' +
' .back>embed{ ' +
' transform: rotate(' + o.backRot + 'deg); ' +
' } ' +
' .front>embed{ ' +
' transform: rotate(' + o.frontRot + 'deg); ' +
' } ' +
' .right>embed{ ' +
' transform: rotate(' + o.rightRot + 'deg); ' +
' } ' +
' .left>embed{ ' +
' transform: rotate(' + o.leftRot + 'deg); ' +
' } ' +
' .top>embed{ ' +
' transform: rotate(' + 90 + o.topRot + 'deg); ' +
' } ' +
' .bottom>embed{ ' +
' transform: rotate(' + o.bottomRot + 'deg); ' +
' } ' +
' b{ ' +
' position:absolute; ' +
' transition: all '+transitions+'s linear; ' +
' } ' +
' /* outline */ ' +
' .face { ' +
' box-sizing: border-box; ' +
' border: ' + o.stroke['stroke-width'] + 'px solid ' + o.stroke.stroke + '; ' +
' } ' +
' /* shade in sides */ ' +
' .wrap { ' +
' overflow: hidden; ' +
' width: ' + o.size + 'px; ' +
' margin: 0 auto; ' +
' } ' +
' .tint { ' +
' position: absolute; ' +
' } ' +
' .tint:before { ' +
' content: ""; ' +
' display: block; ' +
' position: absolute; ' +
' top: 0; ' +
' bottom: 0; ' +
' left: 0; ' +
' right: 0; ' +
' } ' +
' .tint:hover:before { background: none; } ' +
' .tint.top:before { background: rgba(0,0,0, ' + o.topShad + ');} ' +
' .tint.left:before { background: rgba(0,0,0, ' + o.leftShad + ');} ' +
' .tint.right:before { background: rgba(0,0,0, ' + o.rightShad + ');} ' +
' .tint.back:before { background: rgba(0,0,0, ' + o.backShad + '); } ' +
' .tint.front:before { background: rgba(0,0,0, ' + o.frontShad + '); } ' +
' .tint.bottom:before { background: rgba(0,0,0, ' + o.bottomShad + '); } ' +
' /* curved cube. Need something block seeing through, and also extra faces, 1 px in, so inside looks uniform color. Only work for one main color */ ' +
' /*.face { ' +
' border-radius: ' + o.borderRadius + 'px; ' +
' } ' +
' .mid { ' +
' background-color: #e3e2db; /* make stroke color TODO */ ' +
' border-radius: 0px; ' +
' }*/ ' +
' </style>';
if ($('body>.cube').length === 0) {
var cube = $('<div class="cube cube2"></div>')
var cube = $('<div class="cube cube2"></div>');
var imageFront = $(`<b width="256" height="256" class="front tint"><embed type="image/svg+xml" src="${o.frontUrl}" class="face"></embed></b>`)
var imageFront = $('<b width="256" height="256" class="front tint"><embed type="image/svg+xml" src="' + o.frontUrl + '" class="face"></embed></b>');
cube.append(imageFront);
var imageL = $(`<b width="256" height="256" class="left tint"><embed type="image/svg+xml" src="${o.leftUrl}" class="face"></embed></b>`)
var imageL = $('<b width="256" height="256" class="left tint"><embed type="image/svg+xml" src="' + o.leftUrl + '" class="face"></embed></b>');
cube.append(imageL);
var imageRight = $(`<b width="256" height="256" class="right tint"><embed type="image/svg+xml" src="${o.rightUrl}" class="face"></embed></b>`)
var imageRight = $('<b width="256" height="256" class="right tint"><embed type="image/svg+xml" src="' + o.rightUrl + '" class="face"></embed></b>');
cube.append(imageRight);
var imageTop = $(`<b width="256" height="256" class="top tint"><embed type="image/svg+xml" src="${o.topUrl}" class="face"></embed></b>`)
var imageTop = $('<b width="256" height="256" class="top tint"><embed type="image/svg+xml" src="' + o.topUrl + '" class="face"></embed></b>');
cube.append(imageTop);
var imageBack = $(`<b width="256" height="256" class="back tint"><embed type="image/svg+xml" src="${o.backUrl}" class="face"></embed></b>`)
var imageBack = $('<b width="256" height="256" class="back tint"><embed type="image/svg+xml" src="' + o.backUrl + '" class="face"></embed></b>');
cube.append(imageBack);
var imageBottom = $(`<b width="256" height="256" class="bottom tint"><embed type="image/svg+xml" src="${o.bottomUrl}" class="face"></embed></b>`)
var imageBottom = $('<b width="256" height="256" class="bottom tint"><embed type="image/svg+xml" src="' + o.bottomUrl + '" class="face"></embed></b>');
cube.append(imageBottom);
$('body').append(cube);
@@ -254,10 +268,10 @@ SvgCube.prototype.init = function () {
}
$('head>#projection').remove()
$('head>#projection').remove();
$('head').append($(styleStr));
}
};
/* draw cube from urls in options and outline according to options */
SvgCube.prototype.drawCube = function () {
@@ -281,38 +295,65 @@ SvgCube.prototype.drawCube = function () {
// class: 'face top',
// })
}
};
// svg2png
SvgCube.prototype.toPNG = function () {
try {
var dataUrl = svg2png(this.paper.node);
img = document.createElement("img");
document.body.appendChild(img)
img.src = dataUrl
img.id = "toPNG"
} catch (e) {
console.log(e)
};
}
/**
* Get the bounds of the images for a screenshot
* uses $.position to get min,max of each side of the image
* @return {[type]} [description]
*/
SvgCube.prototype.getBounds = function () {
var bounds = {};
$('.cube>b').each(function () {
var pos = this.getBoundingClientRect();
for (var dim in pos) {
if (dim in pos) {
// init
if (bounds[dim]===undefined) {
bounds[dim] = {
'max': pos[dim],
'min': pos[dim]
};
} else {
// get max and min
bounds[dim].max = Math.max(bounds[dim].max, pos[dim]);
bounds[dim].min = Math.min(bounds[dim].min, pos[dim]);
}
}
}
});
return bounds;
};
/**
* Get the bounds of the images for a screenshot
* uses $.position to get min,max of each side of the image
* @return {[type]} [description]
*/
SvgCube.prototype.getAllBounds = function () {
var bounds = {};
$('.cube>b').each(function () {
var pos = this.getBoundingClientRect();
for (var dim in pos) {
if (dim in pos) {
// init
if (bounds[dim]===undefined) {
bounds[dim] = [];
}
// get max and min
bounds[dim].push(pos[dim]);
}
}
});
return bounds;
};
// svg2png
SvgCube.prototype.toPNG2 = function () {
var dataUrl = svg2png(this.paper.node);
img = document.createElement("img");
document.body.appendChild(img)
img.src = dataUrl
img.id = "toPNG"
}
// svg2png
SvgCube.prototype.update = function () {
//this.paper.remove();
this.init();
this.drawCube();
}
try {
module.exports = SvgCube
} catch (e) {};
console.log(this.getBounds());
};
+58 -37
View File
@@ -5,45 +5,40 @@
var webdriverio = require('webdriverio');
var path = require('path');
var fs = require('fs');
var system = require('system')
// var fs = require('fs');
// var system = require('system')
var globby = require('globby');
var gm = require('gm');
// start chromedriver for selenium
var chromedriver = require('chromedriver');
chromedriver.start();
var options = {
host: "localhost",
port: 9515,
desiredCapabilities: {
browserName: 'chrome'
browserName: 'chrome',
chromeOptions: {
binary: '/usr/bin/chromium'
}
}
};
var client = webdriverio.remote(options);
/** TODO
* Need to look at clien bounding box for all parts of cube, then get minLeft, maxRight etc, then crop the screenshot there
*
**/
// get config
var config = {
debug: false
}
// get inputs
var input = process.argv[2];
console.log('input:', input);
/**
* Screenshot for debug and notification of screenshots
*/
var screenHandler = function (err, screenshot, response) {
if (config.debug) {
console.log({
err, screenshot, response
});
} else if (err) {
console.log('saveScreenshot', err);
}
if (process.argv.length < 3 || process.argv.length > 3) {
console.log('Command: ', process.argv);
console.log('Usage: rasterize.js filename');
return;
} else {
var input = process.argv[2];
console.log('input:', input);
}
var bounds;
globby(input).then(inputs => {
console.log('glob(', input, ') ->', inputs);
@@ -57,19 +52,45 @@ globby(input).then(inputs => {
var url = 'file://' + path.join(process.cwd(), file);
var outfile = address.replace(ext, '.png');
// create panel elements temporarily
console.log('Converting ', file, url, '->', outfile);
webdriverio
.remote(options)
.init()
client.init()
.url(url)
.getTitle().then(title => {
console.log('Title is: ' + title);
.waitForVisible('.front', 5000)
// get image boundries from html
.execute(function () {
return $('#dimensions').text();
})
.then(text => {
try {
bounds = JSON.parse(text.value);
} catch (e) {
console.warn(text);
}
})
// take a screen shot
.screenshot()
// crop using graphics magic
.then(res => {
var imgBuffer = new Buffer(res.value, 'base64');
/** Crop it using graphicks magic */
gm(imgBuffer)
.crop(
bounds.right.max - bounds.left.min,
bounds.bottom.max - bounds.top.min,
bounds.left.min,
bounds.top.min
)
.write(outfile, function (err) {
if (!err) console.log('done');
});
})
.waitForVisible('.front', 1000) //.then(callback);
.saveScreenshot(
outfile, screenHandler
)
.end();
}
}, this)
chromedriver.stop();
+13 -18
View File
@@ -3,27 +3,22 @@
<head>
<meta charset="utf-8">
<title>Snap.js isometric SVG</title>
<title>svg2Cube</title>
</head>
<body>
<div style="display: none;" id="dimensions">
</input>
</body>
<script type="text/javascript" src="../../node_modules/snapsvg/dist/snap.svg.js"></script>
<script src="projectSVG.js"></script>
<script type="text/javascript" src="node_modules/snapsvg/dist/snap.svg.js"></script>
<script type="text/javascript" src="node_modules/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="projectSVG.js"></script>
<script>
var cube1
// redraw cube oninput
var update = function() {
if (cube1) {
cube1.update()
}
}
window.onload = function() {
cube1 = new SvgCube({
rotateX: 45,
topUrl: 'inputs/top.svg',
@@ -37,15 +32,15 @@
"stroke": 'black', // stroke color for outline
"stroke-width": 0, // outline width
},
size: 256,
size: 444,
});
cube1.drawCube()
}
var text = cube1.getBounds();
// Get dimensions of each pane and make it accesable to webdriver
$('#dimensions').attr('value', 1)
$('#dimensions').text(JSON.stringify(text))
var module
if (module) {
module.exports = SvgCube
}
</script>
+1 -1
View File
@@ -16,7 +16,7 @@
</body>
<script type="text/javascript" src="../../node_modules/snapsvg/dist/snap.svg.js"></script>
<script type="text/javascript" src="node_modules/snapsvg/dist/snap.svg.js"></script>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<!-- <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js"></script> -->