Skip to content

Instantly share code, notes, and snippets.

@bmershon
Last active July 14, 2017 20:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmershon/7ac748639fed3abf20483b7995e6c24b to your computer and use it in GitHub Desktop.
Save bmershon/7ac748639fed3abf20483b7995e6c24b to your computer and use it in GitHub Desktop.
SVG Spritesheet
scrolling: yes
license: MIT
border: no

Inspired by GitHub's Octicon spritesheet system.

It is often desired to be able to dynamically style monochrome SVG icons much the way text may be styled with the color CSS property. In order to allow for SVGs to be styled, it is recommended to produce flat SVG assets which use only paths with a fill property, rather than some combination of fills and strokes with a given width.

By removing all fill attributes from a candidate SVG's groups and setting the top-level style of a container SVG to fill: currentColor, an SVG may be easily styled to respond to user interaction.

Furthermore, it is often desired to bundle many SVG definitions into a single spritesheet and then reference these definitions using the <use> tag; each instance simply deep clones the definition found in the spritesheet so that the SVG may be uniquely styled by inheriting a color property from a containing DOM element. In order to reference unique definitions, each definition needs a unique id attribute. The filename of a given SVG asset is a good candidate for a unique id.

Drag

Drag SVG files from your file system into the drag-n-drop area.

Inspect

Inspect the rendered SVG assets, which reference a definition in a spritesheet using the filename as a unique id.

Highlighted assets may:

  • Have fill properties with a value other than none or #000000.
  • Use a stroke attribute (a flat SVG uses only paths with a fill property).
  • Have improperly formed markup.

Use

Use the spritesheet.svg downloaded automatically by your browser by referencing ids equal to a given asset's filename like so:

<!DOCTYPE html>
<meta charset="utf-8">
<body>

<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <defs>
   ...
   <symbol>
    <g id=-"my_awesome_icon">
    </g>
   </symbol>
   ...
  </devs>
</svg>

...

<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg">
  <use xlink:href="#my_awesome_icon" />
</svg>

