Skip to content

Instantly share code, notes, and snippets.

@vsapsai
Last active April 12, 2018 05:51
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 vsapsai/875d8ccb584854ef03eec8775a4d04d6 to your computer and use it in GitHub Desktop.
Save vsapsai/875d8ccb584854ef03eec8775a4d04d6 to your computer and use it in GitHub Desktop.
Isometric Layer Composition
license: mit

Once I had a pleasure to work with text layout manager on Mac OS X. And composition of text display illustration proved to be useful. I think I've seen similar image in some other place in Cocoa documentation but cannot find it now.

In some cases such way to demonstrate layer-based composition can be very helpful. But despite its value it seems non-trivial to create such images. This is an experiment in creating images to explain layer composition. The goal is to try and see how hard it is to create such images, to explore animation, to explore various approaches.

Things to explore:

  • landscape and portrait modes;
  • how layers got separated (top stays at the same place and other layers move, middle stays, bottom, etc.);
  • what if skewing and spreading parts are separated in time.

At first thought about more correct isometric projection, more like Mike Bostock’s Isometric. But decided to go with simple transformations that mimic what you are doing by drawing cubes by hand.

I've found another example of presenting data in layers: image at the end of Accumulation section at Hurricane How-To by Adam Pearce.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body svg {
display: block;
margin-left: auto;
margin-right: auto;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="layers.js"></script>
<script>
var width = 500,
height = 500,
margin = 20,
stackSize = {width: 400, height: 200};
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var rootGroup = svg.append("g")
.attr("transform", "translate(" + margin + ", " + (margin + 100) + ")");
var layers = new StackedLayers(rootGroup, stackSize.width, stackSize.height);
var bottomLayer = layers.addLayer()
bottomLayer.getElement()
.append("rect")
.attr({x: 0, y: 0, width: stackSize.width, height: stackSize.height})
.style({fill: "none", stroke: "steelblue", "stroke-width": "1.5px"});
bottomLayer.getElement()
.append("polygon")
.attr("points", "250,120 285,60 320,120")
.style("fill", "red");
var textLayer = layers.addLayer();
textLayer.getElement()
.append("text")
.attr({x: 115, y: 110})
.text("Press Me")
.style({"font-size": "40px", "font-family": "sans-serif"});
textLayer.setDrawBorderInSeparatedState(true);
var circleLayer = layers.addLayer();
circleLayer.getElement()
.append("circle")
.attr({cx: 150, cy: 130, r: 30})
.style({fill: "yellow"});
circleLayer.setDrawBorderInSeparatedState(true);
var isCollapsed = true;
rootGroup.on("click", function() {
if (isCollapsed) {
layers.separateLayers();
} else {
layers.collapseLayers();
}
isCollapsed = !isCollapsed;
});
</script>
function Layer(parentElement, width, height) {
this._layerGroup = parentElement.append("g");
this._isDrawBorderInSeparatedState = false;
this._separateStateBorder = this._layerGroup.append("rect")
.attr({x: 0, y: 0, width: width, height: height})
.classed("separate-state-border", true)
.style({fill: "none", stroke: "#777", "stroke-opacity": 0, "stroke-dasharray": "5,5"});
}
Layer.prototype.getElement = function() {
return this._layerGroup;
};
Layer.prototype.getSeparateStateBorder = function() {
return this._separateStateBorder;
};
Layer.prototype.isDrawBorderInSeparatedState = function() {
return this._isDrawBorderInSeparatedState;
};
Layer.prototype.setDrawBorderInSeparatedState = function(shouldDraw) {
this._isDrawBorderInSeparatedState = shouldDraw;
};
function StackedLayers(element, width, height) {
this._element = element.append("g");
this._layers = [];
this._width = width;
this._height = height;
// TODO(vsapsai): it might be good to let configure these values.
this._separatedMinY = -50;
this._separatedMaxY = 250;
this._duration = 1000;
this._scaleFactor = 0.7;
this._skewAngleInDegrees = 20;
}
StackedLayers.prototype.addLayer = function() {
var layer = new Layer(this._element, this._width, this._height);
this._layers.push(layer);
return layer;
};
StackedLayers.prototype.separateLayers = function() {
// Introduce variables because `this` inside `forEach` callback is bound to different object.
var duration = this._duration,
scaleFactor = this._scaleFactor,
skewAngleInDegrees = this._skewAngleInDegrees,
separatedMinY = this._separatedMinY;
var xSkewingOffset = this._height * Math.sin(this._skewAngleInDegrees * (Math.PI / 180));
var layerHeightAfterSeparating = this._height * this._scaleFactor
* Math.cos(this._skewAngleInDegrees * (Math.PI / 180));
var layersCount = this._layers.length;
var newDesignatedHeight = this._separatedMaxY - this._separatedMinY;
var offsetBetweenLayers = 0;
if ((layersCount > 1) && (newDesignatedHeight > layerHeightAfterSeparating)) {
offsetBetweenLayers = (newDesignatedHeight - layerHeightAfterSeparating) / (layersCount - 1);
}
this._layers.forEach(function(layer, index) {
var layerOffset = separatedMinY + offsetBetweenLayers * (layersCount - 1 - index);
layer.getElement().transition()
.duration(duration)
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
// is indispensable for all transform crazyness
.attr("transform", [
"translate(0, " + layerOffset + ")",
"scale(1.0, " + scaleFactor + ")",
"skewX(-" + skewAngleInDegrees + ")",
"translate(" + xSkewingOffset + ", 0)"
].join(","));
if (layer.isDrawBorderInSeparatedState()) {
layer.getSeparateStateBorder().transition()
.duration(duration)
.style("stroke-opacity", 1.0);
}
});
};
StackedLayers.prototype.collapseLayers = function() {
var duration = this._duration;
this._layers.forEach(function(layer, index) {
layer.getElement().transition()
.duration(duration)
.attr("transform", [
"translate(0, " + 0 + ")",
"scale(1.0, " + 1.0 + ")",
"skewX(-" + 0 + ")",
"translate(" + 0 + ", 0)"
].join(","));
layer.getSeparateStateBorder().transition()
.duration(duration)
.style("stroke-opacity", 0.0);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment