An experiment in translating from SVG into WebGL (via Three.js).
From a class project that involved a creative re-creation of the work of Piet Mondrian.
(Originally written in CoffeeScript, so the code is a little weird.)
An experiment in translating from SVG into WebGL (via Three.js).
From a class project that involved a creative re-creation of the work of Piet Mondrian.
(Originally written in CoffeeScript, so the code is a little weird.)
<!doctype html> | |
<html> | |
<script id="vertexShader" type="x-shader/x-vertex"> | |
precision mediump int; | |
uniform vec4 my_color; | |
varying vec3 vPosition; | |
varying vec4 vColor; | |
varying vec4 vProjected; | |
void main() { | |
vec3 pos = position; | |
vPosition = pos; | |
vColor = my_color; | |
vec4 projected = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 ); | |
vProjected = projected; | |
gl_Position = projected; | |
} | |
</script> | |
<script id="fragmentShader" type="x-shader/x-fragment"> | |
precision mediump float; | |
precision mediump int; | |
varying vec3 vPosition; | |
varying vec4 vColor; | |
varying vec4 vProjected; | |
void main() { | |
vec4 color; | |
// color = vec4( vColor ); | |
// color = vec4( vPosition, 0.5 ); | |
color = vProjected; | |
// gl_FragColor = color; | |
gl_FragColor = vec4( vColor ); | |
} | |
</script> | |
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/r72/three.min.js'></script> | |
<script src='http://d3js.org/d3.v3.min.js'></script> | |
<script> | |
var size = { | |
width: 300, | |
height: 500, | |
depth: 100 | |
}; | |
var cellSize = 100; | |
var rows = Math.floor(size.height / cellSize); | |
var columns = Math.floor(size.width / cellSize); | |
var n = rows * columns; | |
var depthLevels = 5; | |
var colors = d3.range(4); | |
var color = d3.scale | |
.ordinal() | |
.domain(colors) | |
.range(['#a0a0a0', '#f0d050', '#e03040', '#4080c0']); | |
var data = []; | |
function addData() { | |
var newData; | |
newData = d3.range(n).map(function(d, i) { | |
return { | |
column: i % columns, | |
row: Math.floor(i / columns), | |
depth: d3.shuffle(d3.range(depthLevels))[0], | |
color: d3.shuffle(colors)[0] | |
}; | |
}); | |
data = data.concat(newData); | |
}; | |
var body = d3.select('html') | |
.selectAll('body') | |
.data([1]) | |
.enter() | |
.append('body'); | |
translate = function(x, y) { | |
return "translate(" + x + "," + y + ")"; | |
}; | |
var scene = new THREE.Scene(); | |
var camera = new THREE.PerspectiveCamera(void 0, size.width / size.height); | |
var renderer = new THREE.WebGLRenderer({ | |
antialias: true | |
}); | |
renderer.setSize(size.width, size.height); | |
renderer.setClearColor('#ffffff', 1); | |
renderer.sortObjects = true; | |
body.append(function() { | |
return renderer.domElement; | |
}); | |
var fragment = window.fragment = document.createDocumentFragment(); | |
var svg = body.append('svg').attr(size); | |
var frame = (function() { | |
var g, m, mesh; | |
g = new THREE.PlaneGeometry(size.width, size.height); | |
m = new THREE.MeshBasicMaterial({ | |
color: 0x0 | |
}); | |
m.transparent = true; | |
m.opacity = 0.05; | |
mesh = new THREE.Mesh(g, m); | |
scene.add(mesh); | |
return mesh; | |
})(); | |
var bordersObj = new THREE.Object3D(); | |
bordersObj.position.z = 100; | |
scene.add(bordersObj); | |
var frameScale = (function() { | |
var a, arr; | |
a = [[size.width, 1], [size.height, -1]]; | |
arr = a.map(function(a) { | |
return d3.scale.linear() | |
.domain([0, a[0]]) | |
.range([-a[0] / 2 * a[1], a[0] / 2 * a[1]]); | |
}); | |
return { | |
x: arr[0], | |
y: arr[1] | |
}; | |
})(); | |
var random = Math.random; | |
var normal = d3.random.normal(0, cellSize * .2); | |
var vs = document.getElementById('vertexShader').textContent; | |
var fs = document.getElementById('fragmentShader').textContent; | |
var grid = (function() { | |
var arr; | |
arr = [[columns, size.width], [rows, size.height]].map(function(_) { | |
return d3.scale.ordinal() | |
.domain(d3.range(_[0])) | |
.rangeBands([0, _[1]], 0.5, 0.5); | |
}); | |
return { | |
x: arr[0], | |
y: arr[1] | |
}; | |
})(); | |
grid.z = d3.scale.ordinal() | |
.domain(d3.range(depthLevels)) | |
.rangePoints([10, size.depth]); | |
function addRect(cells) { | |
return cells.append('rect').classed('mainRect', true).attr({ | |
'width': grid.x.rangeBand(), | |
'height': grid.y.rangeBand() | |
}).style({ | |
'fill-opacity': 0.9, | |
'fill': '#888888' | |
}).each(function(d, i) { | |
var g, h, material, mesh, r, ref, w; | |
r = d3.select(this); | |
ref = ['width', 'height'].map(function(_) { | |
return r.attr(_); | |
}), w = ref[0], h = ref[1]; | |
g = new THREE.PlaneGeometry(w, h); | |
material = new THREE.ShaderMaterial({ | |
polygonOffset: true, | |
polygonOffsetFactor: i * 10, | |
uniforms: { | |
time: { | |
type: "f", | |
value: 1.0 | |
}, | |
my_color: { | |
type: "v4", | |
value: new THREE.Vector4(1.0, 0.4, 1.0, 1.0) | |
} | |
}, | |
vertexShader: vs, | |
fragmentShader: fs | |
}); | |
mesh = new THREE.Mesh(g, material); | |
mesh.position.x += w / 2; | |
mesh.position.y -= h / 2; | |
mesh.name = 'rect'; | |
return d.object3D.add(mesh); | |
}); | |
}; | |
function updateCells() { | |
var cells, i, transition; | |
cells = svg.selectAll('.cell').data(data); | |
transition = cells.enter() | |
.append('g') | |
.classed('cell', true) | |
.each(function(d) { | |
d.object3D = new THREE.Object3D(); | |
return frame.add(d.object3D); | |
}) | |
.call(addRect).attr({ | |
transform: translate(size.width / 2, size.height / 2), | |
'data-depth': grid.z(0) | |
}) | |
.transition() | |
.duration(500) | |
.transition() | |
.duration(300) | |
.attr('transform', function(d) { | |
return translate(grid.x(d.column), size.height / 2); | |
}) | |
.transition() | |
.attr('transform', function(d) { | |
return translate(grid.x(d.column), grid.y(d.row)); | |
}) | |
.transition() | |
.call(function(t) { | |
var rect; | |
rect = t.selectAll('rect'); | |
return rect.style('fill', function(d) { | |
return color(d.color); | |
}); | |
}) | |
.transition() | |
.attr('data-depth', function(d) { | |
return grid.z(d.depth) + normal() * 2; | |
}) | |
.transition() | |
.call(function(t) { | |
t.select('rect') | |
.each(function(d) { | |
d._width = parseInt(d3.select(this).attr('width')); | |
d._height = parseInt(d3.select(this).attr('height')); | |
}) | |
.attr({ | |
width: function(d) { | |
return Math.max(0, d._width + normal()); | |
}, | |
height: function(d) { | |
return Math.max(0, d._height + normal()); | |
} | |
}); | |
}); | |
i = 0; | |
return new Promise(function(resolve) { | |
return transition | |
.each("end", function(d) { | |
var m; | |
m = this.transform.baseVal[0].matrix; | |
d3.select(this) | |
.transition() | |
.duration(300) | |
.attr('transform', function(d) { | |
return translate(m.e + normal(), m.f + normal()); | |
}) | |
.each("end", function() { | |
if (++i === transition.size()) { | |
return resolve(); | |
} | |
}); | |
}); | |
}); | |
}; | |
var maxLightness = 1; | |
var normalizePosition = { | |
x: d3.scale.linear() | |
.domain([0, size.width / 2, size.width]) | |
.range([maxLightness, 0, maxLightness]), | |
y: d3.scale.linear() | |
.domain([0, size.height / 2, size.height]) | |
.range([maxLightness, 0, maxLightness]) | |
}; | |
var borderRectWidth = 4; | |
function addBorders(d) { | |
var g = d3.select(this); | |
var rect = g.select('rect'); | |
var material = new THREE.ShaderMaterial({ | |
polygonOffset: true, | |
polygonOffsetFactor: -500, | |
uniforms: { | |
time: { | |
type: "f", | |
value: 1.0 | |
}, | |
my_color: { | |
type: "v4", | |
value: new THREE.Vector4(0, 0, 0, 1.0) | |
} | |
}, | |
vertexShader: vs, | |
fragmentShader: fs | |
}); | |
var rectObj = d.object3D.getObjectByName('rect'); | |
var ref = ["width", "height"] | |
.map(function(dim) { | |
return rect.attr(dim); | |
}), w = ref[0], h = ref[1]; | |
var bord = borderRectWidth; | |
var arr = [ | |
[w, bord, 0, +h / 2, 'top'], | |
[w, bord, 0, -h / 2, 'bottom'], | |
[bord, h, -w / 2, 0, 'left'], | |
[bord, h, +w / 2, 0, 'right'] | |
]; | |
return arr.forEach(function(args) { | |
var geometry, height, mesh, width, x, y; | |
if (Math.random() < 0.9) { | |
return; | |
} | |
width = args[0], height = args[1], x = args[2], y = args[3]; | |
geometry = new THREE.PlaneGeometry(width, height); | |
mesh = new THREE.Mesh(geometry, material); | |
mesh.name = 'border'; | |
mesh.position.x = x === 0 ? normal() : x; | |
mesh.position.y = y === 0 ? normal() : y; | |
mesh.position.z = 5; | |
return rectObj.add(mesh); | |
}); | |
}; | |
function repeat() { | |
addData(); | |
return updateCells() | |
.then(function() { | |
svg.selectAll('.cell').transition().each(function(d) { | |
var m, ref, rgb, x, y; | |
m = this.transform.baseVal.getItem(0).matrix; | |
ref = [m.e, m.f], x = ref[0], y = ref[1]; | |
rgb = d3.rgb(color(d.color)); | |
return d3.select(this).select('rect') | |
.style('fill', rgb.toString()); | |
}); | |
return svg.selectAll('.cell').each(addBorders); | |
}); | |
}; | |
var repetitions = 10; | |
d3.range(repetitions).reduce(function(current, next) { | |
return current.then(repeat); | |
}, Promise.resolve()); | |
camera.position.z = 700; | |
camera.position.x = -700; | |
camera.lookAt(new THREE.Vector3(0, 0, 0)); | |
function render() { | |
requestAnimationFrame(render); | |
svg.selectAll('.cell').each(function(d, i) { | |
var cell, colorArray, colorVec, h, m, rect, rectObj, ref, w; | |
cell = d3.select(this); | |
rect = cell.select('.mainRect'); | |
rectObj = d.object3D.getObjectByName('rect'); | |
colorArray = new THREE.Color(rect.node().style.fill).toArray(); | |
colorVec = new THREE.Vector4().fromArray(colorArray.concat(1.0)); | |
rectObj.material.uniforms.my_color.value = colorVec; | |
rectObj.geometry.dispose(); | |
ref = ['width', 'height'].map(function(dim) { | |
return rect.attr(dim); | |
}), w = ref[0], h = ref[1]; | |
rectObj.geometry = new THREE.PlaneGeometry(w, h); | |
m = this.transform.baseVal.getItem(0).matrix; | |
d.object3D.position.x = frameScale.x(m.e); | |
d.object3D.position.y = frameScale.y(m.f); | |
return d.object3D.position.z = cell.attr('data-depth'); | |
}); | |
return renderer.render(scene, camera); | |
}; | |
render(); | |
</script> | |
</html> |