</body>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: helvetica;
margin: 0;
min-width: 100%;
min-height: 100%;
}
.overlay {
border: 3px dashed #fff;
box-sizing: border-box;
height: 100%;
min-height: 500px;
min-width: 960px;
width: 100%;
position: absolute;
z-index: 100;
}
.example {
z-index: 200;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
<body>
<svg class="overlay">
</svg>
</body>
<script src="xml.js"></script>
<script>
var svg = d3.select(".overlay"),
width = +svg.style("width").replace("px", ""),
height = +svg.style("height").replace("px", "");
var text = svg.append("text")
.style("text-anchor", "middle")
.style("font-size", "24px")
.attr("x", width / 2)
.attr("y", height / 2)
.text("Drag and drop SVG files");
d3.select("body")
.on("dragover", dragOver)
.on("mouseleave", mouseleave)
.on("drop", selectFile);
function selectFile() {
// Remove the visual cue that items are being drag-n-dropped.
mouseleave();
text.remove();
var files = event.dataTransfer.files;
var documents = []; // Array of promises which resolve to an XML Document.
d3.event.stopPropagation();
d3.event.preventDefault();
for (var i = 0, file; file = files[i]; i++) {
documents.push(svgDocument(file));
}
// Wait for all files to be parsed from text into an XML Document.
Promise.all(documents)
.then((documents) => {
window.focus();
let spritesheet, metadata;
[spritesheet, data] = svgSpritesheet(documents);
data = data.map((d, i) => {
let datum = {};
Object.assign(datum, d);
datum.name = files[i].name;
return datum;
});
ready(data, spritesheet);
downloadText("spritesheet.svg", spritesheet);
});
}
// Make explicit to the user that the dragging operation will result in a copy.
function dragOver() {
// Produce a visual cue that items are being drag-n-dropped.
svg
.style("background-color", "rgba(150, 208, 150, 0.7)")
.style("border", "3px dashed #000");
d3.event.stopPropagation();
d3.event.preventDefault();
d3.event.dataTransfer.dropEffect = 'copy';
}
function mouseleave() {
// Remove the visual cue that items are being drag-n-dropped.
svg
.style("background-color", null)
.style("border", "3px dashed #fff");
}
// Render each asset specified by it's unique name with a <use> tag,
// referencing the spritesheet that is downloaded.
function ready(data, spritesheet) {
d3.selectAll(".assets").remove();
// SVG definitions.
d3.select("body").append("svg")
.html(spritesheet)
.style("display", "none");
d3.selectAll(".assets").remove();
let assets = d3.select("body").append("div")
.attr("class", "assets")
.style("display", "flex")
.style("flex-direction", "row")
.style("align-items", "center")
.style("flex-wrap", "wrap");
// Create and append a DIV the DOM for each asset in our dataset.
let example = assets.selectAll(".example")
.data(data)
.enter().append("div")
.attr("class", "example")
.style("box-sizing", "border-box")
.style("display", "flex")
.style("flex-shrink", 0)
.style("flex-direction", "column")
.style("align-items", "center")
.style("margin", "5px")
.style("padding", "15px")
.style("width", "145px")
.style("font-family", "helvetica")
.style("font-size", "14px")
.style("background-color", (d) => {
// Highlight possible problematic assets.
if (d.fill || d.stroke) return "#fff8dc";
return null;
})
.on("mouseover", function() {
d3.select(this)
.style("color", "#005F9E");
})
.on("mouseleave", function() {
d3.select(this)
.style("color", null) // Default color.
});
// Create and append a SVG with a <use> tag to the asset DIV.
let svg = example.append("svg")
.attr("width", 32)
.attr("height", 32)
.attr("display", "inline-block")
.attr("vertical-align", "text-top")
.attr("fill", "currentColor")
.append("svg:use")
.attr("xlink:href", d => "#" + d.name.substring(0, d.name.indexOf(".svg")));
// Create and append a DIV as a child of each example which will show text.
let label = example.append("div")
.html(d => d.name)
.style("width", "115px")
.style("white-space", "nowrap")
.style("overflow", "hidden")
.style("text-overflow", "ellipsis")
.style("text-align", "center")
.attr("display", "inline-block");
}
function downloadText(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
</script>
// Returns a promise which resolves with a Document representing a parsed SVG
// from the given text file. The top-level group tag is given an id equal to
// the original file name (without an extension).
function svgDocument(file) {
var reader = new FileReader;
return new Promise((resolve, reject) => {
reader.onload = function(e) {
var text = e.target.result;
var parser = new DOMParser();
var xmlDocument = parser.parseFromString(text, "text/xml");
var name = file.name.substring(0, file.name.indexOf(".svg"));
var svg = xmlDocument.querySelector('svg');
if (svg != null) {
var g = svg.querySelector('g');
if (g != null) {
// Set top-level group id to be the filename.
g.setAttribute('id', name);
} else {
// Enclose SVG elements in a parent group tag.
let newGroup = document.createElement('g');
newGroup.setAttribute('id', name);
for (let i = 0; i < svg.children.length; i++) {
let child = svg.children[i];
newGroup.appendChild(child);
}
svg.appendChild(newGroup);
}
}
resolve(xmlDocument);
};
reader.readAsText(file);
reader.onerror = reject;
});
}
// Returns [Document, Metadata]
function sanitizeSvg(svgDocument) {
let tags = ["path", "g", "polyline", "polygon"],
notes = {
fill: false, // True if a fill rule other than `fill: #000000;` is used.
stroke: false // True if a stroke attribute is used.
};
tags.forEach((tag) => {
svgDocument.querySelectorAll(tag).forEach((element) => {
let fill = element.getAttribute("fill");
if (fill && fill !== "#000000") {
notes.fill = true;
}
let stroke = element.getAttribute("stroke");
if (stroke) {
notes.stroke = true;
}
element.removeAttribute("fill");
element.removeAttribute("stroke");
});
});
// TODO: Return useful metadata regarding the svgDocument.
return [svgDocument, notes];
}
// Returns a string representing a sprite sheet for the given XML documents.
function svgSpritesheet(documents) {
let serializer = new XMLSerializer(),
s = "",
data = [];
documents.forEach((d) => {
let sanitizedSvg,
datum,
formattedSvg,
symbolizedSvg;
[sanitizedSvg, datum] = sanitizeSvg(d);
formattedSvg = serializer.serializeToString(sanitizedSvg);
symbolizedSvg = formattedSvg
.replace("<svg", "<symbol")
.replace("</svg>", "</symbol>");
s += symbolizedSvg;
data.push(datum);
});
return ['<svg xmlns="http://www.w3.org/2000/svg"><defs>'
+ s
+ '</defs></svg>',
data];
}
@bmershon
Copy link
Author

bmershon commented Apr 3, 2017

TODO

  • Ensure drag and drop cleans up the state of the DOM and the overlay captures the drop.
  • Ensure the SVG spritesheet that is produced is valid.

@WillSketchUp
Copy link

Regarding xml.js line 42
Consider returning an immutable object of the structure

{
  original: <svg element>,
  current: <svg element>,
  metadata: { ... } // Structure TBD
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment