The thing about SVG is it's just XML. You can load the SVG into an XML parser and get just the parts you want, then add them to a canvas or otherwise manipulate them independently.
I just did this for my own current project. The image is a simple chess set, but I'm able to have a whole chess set in a single file, including white and black pieces, then parse them out individually, so I can resize or even change their color. This JavaScript could use some refactoring, but it should give you a good idea of what you need to do.
loadPieces("images/SimpleStaunton.svg");
loadPieces(fileName) // the file name is a relative path
{
var request = new XMLHttpRequest();
request.open("GET", fileName);
request.setRequestHeader("Content-Type", "image/svg+xml");
var me = this;
request.addEventListener("load", function(event) {
var response = event.target.responseText;
var doc = new DOMParser();
var xml = doc.parseFromString(response, "image/svg+xml");
me.parseChessSets(xml.children[0]);
});
request.send();
}
parseChessSets(xml)
{
this.baseSet = new ChessSet(xml);
}
To make this next part work, I went into InkScape and named each individual object so I would know exactly what part of the SVG I was pulling out of the XML. (I included a knight that faces left and one that faces right, so that's why there's two knights.) Since they are SVG, you can color them however you want, so I'm just pulling out the paths. You may want to do more than that. Once you get the XML into an object
, you'll be able to more easily see how to do that.
The tricky part is that whitePiece.getAttribute("d")
, or rather xml.children[0].getAttribute("d")
, is the path of the SVG, which isn't obvious, at all.
class ChessSet
{
white = new ChessPieces();
black = new ChessPieces("#fff", "#000");
pieceNames = ["king", "queen", "bishop", "knightl", "knightr", "rook", "pawn"];
constructor (xml)
{
for (var i = 0; i < this.pieceNames.length; i++)
{
var whitePiece = xml.children["white" + this.pieceNames[i]];
if (!whitePiece)
{
continue;
}
var blackPiece = xml.children["black" + this.pieceNames[i]] || whitePiece;
this.white[this.pieceNames[i]] = new SvgImage(whitePiece.getAttribute("d"), whitePiece.style);
this.black[this.pieceNames[i]] = new SvgImage(blackPiece.getAttribute("d"), blackPiece.style);
}
}
class ChessPieces
{
scalePercent = 100; // percentage to shrink king to 90% height of a square
strokeColor;
fillColor;
king;
queen;
bishop;
knightr;
knightl;
rook;
pawn;
constructor(stroke = "#000", fill = "#fff")
{
this.strokeColor = stroke;
this.fillColor = fill;
}
}
class SvgImage
{
transform = {left: 0, down: 0};
pathElement = null;
name = null;
scaleFactor = 1;
strokeWidth = 1;
constructor(path, style)
{
this.path = path;
this.style = style;
}
}
Then I can draw the individual path:
var newElement = this.drawPath(set[this.baseSet.pieceNames[i]].name, set[this.baseSet.pieceNames[i]].path, 1, set.strokeColor, set.fillColor);
this.context.appendChild(newElement);
drawPath(id, textPath, strokeWidth = 1, strokeColor = "#000", fill = "none", opacity = 1)
{
var newpath = document.createElementNS(this.svgns, "path");
newpath.setAttributeNS(null, "id", id);
newpath.setAttributeNS(null, "d", textPath);
newpath.setAttributeNS(null, "stroke", strokeColor);
newpath.setAttributeNS(null, "stroke-width", strokeWidth);
newpath.setAttributeNS(null, "opacity", opacity);
newpath.setAttributeNS(null, "fill", fill);
return newpath;
}
The variable this.context
is the canvas DOM I'm using.
Now I can resize the piece using a CSS transform and the object/variable piece.scaleFactor
. I'm sending piece
as the individual SVG we parsed out earlier into the SVGImage
class. The X and Y coordinated I send to Move
are pixels.
transformPiece(piece, left, down)
{
if (piece)
{
piece.transform.left = left;
piece.transform.down = down;
piece.pathElement.style.transform = "scale(" + piece.scaleFactor + ") translate(" + piece.transform.left + "px, " + piece.transform.down + "px)";
}
}
movePiece(piece, x, y)
{
if (piece)
{
var left = x / piece.scaleFactor;
var down = y / piece.scaleFactor;
this.transformPiece(piece, left, down);
}
}
The caveat is that you'll need to accommodate for the offset of the SVG in the original image. What I ended up doing is to rewrite the starting coordinates in the SVG path itself. Part of the caveat is that you have to draw the image to the canvas before you can figure out what the offset even is, so you end up having to remove the original image and add the new image to the canvas before you can call movePiece
or transformPiece
and have it end up where you expect it.
fixPath(piece, strokeColor, fillColor)
{
if (piece)
{
var box = piece.pathElement.getBoundingClientRect()
var fragments = piece.path.split(" ");
var coords = fragments[1].split(",");
var x = (coords[0] * 1) - box.x - window.scrollX;
var y = (coords[1] * 1) - box.y - window.scrollY;
fragments[1] = x + "," + y;
piece.path = fragments.join(" ");
this.context.removeChild(piece.pathElement);
piece.pathElement = this.drawPath(piece.name, piece.path, 1, strokeColor, fillColor);
this.context.appendChild(piece.pathElement);
}
}
It only took me a week or two to figure all this out, as well as all the wrong ways to do it. ;-)
Sorry this is a bit complicated. I simplified it a bit, but I also left in a bunch of object
notation and properties you don't care about. I'm sure you could simplify this considerably for your needs. Mostly I just copy & pasted this from my project so I would know the code was working.