Skip to content

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
rule-of-thirds bookmarklet

rule-of-thirds bookmarklet that overlays a rule-of-thirds style grid over all images on a page.

To get the bookmarklet, visit http://bl.ocks.org/4331769

Bookmarklet Development Notes

Do the following to be able to use the "RuleOfThirds-localhost" development version of the bookmarklet:

# checkout the gist
git clone https://gist.github.com/4331769.git ~/code/rule-of-thirds
# start a mini webserver, to enable http://localhost:9090/rule-of-thirds.bookmarklet.js
cd ~/code/rule-of-thirds
python -m SimpleHTTPServer 9090 

When you're done, kill the server via CTRL-C.

TODO

Dev Links

CSS:

SVG:

Misc:

Bookmarklet:

Canvas:

jQuery position:

Require-JS:

YepNope-JS:

Pages to Test:

Photography:

<html>
<title>rule-of-thirds.js bookmarklet</title>
<head>
<style type="text/css">
.button-wrapper {
margin: 2em 0;
}
.inline-button,
.button {
/* base64 encoder: http://www.greywyvern.com/code/php/binary2base64 */
/* Source image: http://tiny.cc/public/images/default-favicon.ico */
background-image: url(data:image/vnd.microsoft.icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAA+/v8APn3+QCeTWQAcmh+APj5+gBVKksA9/X3APb29gD08vMA9/j5APT09gD7/PwA8/HyAPPz9QD6+/sA9fP1AGcwWQDy8vQAOB07APn6+gD49vcA2eryAPX19gCckqgA8/HzAPb19gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABMTExMTExMTExMTExMAAAAGDQ0NDQ0NDQ0NDQ0GAAAAERkZGRkZGRkZGRkZEQAAAAMJCQkJCQkJCQkJCQMAAAADEg4SDhISEg4ODg4DAAAAAxAQEBAQEBAQEBAQAwAAAAMaGhoaGhoLFxcXGgMAAAADBwcHBwcHBwcHBwcDAAAAAxUVFRUVFRUVFRUVAwAAAAMCAgICAgICAgICAgMAAAAECgUKBQoKCgUWFhYEAAAABAUUBRQFBQUYGBgYBAAAAAQUDxQPFBQUGAgIFhgAAAAEDwEPAQ8PDxgIFhgAAAAABAEMAQwBAQEYFhgAAAAAABgYGBgYGBgYGBgAAAAAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACABwAAgA8AAIAfAAA=);
background-repeat: no-repeat;
background-color: #65a9d7;
background-position: 10px center;
padding: 10px 20px 10px 32px;
border-radius: 8px;
color: white;
font-size: 18px;
font-family: 'Lucida Grande', Helvetica, Arial, Sans-Serif;
text-decoration: none;
}
.inline-button {
display: inline;
background-color: transparent;
padding-right: 3px;
padding-left: 28px;
font-size: 1em;
background-size: 13px 13px;
}
.inline-button a {
text-decoration: none;
color: #65a9d7;
}
body {
padding: 20px;
font-family: Helvetica, Arial, sans-serif;
font-size: .9em;
color: #444;
}
p {
line-height: 1.5em;
margin: .5em 0 1em 0;
}
ul {
line-height: 2em;
list-style-type: none;
padding-left: 1em;
}
</style>
</head>
<body>
<section data-markdown>
To install the bookmarklet, [drag it to your bookmark toolbar](http://dl-web.dropbox.com/u/29440342/screenshots/HEILHU-2013.1.2-18.52.png), after which you can run it on any webpage with interesting images.
<div class="button-wrapper"><a class="button" href="javascript:(function(){var s = document.createElement('script');s.src = 'https://gist.github.com/raw/4331769/cdffe1f17ccda7eeadd1a732753ce067aba4147f/rule-of-thirds.bookmarklet.js';document.body.appendChild(s);})();">RuleOfThirds</a></div>
The above is the January 2, 2012 release of rule-of-thirds.bookmarklet.js. For
development purposes, I've also created the following variants of the
bookmarklet. Since the dated release is more stable and secure, only use these
if you know what you're doing.
* <div class="inline-button"><a href="javascript:(function(){var s = document.createElement('script');s.src='https://gist.github.com/raw/4331769/rule-of-thirds.bookmarklet.js';document.body.appendChild(s);})();">RuleOfThirds-HEAD</a></div>
https://gist.github.com/raw/4331769/rule-of-thirds.bookmarklet.js
* <div class="inline-button"><a href="javascript:(function(){var s = document.createElement('script');s.src='http://localhost:9090/rule-of-thirds.bookmarklet.js?time'+(new Date().getTime());document.body.appendChild(s);})();">RuleOfThirds-localhost</a></div>
http://localhost:9090/rule-of-thirds.bookmarklet.js?time=12345678
Feel free to try out the bookmarklet with some famous photographs:
<a data-pin-do="embedBoard" href="http://pinterest.com/dergachev/beyond-rule-of-thirds-lecture/"></a>
</section>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
<script>
// adapted from http://blog.harakys.com/blog/2012/02/21/embed-markdown-into-your-html/
// see also https://gist.github.com/1343518
[].forEach.call( document.querySelectorAll('[data-markdown]'), function fn(elem){
var html = (new Showdown.converter()).makeHtml(elem.innerHTML);
elem.innerHTML = html;
});
</script>
<script type="text/javascript" src="http://assets.pinterest.com/js/pinit.js" ></script>
</body>
</html>
/*!
* jQuery UI Position @VERSION
* http://jqueryui.com
*
* Copyright 2012 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/position/
*/
(function( $, undefined ) {
$.ui = $.ui || {};
var cachedScrollbarWidth,
max = Math.max,
abs = Math.abs,
round = Math.round,
rhorizontal = /left|center|right/,
rvertical = /top|center|bottom/,
roffset = /[\+\-]\d+%?/,
rposition = /^\w+/,
rpercent = /%$/,
_position = $.fn.position;
function getOffsets( offsets, width, height ) {
return [
parseInt( offsets[ 0 ], 10 ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
parseInt( offsets[ 1 ], 10 ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
];
}
function parseCss( element, property ) {
return parseInt( $.css( element, property ), 10 ) || 0;
}
function getDimensions( elem ) {
var raw = elem[0];
if ( raw.nodeType === 9 ) {
return {
width: elem.width(),
height: elem.height(),
offset: { top: 0, left: 0 }
};
}
if ( $.isWindow( raw ) ) {
return {
width: elem.width(),
height: elem.height(),
offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
};
}
if ( raw.preventDefault ) {
return {
width: 0,
height: 0,
offset: { top: raw.pageY, left: raw.pageX }
};
}
return {
width: elem.outerWidth(),
height: elem.outerHeight(),
offset: elem.offset()
};
}
$.position = {
scrollbarWidth: function() {
if ( cachedScrollbarWidth !== undefined ) {
return cachedScrollbarWidth;
}
var w1, w2,
div = $( "<div style='display:block;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>" ),
innerDiv = div.children()[0];
$( "body" ).append( div );
w1 = innerDiv.offsetWidth;
div.css( "overflow", "scroll" );
w2 = innerDiv.offsetWidth;
if ( w1 === w2 ) {
w2 = div[0].clientWidth;
}
div.remove();
return (cachedScrollbarWidth = w1 - w2);
},
getScrollInfo: function( within ) {
var overflowX = within.isWindow ? "" : within.element.css( "overflow-x" ),
overflowY = within.isWindow ? "" : within.element.css( "overflow-y" ),
hasOverflowX = overflowX === "scroll" ||
( overflowX === "auto" && within.width < within.element[0].scrollWidth ),
hasOverflowY = overflowY === "scroll" ||
( overflowY === "auto" && within.height < within.element[0].scrollHeight );
return {
width: hasOverflowX ? $.position.scrollbarWidth() : 0,
height: hasOverflowY ? $.position.scrollbarWidth() : 0
};
},
getWithinInfo: function( element ) {
var withinElement = $( element || window ),
isWindow = $.isWindow( withinElement[0] );
return {
element: withinElement,
isWindow: isWindow,
offset: withinElement.offset() || { left: 0, top: 0 },
scrollLeft: withinElement.scrollLeft(),
scrollTop: withinElement.scrollTop(),
width: isWindow ? withinElement.width() : withinElement.outerWidth(),
height: isWindow ? withinElement.height() : withinElement.outerHeight()
};
}
};
$.fn.position = function( options ) {
if ( !options || !options.of ) {
return _position.apply( this, arguments );
}
// make a copy, we don't want to modify arguments
options = $.extend( {}, options );
var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
target = $( options.of ),
within = $.position.getWithinInfo( options.within ),
scrollInfo = $.position.getScrollInfo( within ),
collision = ( options.collision || "flip" ).split( " " ),
offsets = {};
dimensions = getDimensions( target );
if ( target[0].preventDefault ) {
// force left top to allow flipping
options.at = "left top";
}
targetWidth = dimensions.width;
targetHeight = dimensions.height;
targetOffset = dimensions.offset;
// clone to reuse original targetOffset later
basePosition = $.extend( {}, targetOffset );
// force my and at to have valid horizontal and vertical positions
// if a value is missing or invalid, it will be converted to center
$.each( [ "my", "at" ], function() {
var pos = ( options[ this ] || "" ).split( " " ),
horizontalOffset,
verticalOffset;
if ( pos.length === 1) {
pos = rhorizontal.test( pos[ 0 ] ) ?
pos.concat( [ "center" ] ) :
rvertical.test( pos[ 0 ] ) ?
[ "center" ].concat( pos ) :
[ "center", "center" ];
}
pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
// calculate offsets
horizontalOffset = roffset.exec( pos[ 0 ] );
verticalOffset = roffset.exec( pos[ 1 ] );
offsets[ this ] = [
horizontalOffset ? horizontalOffset[ 0 ] : 0,
verticalOffset ? verticalOffset[ 0 ] : 0
];
// reduce to just the positions without the offsets
options[ this ] = [
rposition.exec( pos[ 0 ] )[ 0 ],
rposition.exec( pos[ 1 ] )[ 0 ]
];
});
// normalize collision option
if ( collision.length === 1 ) {
collision[ 1 ] = collision[ 0 ];
}
if ( options.at[ 0 ] === "right" ) {
basePosition.left += targetWidth;
} else if ( options.at[ 0 ] === "center" ) {
basePosition.left += targetWidth / 2;
}
if ( options.at[ 1 ] === "bottom" ) {
basePosition.top += targetHeight;
} else if ( options.at[ 1 ] === "center" ) {
basePosition.top += targetHeight / 2;
}
atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
basePosition.left += atOffset[ 0 ];
basePosition.top += atOffset[ 1 ];
return this.each(function() {
var collisionPosition, using,
elem = $( this ),
elemWidth = elem.outerWidth(),
elemHeight = elem.outerHeight(),
marginLeft = parseCss( this, "marginLeft" ),
marginTop = parseCss( this, "marginTop" ),
collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width,
collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height,
position = $.extend( {}, basePosition ),
myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
if ( options.my[ 0 ] === "right" ) {
position.left -= elemWidth;
} else if ( options.my[ 0 ] === "center" ) {
position.left -= elemWidth / 2;
}
if ( options.my[ 1 ] === "bottom" ) {
position.top -= elemHeight;
} else if ( options.my[ 1 ] === "center" ) {
position.top -= elemHeight / 2;
}
position.left += myOffset[ 0 ];
position.top += myOffset[ 1 ];
// if the browser doesn't support fractions, then round for consistent results
if ( !$.support.offsetFractions ) {
position.left = round( position.left );
position.top = round( position.top );
}
collisionPosition = {
marginLeft: marginLeft,
marginTop: marginTop
};
$.each( [ "left", "top" ], function( i, dir ) {
if ( $.ui.position[ collision[ i ] ] ) {
$.ui.position[ collision[ i ] ][ dir ]( position, {
targetWidth: targetWidth,
targetHeight: targetHeight,
elemWidth: elemWidth,
elemHeight: elemHeight,
collisionPosition: collisionPosition,
collisionWidth: collisionWidth,
collisionHeight: collisionHeight,
offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
my: options.my,
at: options.at,
within: within,
elem : elem
});
}
});
if ( options.using ) {
// adds feedback as second argument to using callback, if present
using = function( props ) {
var left = targetOffset.left - position.left,
right = left + targetWidth - elemWidth,
top = targetOffset.top - position.top,
bottom = top + targetHeight - elemHeight,
feedback = {
target: {
element: target,
left: targetOffset.left,
top: targetOffset.top,
width: targetWidth,
height: targetHeight
},
element: {
element: elem,
left: position.left,
top: position.top,
width: elemWidth,
height: elemHeight
},
horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
};
if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
feedback.horizontal = "center";
}
if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
feedback.vertical = "middle";
}
if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
feedback.important = "horizontal";
} else {
feedback.important = "vertical";
}
options.using.call( this, props, feedback );
};
}
elem.offset( $.extend( position, { using: using } ) );
});
};
$.ui.position = {
fit: {
left: function( position, data ) {
var within = data.within,
withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
outerWidth = within.width,
collisionPosLeft = position.left - data.collisionPosition.marginLeft,
overLeft = withinOffset - collisionPosLeft,
overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
newOverRight;
// element is wider than within
if ( data.collisionWidth > outerWidth ) {
// element is initially over the left side of within
if ( overLeft > 0 && overRight <= 0 ) {
newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset;
position.left += overLeft - newOverRight;
// element is initially over right side of within
} else if ( overRight > 0 && overLeft <= 0 ) {
position.left = withinOffset;
// element is initially over both left and right sides of within
} else {
if ( overLeft > overRight ) {
position.left = withinOffset + outerWidth - data.collisionWidth;
} else {
position.left = withinOffset;
}
}
// too far left -> align with left edge
} else if ( overLeft > 0 ) {
position.left += overLeft;
// too far right -> align with right edge
} else if ( overRight > 0 ) {
position.left -= overRight;
// adjust based on position and margin
} else {
position.left = max( position.left - collisionPosLeft, position.left );
}
},
top: function( position, data ) {
var within = data.within,
withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
outerHeight = data.within.height,
collisionPosTop = position.top - data.collisionPosition.marginTop,
overTop = withinOffset - collisionPosTop,
overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
newOverBottom;
// element is taller than within
if ( data.collisionHeight > outerHeight ) {
// element is initially over the top of within
if ( overTop > 0 && overBottom <= 0 ) {
newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset;
position.top += overTop - newOverBottom;
// element is initially over bottom of within
} else if ( overBottom > 0 && overTop <= 0 ) {
position.top = withinOffset;
// element is initially over both top and bottom of within
} else {
if ( overTop > overBottom ) {
position.top = withinOffset + outerHeight - data.collisionHeight;
} else {
position.top = withinOffset;
}
}
// too far up -> align with top
} else if ( overTop > 0 ) {
position.top += overTop;
// too far down -> align with bottom edge
} else if ( overBottom > 0 ) {
position.top -= overBottom;
// adjust based on position and margin
} else {
position.top = max( position.top - collisionPosTop, position.top );
}
}
},
flip: {
left: function( position, data ) {
var within = data.within,
withinOffset = within.offset.left + within.scrollLeft,
outerWidth = within.width,
offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
collisionPosLeft = position.left - data.collisionPosition.marginLeft,
overLeft = collisionPosLeft - offsetLeft,
overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
myOffset = data.my[ 0 ] === "left" ?
-data.elemWidth :
data.my[ 0 ] === "right" ?
data.elemWidth :
0,
atOffset = data.at[ 0 ] === "left" ?
data.targetWidth :
data.at[ 0 ] === "right" ?
-data.targetWidth :
0,
offset = -2 * data.offset[ 0 ],
newOverRight,
newOverLeft;
if ( overLeft < 0 ) {
newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset;
if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
position.left += myOffset + atOffset + offset;
}
}
else if ( overRight > 0 ) {
newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft;
if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
position.left += myOffset + atOffset + offset;
}
}
},
top: function( position, data ) {
var within = data.within,
withinOffset = within.offset.top + within.scrollTop,
outerHeight = within.height,
offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
collisionPosTop = position.top - data.collisionPosition.marginTop,
overTop = collisionPosTop - offsetTop,
overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
top = data.my[ 1 ] === "top",
myOffset = top ?
-data.elemHeight :
data.my[ 1 ] === "bottom" ?
data.elemHeight :
0,
atOffset = data.at[ 1 ] === "top" ?
data.targetHeight :
data.at[ 1 ] === "bottom" ?
-data.targetHeight :
0,
offset = -2 * data.offset[ 1 ],
newOverTop,
newOverBottom;
if ( overTop < 0 ) {
newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset;
if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) {
position.top += myOffset + atOffset + offset;
}
}
else if ( overBottom > 0 ) {
newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop;
if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) {
position.top += myOffset + atOffset + offset;
}
}
}
},
flipfit: {
left: function() {
$.ui.position.flip.left.apply( this, arguments );
$.ui.position.fit.left.apply( this, arguments );
},
top: function() {
$.ui.position.flip.top.apply( this, arguments );
$.ui.position.fit.top.apply( this, arguments );
}
}
};
// fraction support test
(function () {
var testElement, testElementParent, testElementStyle, offsetLeft, i,
body = document.getElementsByTagName( "body" )[ 0 ],
div = document.createElement( "div" );
//Create a "fake body" for testing based on method used in jQuery.support
testElement = document.createElement( body ? "div" : "body" );
testElementStyle = {
visibility: "hidden",
width: 0,
height: 0,
border: 0,
margin: 0,
background: "none"
};
if ( body ) {
$.extend( testElementStyle, {
position: "absolute",
left: "-1000px",
top: "-1000px"
});
}
for ( i in testElementStyle ) {
testElement.style[ i ] = testElementStyle[ i ];
}
testElement.appendChild( div );
testElementParent = body || document.documentElement;
testElementParent.insertBefore( testElement, testElementParent.firstChild );
div.style.cssText = "position: absolute; left: 10.7432222px;";
offsetLeft = $( div ).offset().left;
$.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11;
testElement.innerHTML = "";
testElementParent.removeChild( testElement );
})();
}( jQuery ) );
function initMyBookmarklet() {
(window.myBookmarklet = function() {
// blow away customizations to underscore template parsing (eg 500px)
_.templateSettings.interpolate = /<%=([\s\S]+?)%>/g
_.templateSettings.evaluate = /<%([\s\S]+?)%>/g
// TODO: simplify state handling on multiple-clicks of bookmarklet
RuleOfThirds.stateMachine.advance();
})();
}
// inspired by http://lamehacks.net/blog/implementing-a-state-machine-in-javascript/
RuleOfThirds = window.RuleOfThirds || {}
RuleOfThirds.stateMachine = RuleOfThirds.stateMachine || new (function (){
states = [
{ name:'OFF',
onEnter: function() {
removeOverlays();
}
},
{ name:'GRID_THIRDS',
onEnter: function() {
removeOverlays(); // unnecessary due to order of state advance()
createOverlays(function(width, height) { return getSVGOverlayThirdsGrid(width, height, "GRID_THIRDS")} );
}
},
{ name:'GRID_PHI',
onEnter: function() {
removeOverlays(); // unnecessary due to order of state advance()
createOverlays(function(width, height) { return getSVGOverlayThirdsGrid(width, height, "GRID_PHI")} );
}
},
{ name:'TRIANGLES',
onEnter: function() {
removeOverlays(); // unnecessary due to order of state advance()
createOverlays(function(width, height) { return getSVGOverlayThirdsGrid(width, height, "TRIANGLES")} );
}
},
{ name:'TRIANGLES_90',
onEnter: function() {
removeOverlays(); // unnecessary due to order of state advance()
createOverlays(function(width, height) { return getSVGOverlayThirdsGrid(width, height, "TRIANGLES_90")} );
}
},
{ name:'SPIRAL_0',
onEnter: function() {
removeOverlays();
createOverlays(function(width,height,degrees) { return getSVGOverlaySpiral(width, height, "SPIRAL_0"); });
}
},
{ name:'SPIRAL_90',
onEnter: function() {
removeOverlays();
createOverlays(function(width,height,degrees) { return getSVGOverlaySpiral(width, height, "SPIRAL_90"); });
}
},
{ name:'SPIRAL_180',
onEnter: function() {
removeOverlays();
createOverlays(function(width,height,degrees) { return getSVGOverlaySpiral(width, height, "SPIRAL_180"); });
}
},
{ name:'SPIRAL_270',
onEnter: function() {
removeOverlays();
createOverlays(function(width,height,degrees) { return getSVGOverlaySpiral(width, height, "SPIRAL_270"); });
}
}
];
this.currentState = states[0];
this.advance = function(){
this.currentState = states[(states.indexOf(this.currentState) + 1) % states.length];
this.currentState.onEnter();
}
})();
/*
* Removes RuleOfThirds overlays.
*/
function removeOverlays() {
if (jQuery('.rule-of-thirds').remove().length) {
jQuery('.rule-of-thirds-wrapper > img').unwrap();
}
}
function createOverlays(overlayFactory) {
jQuery('img').each(function(){
window.el = jQuery(this);
var el = jQuery(this),
w = el.width(),
h = el.height(),
src = el.attr('src') || "",
useWrapper = (el.siblings().length == 0) && (el.offsetParent().width() == w)
//skip small or invisible images
if (!el.is(':visible') || w < 100 || h < 100) {
return;
}
window.overlay = jQuery('<div />')
.addClass('rule-of-thirds')
.css( {
'position': 'absolute',
'pointer-events':'none',
'width': w,
'height': h,
})
.attr('rel',src || "" ) // "undefined" chokes .attr; http://bit.ly/134P0C9
.append(overlayFactory(w,h))
// .append(getCanvasOverlay(w,h))
if (useWrapper) { // should be fairly harmless; fixes pinterest zoom
var wrapper = jQuery('<div />')
.addClass('rule-of-thirds-wrapper')
.css({
'position':'relative',
'display' : 'inline-block', //fixes super-long pins eg http://pinterest.com/pin/552113235537700759/
'text-align' : 'center',
})
.appendTo(el.parent())
.append(el)
.append(overlay);
overlay.css({
'width' : '100%',
'height' : '100%',
'top': '0px',
'left': '0px',
});
} else {
overlay.insertAfter(el);
//XXX: why does overlay.position need to be run twice on http://bl.ocks.org/4331769
// console.log("img offset", el.position());
// console.log("overlay offset", overlay.position());
overlay.position({"of": el, at: "center"});
// console.log("overlay offset", overlay.position());
overlay.position({"of": el, at: "center"});
// console.log("overlay offset", overlay.position());
}
});
}
//from http://en.wikipedia.org/wiki/File:Fibonacci_spiral.svg
// according to lightroom this is the vertical orientation.
// for horizontal images, need to rotate 90 degrees CW
function getSVGOverlaySpiralTemplate() {
var svg = '<svg\
xmlns:dc="http://purl.org/dc/elements/1.1/"\
xmlns:xlink="http://www.w3.org/1999/xlink"\
xmlns:cc="http://web.resource.org/cc/"\
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\
xmlns:svg="http://www.w3.org/2000/svg"\
xmlns="http://www.w3.org/2000/svg"\
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"\
id="svg2"\
sodipodi:version="0.32"\
inkscape:version="0.44"\
version="1.0"\
viewbox="<%= viewbox %>"\
preserveAspectRatio="none"\
sodipodi:docname="Fibonacci_spiral.svg">\
<defs>\
<g id="spiral" >\
<path id="path1873" sodipodi:type="arc"\
vector-effect="non-scaling-stroke"\
sodipodi:cx="337.94031" sodipodi:cy="2354.0146" sodipodi:rx="301.58487" sodipodi:ry="281.75464"\
d="M 36.355438,2354.0146 A 301.58487,281.75464 0 0 1 337.94031,2072.26"\
sodipodi:start="3.1415927" sodipodi:end="4.712389" sodipodi:open="true"\
transform="matrix(2.022351,0,0,2.164687,-73.02346,-4485.255)"\
vector-effect="non-scaling-stroke" />\
<path id="path1875" sodipodi:type="arc"\
transform="matrix(-1.247009,0,0,1.334775,1031.657,-2765.459)"\
sodipodi:open="true" sodipodi:end="4.712389" sodipodi:start="3.1415927"\
d="M 36.355438,2354.0146 A 301.58487,281.75464 0 0 1 337.94031,2072.26"\
vector-effect="non-scaling-stroke"\
sodipodi:ry="281.75464" sodipodi:rx="301.58487" sodipodi:cy="2354.0146" sodipodi:cx="337.94031"\
vector-effect="non-scaling-stroke" />\
<path id="path2762" sodipodi:type="arc"\
sodipodi:cx="337.94031" sodipodi:cy="2354.0146" sodipodi:rx="301.58487" sodipodi:ry="281.75464"\
d="M 36.355438,2354.0146 A 301.58487,281.75464 0 0 1 337.94031,2072.26"\
sodipodi:start="3.1415927" sodipodi:end="4.712389" sodipodi:open="true"\
transform="matrix(0,-0.770297,-0.824511,0,2694.923,636.9008)"\
vector-effect="non-scaling-stroke" />\
<path id="path2764" sodipodi:type="arc"\
transform="matrix(0.473354,0,0,-0.50667,594.0855,1658.848)"\
sodipodi:open="true" sodipodi:end="4.712389" sodipodi:start="3.1415927"\
d="M 36.355438,2354.0146 A 301.58487,281.75464 0 0 1 337.94031,2072.26"\
sodipodi:ry="281.75464" sodipodi:rx="301.58487" sodipodi:cy="2354.0146" sodipodi:cx="337.94031"\
vector-effect="non-scaling-stroke" />\
<path id="path2766" sodipodi:type="arc"\
sodipodi:cx="337.94031" sodipodi:cy="2354.0146" sodipodi:rx="301.58487" sodipodi:ry="281.75464"\
d="M 36.355438,2354.0146 A 301.58487,281.75464 0 0 1 337.94031,2072.26"\
sodipodi:start="3.1415927" sodipodi:end="4.712389" sodipodi:open="true"\
transform="matrix(0.290281,0,0,0.310712,600.7412,-265.2473)"\
vector-effect="non-scaling-stroke" />\
<path id="path2768" sodipodi:type="arc"\
transform="matrix(-0.181793,0,0,0.194589,760.2218,-24.60904)"\
sodipodi:open="true" sodipodi:end="4.712389" sodipodi:start="3.1415927"\
d="M 36.355438,2354.0146 A 301.58487,281.75464 0 0 1 337.94031,2072.26"\
sodipodi:ry="281.75464" sodipodi:rx="301.58487" sodipodi:cy="2354.0146" sodipodi:cx="337.94031"\
vector-effect="non-scaling-stroke" />\
<path id="path2770" sodipodi:type="arc"\
sodipodi:cx="337.94031" sodipodi:cy="2354.0146" sodipodi:rx="301.58487" sodipodi:ry="281.75464"\
d="M 36.355438,2354.0146 A 301.58487,281.75464 0 0 1 337.94031,2072.26"\
sodipodi:start="3.1415927" sodipodi:end="4.712389" sodipodi:open="true"\
transform="matrix(-0.109629,0,0,-0.117346,757.5983,709.0538)"\
vector-effect="non-scaling-stroke" />\
<path id="path2772" sodipodi:type="arc"\
transform="matrix(6.725143e-2,0,0,-7.198541e-2,697.9484,615.0548)"\
sodipodi:open="true" sodipodi:end="4.712389" sodipodi:start="3.1415927"\
d="M 36.355438,2354.0146 A 301.58487,281.75464 0 0 1 337.94031,2072.26"\
sodipodi:ry="281.75464" sodipodi:rx="301.58487" sodipodi:cy="2354.0146" sodipodi:cx="337.94031"\
vector-effect="non-scaling-stroke"\ />\
</g>\
<g id="rectangles">\
<path d="M 754.24901,466.52217 L 610.30258,466.52217" id="rect2800" sodipodi:nodetypes="cc" vector-effect="non-scaling-stroke" />\
<path d="M 754.24901,377.48564 L 754.24901,610.4945" id="rect2802" sodipodi:nodetypes="cc" vector-effect="non-scaling-stroke" />\
<path d="M 610.30252,377.48564 L 987.15967,377.48564" id="rect2804" sodipodi:nodetypes="cc" vector-effect="non-scaling-stroke" />\
<path d="M 610.30252,610.49292 L 610.30252,0.50000006" id="rect2806" sodipodi:nodetypes="cc" vector-effect="non-scaling-stroke" />\
</g>\
</defs>\
<% _.each(transform, function(t) { %>\
<g id="drawing" transform="<%= t %>">\
<% }); %>\
<use xlink:href="#rectangles" style="vector-effect:none; stroke: grey; stroke-width: 4;"/>\
<use xlink:href="#rectangles" style="vector-effect:none; stroke: white; stroke-width: 2;"/>\
<use xlink:href="#spiral" style="fill:none; stroke: grey; stroke-width: 4;" />\
<use xlink:href="#spiral" style="fill:none; stroke: white; stroke-width: 2;" />\
<% _.each(transform, function(t) { %>\
</g>\
<% }); %>\
</svg>';
return svg;
}
function getSVGOverlaySpiral(width, height, type) {
//corresponds to values hardcoded in SVG template
var viewboxWidth = 987.6,
viewboxHeight = 611,
viewbox = [0, 0, viewboxWidth, viewboxHeight].join(','),
// rotation
needsRotation = (width < height),
viewboxCenterX = viewboxWidth / 2,
viewboxCenterY = viewboxHeight / 2,
rotationTransform = 'rotate(' + [90, viewboxCenterX, viewboxCenterY].join(",") + ')',
// after rotating around (W/2,H/2), recenter around (H/2,W/2)
translateX = viewboxCenterY - viewboxCenterX,
translateY = viewboxCenterX - viewboxCenterY;
translationTransform = 'translate(' + translateX + ',' + translateY + ')';
// from http://tech.groups.yahoo.com/group/svg-developers/message/14903
var transformations = [];
if (needsRotation) {
// Note: SVG transformation order is important and counter-intuitive
transformations.push(translationTransform, rotationTransform);
viewbox = [0, 0, viewboxHeight, viewboxWidth].join(',');
}
switch (type) {
case "SPIRAL_90":
var transform = 'matrix(-1,0,0,1,' + viewboxWidth +',0)'; //horizontal flip
// var transform = 'rotate(90 494 306)';
break;
case "SPIRAL_180":
var transform = 'matrix(-1,0,0,-1,' + viewboxWidth +',' + viewboxHeight + ')'; // vertical + horizontal flip
break;
case "SPIRAL_270":
var transform = '';
var transform = 'matrix(1,0,0,-1,0,' + viewboxHeight + ')'; //horizontal flip
break;
case "SPIRAL_0":
default:
var transform = '';
}
if (transform) {
transformations.push(transform);
}
var svg = _.template(getSVGOverlaySpiralTemplate(), {
transform: transformations,
viewbox: viewbox
});
// because firefox seems to need this; see /tests/svg-viewbox.html
svg = jQuery(svg).css({
width: '100%',
height: '100%'
});
return svg;
}
function getSVGOverlayThirdsGridTemplate() {
// multiline strings in JS require trailing backslashes
// including the <?xml tag causes errors, so we remove it
// var svg = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\
var svg = '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="<%= viewbox %>" preserveAspectRatio="none">\
<% _.each(lines, function(line) { %>\
<polyline points="<%= line %>" style="stroke: grey; stroke-width:4;" vector-effect="non-scaling-stroke" />\
<polyline points="<%= line %>" style="stroke: white; stroke-width:2;" vector-effect="non-scaling-stroke" />\
<% }); %>\
</svg>';
return svg;
}
function getSVGOverlayThirdsGrid(width, height, type) {
/* | |
--+--+-- h1
| |
--+--+-- h2
| |
v1 v2 */
switch (type) {
case "TRIANGLES":
var w = width, h = height, w_sq = Math.pow(w,2), h_sq = Math.pow(h,2);
var lines = {
d1: [0, 0, (h_sq * w)/(w_sq + h_sq), h * w_sq / (w_sq + h_sq)],
d2: [w - (h_sq * w)/(w_sq + h_sq), h - h * w_sq / (w_sq + h_sq), w, h],
d3: [0, h, w, 0],
};
break;
case "TRIANGLES_90":
var w = width, h = height, w_sq = Math.pow(w,2), h_sq = Math.pow(h,2);
// vertical axis mirror from TRIANGLES
var lines = {
d1: [w, 0, w - (h_sq * w)/(w_sq + h_sq), h * w_sq / (w_sq + h_sq)],
d2: [(h_sq * w)/(w_sq + h_sq), h - h * w_sq / (w_sq + h_sq), 0, h],
d3: [0,0,w,h]
};
break;
case "GRID_PHI":
var phi = 1.6180339887;
var v1 = width * (1-(1/phi)),
v2 = width * (1/phi),
h1 = height * (1-(1/phi)),
h2 = height * (1/phi);
var lines = {
h1: [0, h1, width, h1],
h2: [0, h2, width, h2],
v1: [v1, 0, v1, height],
v2: [v2, 0, v2, height]
};
break;
case "GRID_THIRDS":
default:
var v1 = width * (1/3),
v2 = width * (2/3),
h1 = height * (1/3),
h2 = height * (2/3);
var lines = {
h1: [0, h1, width, h1],
h2: [0, h2, width, h2],
v1: [v1, 0, v1, height],
v2: [v2, 0, v2, height]
};
}
// inspired by https://github.com/documentcloud/underscore/issues/220
function objectMapPreserveKeys(obj,map) {
return _.reduce(obj, function(memo,v,k) { memo[k] = map(v); return memo; }, {});
}
var svg = _.template(getSVGOverlayThirdsGridTemplate(), {
viewbox: [0,0,width,height].join(" "),
lines: _.map(lines, function(v) { return v[0] + "," + v[1] + " " + v[2] + "," + v[3]; })
});
return jQuery(svg);
}
function getCanvasOverlay(width,height){
var canvas = jQuery('<canvas />')
.attr({
'width':width,
'height':height
});
var ctx = canvas[0].getContext('2d');
var drawLine = function(line,strokeStyle,lineWidth) {
var x1 = line[0],
y1 = line[1],
x2 = line[2],
y2 = line[3];
ctx.beginPath();
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.stroke();
}
function drawLines(lines,color,size) {
lines.forEach(function(line) {
drawLine(line,color,size);
});
}
/* | |
--+--+-- h1
| |
--+--+-- h2
| |
v1 v2 */
var h1 = [0, height/3, width, height/3],
h2 = [0, 2*height/3, width, 2*height/3],
v1 = [width/3, 0, width/3, height],
v2 = [2*width/3, 0, 2*width/3, height];
var lines = [h1,h2,v1,v2];
drawLines(lines,'white',12);
drawLines(lines,'black',4);
return canvas;
}
(function(){
var done = false;
var script = document.createElement("script");
script.src = "//cdnjs.cloudflare.com/ajax/libs/yepnope/1.5.4/yepnope.min.js";
script.onload = script.onreadystatechange = function(){
if (!done && (!this.readyState || this.readyState == "loaded" || this.readyState == "complete")) {
done = true;
requireDeps();
}
};
document.getElementsByTagName("head")[0].appendChild(script);
})();
function requireDeps() {
function isUndefined(val) {
return typeof(val) === "undefined";
}
var needJq = isUndefined(window.jQuery) || jQuery.fn.jquery.match(/^1\.[0-9]+/) <= 1.4,
needJqUiPosition = isUndefined(window.jQuery) || isUndefined(jQuery.ui) || isUndefined(jQuery.ui.position),
needUnderscore = isUndefined(window._) || isUndefined(_.template);
yepnope([{
test: needJq,
yep: '//cdnjs.cloudflare.com/ajax/libs/jquery/1.4.4/jquery.min.js',
}, {
// check for jQuery.ui.position
test: needJqUiPosition,
yep: 'https://gist.github.com/raw/4331769/dbfadedd1691b5e5fa006062682bab14390bec52/jquery.ui.position.js'
}, {
// check for jQuery.ui.position
test: needUnderscore,
yep: '//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.3/underscore-min.js',
complete: function (url, result, key) {
initMyBookmarklet();
// TODO: makes our jQuery version not clobber pre-existing one (eg for pinterest)
// jQuery.noConflict(true) doesn't seem to work properly
}
}
]);
}
div.imgWrapper.enabled {
position: relative;
display: inline-block;
}
div.imgWrapper.enabled:before {
background:url("https://gist.github.com/raw/4331769/rule-of-thirds.png");
background-size: 100% 100%;
background-repeat:no-repeat;
width:100%;
height: 100%;
z-index: 1;
top:0;
left:0;
position: absolute;
content: " ";
}
div.imgWrapper.enabled>img {
filter: url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'grayscale\'><feColorMatrix type=\'matrix\' values=\'0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\'/></filter></svg>#grayscale"); /* Firefox 10+, Firefox on Android */
-webkit-filter: grayscale(100%);
-moz-filter: grayscale(100%);
-ms-filter: grayscale(100%);
-o-filter: grayscale(100%);
filter: grayscale(100%);
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<h1> Spiral on Vertical Image </h1>
<img src="//upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_natural_color.jpg/401px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_natural_color.jpg" width="401" height="599">
<script type="text/javascript" src="/rule-of-thirds.bookmarklet.js"></script>
<script type="text/javascript">
//overrides standard behavior
function initMyBookmarklet() {
createOverlays(function(width,height,degrees) {
return getSVGOverlaySpiral(width, height, "SPIRAL_270");
});
}
</script>
<style>
svg {
border: 2px solid green;
}
div {
border: 2px solid blue;
}
</style>
<h1> SVG viewbox rotation </h1>
This demonstrates what it takes to achieve rotating the svg. Note that the red lines are originally horizontal.
Pay attention to the following:
<li> rotate(90,CX,CY)
<li> translate(CY-CX, CX-CY)
<li> there's width on the wrapper, which is post-rotation stretched via viewbox="0,0,H,W", preserveAspectRatio=none
<li> viewbox="0,0,H,W" instead of "0,0,W,H"
<h2> Resources </h2>
<li> http://en.wikipedia.org/wiki/Matrix_multiplication
<li> http://www.w3.org/TR/SVGTiny12/coords.html#NestedTransformations
<li> http://commons.oreilly.com/wiki/index.php/SVG_Essentials/Transforming_the_Coordinate_System
<li> http://webdesign.about.com/od/svg/a/svg-viewbox-attribute.htm
<div id="wrapper">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewboxOriginal="0,0,300,600"
viewbox="0,0,300,600"
preserveAspectRatio="none"
version="1.1">
<defs>
<g id="horizontal-lines">
<polyline vector-effect="non-scaling-stroke" points="0,100 600,100" />
<polyline vector-effect="non-scaling-stroke" points="0,200 600,200" />
</g>
<g id="vertical-lines">
<polyline vector-effect="non-scaling-stroke" points="200,0 200,300" />
<polyline vector-effect="non-scaling-stroke" points="400,0 400,300" />
</g>
</defs>
<g transform="translate(-150,150)">
<g transform="rotate(90,300,150)">
<use xlink:href="#horizontal-lines" style="stroke: red; stroke-width: 2"/>
<use xlink:href="#vertical-lines" style="stroke: black; stroke-width: 2"/>
</g>
</g>
</svg>
</div>
<style>
svg {
border: 2px solid green;
}
div#wrapper {
border: 2px solid blue;
width: 450px;
height: 900px;
}
</style>
<h1> SVG viewbox </h1>
<p>This demonstrates that an embedded SVG is sized differently in chrome and firefox.
It seems that firefox just ignores preserveAspectRatio.
<p>The following CSS demonstrates a workaround: <pre>svg:hover { width:100%; height: 100%}</pre>
<div id="wrapper">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewbox="0,0,600,300"
preserveAspectRatio="none"
version="1.1">
<defs>
<g id="horizontal-lines">
<polyline vector-effect="non-scaling-stroke" points="0,100 600,100" />
<polyline vector-effect="non-scaling-stroke" points="0,200 600,200" />
</g>
<g id="vertical-lines">
<polyline vector-effect="non-scaling-stroke" points="200,0 200,300" />
<polyline vector-effect="non-scaling-stroke" points="400,0 400,300" />
</g>
</defs>
<use xlink:href="#horizontal-lines" style="stroke: red; stroke-width: 2"/>
<use xlink:href="#vertical-lines" style="stroke: black; stroke-width: 2"/>
</svg>
</div>
<style>
svg {
border: 2px solid green;
}
svg:hover {
width: 100%;
height: 100%;
}
div#wrapper {
border: 2px solid blue;
width: 150px;
height: 300px;
}
</style>
<h2> Resources </h2>
<li> http://webdesign.about.com/od/svg/a/svg-viewbox-attribute.htm
<li> https://bugzilla.mozilla.org/show_bug.cgi?id=777265
<li> http://jsfiddle.net/gonsfx/Rm2LE/ "Firefox PreserveAspectRatio BUG"
<li> http://stackoverflow.com/questions/644896/how-do-i-scale-a-stubborn-svg-embedded-with-the-object-tag
<li> http://stackoverflow.com/questions/10524978/html5-svg-preserveaspectratio-none-not-working-in-firefox
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.