Skip to content

Instantly share code, notes, and snippets.

@wassname
Last active December 11, 2022 15:55
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wassname/a9d3d0914c739eec82ee to your computer and use it in GitHub Desktop.
Save wassname/a9d3d0914c739eec82ee to your computer and use it in GitHub Desktop.
Isometric cubes using svg tranforms
<html>
<head>
<meta charset="utf-8">
<title>Isometric SVG</title>
<link type="text/css" href="style.css" rel="stylesheet" />
</head>
<body>
<h1>An svg isometric cube at any angle</h1>
<p>Move the slide to change the perpsective angle. Isometric SVG's are usefull for game sprites and this example show how to use SVG transforms to generate an isometric cube at any angle.</p>
</body>
</html>

Isometric cubes using svg tranforms

Isometric cubes using svg tranforms. This uses SVG transforms: skew, rotate, and scale, to make isometric cubes of any projections angle (26.6, 30, 45). Good for making SVG sprites for isometric games.

A Pen by M Clark on CodePen.

License.

var url = "https://upload.wikimedia.org/wikipedia/commons/b/bd/Test.svg"
var SvgCube = function (options) {
// Inputs and options
var defaultOptions = {
angle: 30,
size: 64,
verbose: false,
// outline
drawOutline: true,
drawShading: true,
clipCircle: false,
stroke: {
"arrow-end": 'none',
"stroke": 'black', // stroke color for outline
"stroke-width": Math.sqrt(options.size)/2, // outline width
"stroke-linecap": "round",
"stroke-linejoin": "round",
"fill": "none",
},
// cube
flatten: 0, // fraction to vertically flatten the cube
topUrl: '', // url for image in top of cuve
topRot: 0, // rotation of top image in degrees
topShad: 0, // shading for top
leftUrl: '',
leftRot: 0,
leftShad: 0.3,
rightUrl: '',
rightRot: 0,
rightShad: 0.4,
svgNS: "http://www.w3.org/2000/svg",
padding: 0,
}
// add defaults, 2 levels deep
options = options || {};
for (var opt in defaultOptions){
if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt)){
options[opt] = defaultOptions[opt];
}
for (var opt2 in defaultOptions[opt]){
if (defaultOptions[opt].hasOwnProperty(opt2) && !options[opt].hasOwnProperty(opt2)){
options[opt][opt2] = defaultOptions[opt][opt2];
}
}
}
this.options = options
this.angle = options.angle
this.w = options.size; // input image width
this.h = options.size;
this.f = options.flatten
this.rot = this.angle * Math.PI / 180
this.padding = options.padding; // pading fraction
this.cw = this.w; // we will keep same width but change height
this.ch = (1 + this.padding) * (this.h / 2 + this.h * Math.tan(this.rot)) -this.h/2*(1-this.f); //canvas height full
// create SVG element
this.paper = Snap(this.cw, this.ch);
this.svg=this.paper.node
// this.svg = document.createElementNS(options.svgNS,"svg");
// this.svg.setAttributeNS (null, "viewBox", "0 0 " + this.cw + " " + this.ch);
// this.svg.setAttributeNS (null, "width", this.cw);
// this.svg.setAttributeNS (null, "height", this.ch);
// this.svg.style.display = "block";
// this.svg = document.querySelector('svg')
// document.body.appendChild (this.svg);
}
/* drawing measurements as a function of padding */
SvgCube.prototype.measurements= function(p){
if (p===undefined){
p=0;
}
var f = this.options.flatten
var tBox = this.imageT.getBBox()
var lBox = this.imageL.getBBox()
var rBox = this.imageR.getBBox()
return {
// measurements
uf : 1-f,
p : 0, // lw/2, // padding
tw: this.cw-p/2, // right side. adjust by half line thickness to keep line in canvas
th: tBox.height-p/2, // height of square, and dist to middle
mw: this.cw/2, // middle x
mh: tBox.height/2, // middle of top square
bh: 0+p/2, // top of picture, bottom y
bw: 0+p/2, // left of picture, bottom of x
sw: this.cw-p/2,// bottom of cube
sh: this.ch-p/2, // right of cube
lq: -tBox.height/2+this.ch-p/2, // lower quarter of height
uq: tBox.height/2+p/2 // upper quarter
}
}
/*
* Adds a svg transform to an element, the transform has the origin of
* xi,yi fraction of the element, so 0.5,0.5 is the middle,
* unlike normalsvg it adds transforms in order of application not reverse order
* Usage: svgTransform(elementImage,'rotate',[45],0.5,0.5)
* This would rotate 45 degrees around center of image
*/
SvgCube.prototype.svgTransform = function (element, op, inputs, xi, yi) {
if (isNaN(xi)) {
xi = 0.5;
}
if (isNaN(yi)) {
yi = 0.5;
}
var svgBox = this.svg.getClientRects()[0]
var cbox = element.getBoundingClientRect();
var x = cbox.left + xi * cbox.width -svgBox.left;
var y = cbox.top + yi * cbox.height -svgBox.top;
var matrix = this.svg.createSVGMatrix()
matrix = matrix.translate(x, y)
matrix = matrix[op].apply(matrix, inputs);
matrix = matrix.translate(-x, -y);
var transform = this.svg.createSVGTransform();
transform.setMatrix(matrix);
//element.transform.baseVal.appendItem(transform); // for reverse order
element.transform.baseVal.insertItemBefore(transform, 0) // normal order
}
/*
* transform an element to be the left of an isometric cube
* Inputs: dom element, angle in degrees, and xi,yi which
* are the transform origin as a fraction of element size
*/
SvgCube.prototype.toLeft = function (element, angle, xi, yi) {
// half it's width
xi = 0
yi = 1
if (this.f>0){
this.svgTransform(element, 'scaleNonUniform', [1,this.f],0.5,0.5)
}
this.svgTransform(element, 'translate', [-1, -1]) // pixel adjustment HACK
this.svgTransform(element, 'scaleNonUniform', [1 / 2, 1 / 2], xi, yi)
// skew it, in degrees
this.svgTransform(element, 'skewY', [angle], xi, yi)
}
/*
* transform an element to be the right of an isometric cube
* Inputs: dom element, angle in degrees, and xi,yi which
* are the transform origin as a fraction of element size
*/
SvgCube.prototype.toRight = function (element, angle, xi, yi) {
xi = 1
yi = 1
if (this.f>0){
this.svgTransform(element, 'scaleNonUniform', [1,this.f],0.5,0.5)
}
this.svgTransform(element, 'translate', [-1, -2]) // pixel adjustment HACK
// half it's width to fiit in canvas
this.svgTransform(element, 'scaleNonUniform', [1 / 2, 1 / 2], xi, yi)
// skew it
this.svgTransform(element, 'skewY', [-angle], xi, yi)
}
/*
* transform an element to be the top of an isometric cube
* Inputs: dom element, angle in degrees, and xi,yi which
* are the transform origin as a fraction of element size
*/
SvgCube.prototype.toTop = function (element, angle, xi, yi) {
var rot = angle * Math.PI / 180;
this.svgTransform(element, 'translate', [-2, -1]) // pixel adjustment HACK
// rotate so it's a diamond
this.svgTransform(element, 'rotate', [45], xi, yi)
// squish - along x axis to fit in canvas, along y axis for perspective change
this.svgTransform(element, 'scaleNonUniform', [Math.sin(45 * Math.PI / 180), Math.tan(rot) * Math.sin(45 * Math.PI / 180)], xi, yi)
}
SvgCube.prototype.moveTop = function (element) {
// align top of cube with top of canvas
var cbox = element.getBoundingClientRect();
this.svgTransform(element, 'translate', [-cbox.left + this.pPx, -cbox.top + this.pPx])
}
/*
* align left of cube with top,
* fit upper-left of left panel with middle-left of top panel
*/
SvgCube.prototype.moveLeft = function (elem, elemT) {
var cboxL = elem.getBoundingClientRect();
var cboxT = elemT.getBoundingClientRect();
// align left
var x = cboxT.left - cboxL.left
// align top of left with half height of
var y = cboxT.top - cboxL.top + cboxT.height / 2
this.svgTransform(elem, 'translate', [x, y])
}
/*
* align right of cube with top,
* fit upper-right of right panel with middle-right of top panel
*/
SvgCube.prototype.moveRight = function (elem, elemT) {
// line up left with top, move to half tops height, and to align left
var cboxL = elem.getBoundingClientRect();
var cboxT = elemT.getBoundingClientRect();
// align right with right
var x = cboxT.right - cboxL.right
// align top of left with half height of
var y = cboxT.top - cboxL.top + cboxT.height / 2
this.svgTransform(elem, 'translate', [x, y])
}
/* draw outline get line color from option.strokeColor, and width from options.stroke-width */
SvgCube.prototype.drawOutline = function(lw){
lw=lw||this.options.stroke["stroke-width"];
var ms = this.measurements(lw/2)
var tb = this.imageT.getBBox()
var lb = this.imageL.getBBox()
var rb = this.imageR.getBBox()
// Draw outline of top
var strTop=
'M'+ms.mw +' '+ms.th +' '+ // Move to bottom
'L'+ms.bw +' '+ms.mh+' '+ // left
'L'+ms.mw +' '+ms.bh+ ' '+ // top
'L'+ms.tw +' '+ms.mh+' '+ // right
'Z' // close
var pathTop = this.paper.path(strTop);
// // outline of left
var strLeft=
'M'+ms.mw +' '+ms.th+' '+ // middle
'L'+ms.bw +' '+ms.uq+' '+ // left top
'L'+ms.bw +' '+ms.lq+' '+ // left bottom
'L'+ms.mw +' '+ms.sh+' '+ // middle bottom
'Z'
var pathLeft = this.paper.path(strLeft);
//
// right
var strRight=
'M'+ms.mw +' '+ms.th+' '+ // middle
'L'+ms.mw +' '+ms.sh +' '+ // middle bottom
'L'+ms.tw +' '+ms.lq+' '+ // right bottom
'L'+ms.tw +' '+ms.uq+' '+ // right top
'Z' // close
var pathRight = this.paper.path(strRight);
// join into set
var pathGroup = this.paper.group();
pathGroup.append(pathTop);
pathGroup.append(pathLeft);
pathGroup.append(pathRight);
// set attrs from options
// ref http://raphaeljs.com/reference.html#Element.attr
var blackList = ['url','target','src','title']
for (var a in this.options.stroke){
if (this.options.stroke.hasOwnProperty(a) && blackList.indexOf(a)<0){
pathGroup.attr(a,this.options.stroke[a]);
}
}
this.outline=pathGroup;
}
/* draw outline get line color from option.strokeColor, and width from options.stroke-width */
SvgCube.prototype.drawShading = function(lw){
lw=lw||this.options.stroke["stroke-width"];
var ms = this.measurements(0);
var pathGroup = this.paper.g();
var strTop=
'M'+ms.mw +' '+ms.th +' '+ // Move to bottom
'L'+ms.bw +' '+ms.mh+' '+ // left
'L'+ms.mw +' '+ms.bh+ ' '+ // top
'L'+ms.tw +' '+ms.mh+' '+ // right
'Z' // close
var pathTop = this.paper.path(strTop);
pathGroup.append(pathTop);
// outline of left
var strLeft=
'M'+ms.mw +' '+ms.th+' '+ // middle
'L'+ms.bw +' '+ms.uq+' '+ // left top
'L'+ms.bw +' '+ms.lq+' '+ // left bottom
'L'+ms.mw +' '+ms.sh+' '+ // middle bottom
'Z'
var pathLeft = this.paper.path(strLeft);
pathGroup.append(pathLeft);
// last line from middle down
var strRight=
'M'+ms.mw +' '+ms.th+' '+ // middle
'L'+ms.mw +' '+ms.sh +' '+ // middle bottom
'L'+ms.tw +' '+ms.lq+' '+ // right bottom
'L'+ms.tw +' '+ms.uq+' '+ // right top
'Z' // close
var pathRight = this.paper.path(strRight);
pathGroup.append(pathRight);
// style the set
pathGroup.attr({
'stroke': 'none',
'fill': 'black',
'stroke-width': 0,
'stroke-opacity': 0,
'stroke-linecap': 'round',
'stroke-linejoin': 'round'
});
// shade each side, 0 is no shading, 1 is black
pathTop.attr({'fill-opacity': this.options.topShad});
pathLeft.attr({'fill-opacity': this.options.leftShad});
pathRight.attr({'fill-opacity': this.options.rightShad});
this.shading=pathGroup;
}
/*clip the cube using an elipse to give rounded corners */
SvgCube.prototype.clipCircle = function(amount){
var cp = document.createElementNS("http://www.w3.org/2000/svg","clipPath")
cp.id="cp";
var rxc=51+4*(1-this.f)*(1-this.f);
var ryc=50-2/Math.sqrt(1-this.f);
cp.innerHTML = '<ellipse xmlns="http://www.w3.org/2000/svg" cx="50%" cy="50%" rx="'+rxc+'%" ry="'+ryc+'%" fill="white"/>'
this.paper.node.getElementsByTagName("defs")[0].appendChild(cp);
this.paper.node.setAttribute("clip-path","url(#cp)");
}
SvgCube.prototype.drawCube = function () {
var cube = this.paper.g();
this.imageL = cube.image(this.options.leftUrl, 0, 0, this.w, this.h);
this.toLeft(this.imageL.node, this.angle);
this.imageT = cube.image(this.options.topUrl, 0, 0, this.w, this.h);
this.toTop(this.imageT.node, this.angle);
this.imageR = cube.image(this.options.rightUrl, 0, 0, this.w, this.h);
this.toRight(this.imageR.node, this.angle);
// move left and right so top is equal to bottom of top side, this work swell but possibly only in browsers?
// moveTop(this.imageT.node)
// moveLeft(this.imageL.node,this.imageT.node)
// moveRight(this.imageR.node,this.imageT.node)
// move to center of padding
//svgTransform(cube.node,'translate',[padding*cw/2,padding*ch])
// move the whole cube up by half the vertical distance it shrank
var pPx = 0 // padding px
var pPy = 0 // padding px
this.svgTransform(cube.node, 'translate', [-pPx, -this.h / 2 * (1 - Math.tan(this.rot)) - pPy])
if (this.options.drawOutline){
this.drawOutline();
}
if (this.options.drawShading){
this.drawShading();
}
if (this.options.clipCircle){
this.clipCircle();
}
return this.toSVG();
}
/* draw tile */
SvgCube.prototype.drawTile = function (){
this.imageT = this.svg.image(this.options.topUrl, 0, 0, this.w, this.h);
this.toTop(this.imageT.node, this.angle);
return this.toSVG();
}
/* returns svg string */
SvgCube.prototype.toSVG = function(){
var svg3 = this.paper.toString();
return svg3
}
var cube1
// controls
var div = document.createElement("div")
var range = document.createElement("input")
range.type = "range"
range.min = "0"
range.value = "30"
range.max = "90"
range.id = "angleInput"
var flatten = document.createElement("input")
flatten.type = "range"
flatten.min = "0.01"
flatten.value = "100"
flatten.max = "200"
flatten.id = "fInput"
var text = document.createElement("text")
text.id = "angleDisplay"
text.textContent = "30 deg"
var text2 = document.createElement("text")
text2.id = "fDisplay"
text2.textContent = "100% height"
document.body.appendChild(div)
div.appendChild(range)
div.appendChild(text)
div.appendChild(flatten)
div.appendChild(text2)
// redraw cube oninput
flatten.oninput = range.oninput = function (event) {
var angle = document.querySelector("#angleInput").valueAsNumber
var f = document.querySelector("#fInput").valueAsNumber/100
if (cube1) {
cube1.svg.remove()
}
cube1 = new SvgCube({
angle: angle,
flatten: f,
topUrl: url,
leftUrl: url,
rightUrl: url,
leftShad: 0.3,
rightShad: 0.4,
clipCircle: false,
stroke: {
"arrow-end": 'none',
"stroke": 'black', // stroke color for outline
"stroke-width": 2, // outline width
"stroke-linecap": "round",
"stroke-linejoin": "round",
"stroke-opacity": 0.5,
},
size: 128,
});
cube1.drawCube()
document.querySelector("#angleDisplay").textContent = angle + ' deg'
document.querySelector("#fDisplay").textContent = parseInt(f*100) + '% height'
}
window.onload = function () {
// initial draw by triggering out inputs
range.oninput({
srcElement: range
})
}
<script src="http://snapsvg.io/assets/js/snap.svg-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
svg, img, canvas {
border: 1px dashed grey;
}
<link href="https://cdn.jsdelivr.net/foundation/5.5.0/css/foundation.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment