“Instead of cursing the darkness, light a candle.”
Benjamin Franklin
The following project was an experiment in visualizing light qualities, and modeling using three different designs by analyzing a bust of Benjamin Franklin. The artwork was created by French Artist Jean Antoine Houdon in 1778. The project utilizes D3.js, Vanilla Javascript, and generated CSV files to assemble the images.
Method 1: Value Map
Using a custom script, I was able to artificially create X, Y, Z points from many images and angles of the bust. Each point consists of the light and dark value, and its locatation based on pixel position.
Method 2: Position of Values Across Surface
This plot is a flattened diagram of all the points (X,Y pairs), and a graphical representation of their light values(Z). Each thread is one point in the image. The model below represents the image above.
X
Y
Z
Method 3: Contour and Density
The last plot is a density diagram of light masses (shadow and light).
ROUGH WORKING CODE RESPONSIBLE FOR PROJECT
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
body{
background: #d2c2af;
}
.axis path{
stroke: none;
stroke-width: 4;
}
.axis line{
stroke: none;
}
.axis text{
visibility:hidden;
}
.axisp path{
stroke: black;
stroke-width: 4;
}
.axisp line{
stroke: black;
}
.axisp text{
visibility:hidden;
}
</style>
</head>
<body>
<button type="button" id = "download">Image</button>
<canvas width="4000" height="4000" id = "one"></canvas>
<div style="width: 8000px;">
<div id = "my_dataviz" style="float: left; width: 4000px;" ></div>
<div id = "my_dataviz2" style="float: left; width: 4000px;" ></div>
<br style="clear: left;" />
<div id = "my_dataviz5" ></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/2.5.0/stackblur.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/d3-contour.v1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/save-svg-as-png/1.4.17/saveSvgAsPng.js"></script>
<script>
const canvas = document.querySelector("#one"),
context = canvas.getContext("2d"),
img = new Image(),
width = 1600,
height = 2000,
threshold = 0.99,
worker = new Worker("worker.js");
context.fillStyle = "#444";
img.onload = function() {
// Draw image
context.drawImage(img, 0, 0, width, height);
// Blur image to smooth it out
StackBlur.canvasRGBA(canvas, 0, 0, width, height, 5);
const density = getDensityFunction(context);
context.drawImage(img, 0, 0, width, height);
// Get initial points
const points = generatePoints(density, 45000);
console.log(points)
// let csvContent = "data:text/csv;charset=utf-8,";
// points.forEach(function(rowArray) {
// let row = rowArray.join(",");
// csvContent += row + "\r\n";
// });
// var encodedUri = encodeURI(csvContent);
// window.open(encodedUri);
worker.onmessage = event => draw(event.data);
// Compute in worker
worker.postMessage({ density, points, width, height, threshold });
};
img.src = "quarterLeft.jpg";
// Draw the points
function draw(points) {
context.clearRect(width, 0, width, height);
points.forEach(function(point) {
context.beginPath();
if (point.r) {
context.arc(width + point[0], point[1], point.r, 0, 2 * Math.PI);
context.fill();
}
});
}
function generatePoints(density, numPoints) {
// Generate starting points with rejection sampling against pixel brightness
return d3.range(numPoints).map(function() {
let x, y, d;
while (true) {
x = Math.random() * width;
y = Math.random() * height;
d = density[width * Math.floor(y) + Math.floor(x)];
if (Math.random() > d) {
return [x, y,d];
}
}
});
}
// Convert imageData into an array of brightness values from 0-1
function getDensityFunction(context) {
const data = context.getImageData(0, 0, width, height).data;
return d3.range(0, data.length, 4).map(i => data[i] / 255);
}
// set the dimensions and margins of the graph
var margin = {top: 10, right: 20, bottom: 30, left: 50},
width2 = 4000 - margin.left - margin.right,
height2 = 4000 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width2 + margin.left + margin.right)
.attr("height", height2 + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// append the svg object to the body of the page
var svg3 = d3.select("#my_dataviz2")
.append("svg")
.attr("width", width2 + margin.left + margin.right)
.attr("height", height2 + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// append the svg object to the body of the page
var svg6 = d3.select("#my_dataviz5")
.append("svg")
.attr("width", width2 + margin.left + margin.right)
.attr("height", height2 + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
//Read the data
d3.csv("quarterLeft.csv", function(data) {
var myColor = d3.scaleSequential()
.interpolator(d3.interpolateGreys)
.domain([1,0])
// Add X axis
var x = d3.scaleLinear()
.domain([0, 1000])
.range([ 0, width2 ])
svg.append("g")
.attr("transform", "translate(0," + height2 + ")")
.call(d3.axisBottom(x))
.attr("class", "axis")
svg3.append("g")
.attr("transform", "translate(0," + height2 + ")")
.call(d3.axisBottom(x))
.attr("class", "axis")
// Add Y axis
var y = d3.scaleLinear()
.domain([1000, 0])
.range([ height2, 0])
svg.append("g")
.call(d3.axisLeft(y))
.attr("class", "axis")
svg3.append("g")
.call(d3.axisLeft(y))
.attr("class", "axis")
// Add a scale for bubble size
var z = d3.scaleLinear()
.domain([0, 1])
.range([ 0, 10]);
// svg.selectAll("dot")
// .data(data)
// .enter().append("text")
// .attr("x", function(d) { return x(d.x); })
// .attr("y", function(d) { return y(d.y); })
// .style("font-family", "Arial")
// .style("font-size", 8)
// .style("fill", "lightgray")
// .text(function(d) { return d.x })
// .attr("stroke", "none")
svg3.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", .1)
.attr("d", d3.line().curve(d3.curveCardinalClosed)
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) })
)
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", .1)
.attr("d", d3.line().curve(d3.curveStepBefore)
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) })
)
data.sort(function(b, a) {
return a.z - b.z;
});
// compute the density data
var densityData = d3.contourDensity()
.x(function(d) { return x(d.x); }) // x and y = column name in .csv input data
.y(function(d) { return y(d.y); })
.size([width2, height2])
.bandwidth(40) // smaller = more precision in lines = more lines
(data)
// Add dots
svg3.append('g')
.selectAll("dot")
.data(data)
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.x); } )
.attr("cy", function (d) { return y(d.y); } )
.attr("r", function (d) { return z(d.z); } )
.style("fill", function(d){ return(myColor(d.z))})
// // Add dots
// svg.append('g')
// .selectAll("dot")
// .data(data)
// .enter()
// .append("rect")
// .attr("x", function (d) { return x(d.x); } )
// .attr("y", function (d) { return y(d.y); } )
// .attr("width", 100 )
// .attr("height", .1)
// .style("stroke", "black")
// .style("fill", "black")
// Add dots
svg.append('g')
.selectAll("dot")
.data(data)
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.x); } )
.attr("cy", function (d) { return y(d.y); } )
.attr("r", 5 )
.style("fill", 'black')
// Add the contour: several "path"
svg
.selectAll("path")
.data(densityData)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", "none")
.attr("stroke", "black")
.attr("opacity", .8)
.attr("stroke-width", 5)
.attr("stroke-linejoin", "round")
data.sort(function(b, a) {
return a.z - b.z;
});
// Extract the list of dimensions we want to keep in the plot. Here I keep all except the column called Species
dimensions = d3.keys(data[0])
// For each dimension, I build a linear scale. I store all in a y object
var y3 = {}
for (i in dimensions) {
name = dimensions[i]
y[name] = d3.scaleLinear()
.domain( d3.extent(data, function(d) { return +d[name]; }) )
.range([height2, 0])
}
// Build the X scale -> it find the best position for each Y axis
x3 = d3.scalePoint()
.range([0, width2])
.domain(dimensions);
// The path function take a row of the csv as input, and return x and y coordinates of the line to draw for this raw.
function path(d) {
return d3.line()(dimensions.map(function(p) { return [x3(p), y[p](d[p])]; }));
}
// Draw the lines
svg6
.selectAll("myPath")
.data(data)
.enter().append("path")
.attr("d", path)
.style("stroke", function(d){ return(myColor(d.z))})
.style("fill", 'none')
.style("stroke-width", .3)
.style("opacity", 1)
// Draw the axis:
svg6.selectAll("myAxis")
// For each dimension of the dataset I add a 'g' element:
.data(dimensions).enter()
.append("g")
// I translate this element to its right position on the x axis
.attr("transform", function(d) { return "translate(" + x3(d) + ")"; })
.attr("class", "axisp")
// And I build the axis with the call function
.each(function(d) { d3.select(this).call(d3.axisLeft().scale(y[d])); })
// Add axis title
.append("text")
.style("text-anchor", "middle")
.attr("y", 0)
.text(function(d) { return d; })
.style("fill", "black")
});
d3.select("#download")
.on('click', function(){
// Get the d3js SVG element and save using saveSvgAsPng.js
saveSvgAsPng(document.getElementsByTagName("svg")[0], "plot.png", {scale: 2, backgroundColor: 'none'});
saveSvgAsPng(document.getElementsByTagName("svg")[1], "plot.png", {scale: 2, backgroundColor: 'none'});
saveSvgAsPng(document.getElementsByTagName("svg")[2], "plot.png", {scale: 2, backgroundColor: 'none'});
saveSvgAsPng(document.getElementsByTagName("svg")[3], "plot.png", {scale: 2, backgroundColor: 'none'});
})
</script>