Skip to content

Instantly share code, notes, and snippets.

@chriswmackey
Last active October 23, 2019 20:40
Show Gist options
  • Save chriswmackey/2a7f80d0f36a7134d289ee634edd8238 to your computer and use it in GitHub Desktop.
Save chriswmackey/2a7f80d0f36a7134d289ee634edd8238 to your computer and use it in GitHub Desktop.
Direct Sun - Facade + Shade
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="solarPosition.js"></script>
<script src="intersection.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
#inputSliders { font-family:sans-serif;outline:none;margin-top:10px}
.inputgroup {border:none;}
.slider { width:400px;float:left;padding:10px;}
.slider2 { width:210px;float:left;padding:10px;}
label { float:left;font-weight:bold;padding-bottom:10px;}
input[type=range] { float:left;clear:left;margin-right:10px;width:320px;}
.slider2 input[type=range] { float:left;clear:left;margin-right:10px;width:130px;}
input[type=range]::-ms-track { background: transparent;border-color: transparent;color: transparent;-webkit-appearance: none}
input[type=range]::-webkit-slider-runnable-track { height: 5px;background:#7c7c7c; margin-top: -4px;}
input[type=range]::-webkit-slider-thumb { margin-top:-6px;}
#inputSliders p {padding-top:10px;}
</style>
</head>
<body>
<div id="inputSliders">
<form id="sliders" autocomplete="off">
<fieldset class="inputgroup">
<div class="slider" id="altitude">
<label>Month</label>
<input type="range" name="mon" id="mon" value="9" min="1" max="12" step = "1"><p id="monoutput">Sep</p></div>
<div class="slider" id="azimuth">
<label>Day</label>
<input type="range" name="day" id="day" value="21" min="1" max="31" step = "1"><p id="dayoutput">21</p></div>
<div class="slider2">
<label>Window Width</label>
<input type="range" name="width" id="width" value="5" min="1" max="15" step = "0.5"><p id="widthoutput">5 ft</p></div>
<div class="slider2">
<label>Window Height</label>
<input type="range" name="height" id="height" value="7" min="1" max="12" step = "0.5"><p id="heightoutput">7 ft</p></div>
<div class="slider2">
<label>Vertical Shade Depth</label>
<input type="range" name="vd" id="vd" value="0" min="0" max="5" step = "0.5"><p id="xoutput">0 ft</p></div>
<div class="slider2">
<label>Horizontal Shade Depth</label>
<input type="range" name="hd" id="hd" value="0" min="0" max="6" step = "0.5"><p id="youtput">0 ft</p></div>
</fieldset>
</form>
</div>
<div id="content">
</div>
<script>
// Global variables
var dimensions = {x: 60, y:30}
var gridSize = .25
var facadeY = -dimensions.y * gridSize
var increment = gridSize/2
var floorH = 3
// Create a colored grid of results.
var svgWidth = 960
var svgHeight = 400
var padding = {top: 10, left:100, right:100}
var cellDim = parseInt((svgWidth - padding.left - padding.right)/dimensions.x)
// Create a grid of points
var svg = d3.select("#content").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
var pointGrid = []
var dataset = []
for (var i = 0; i < dimensions.x; i++) {
for (var j = 0; j < dimensions.y; j++) {
pointGrid.push({x:(i*gridSize)+increment, y:-(j*gridSize)-increment, z:floorH})
svg.append("rect")
.attr("x", padding.left + cellDim*i)
.attr("y", padding.top + cellDim*j)
.attr("width", cellDim)
.attr("height", cellDim)
.attr('fill', 'red')
.style("stroke", "#000")
.style("stroke-width", "0.05em");
}
}
// Create the facade graphic
var facadeSvg = d3.select("#content").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
facadeSvg.append("rect")
.attr("x", padding.left)
.attr("y", padding.top)
.attr("width", (cellDim*dimensions.x))
.attr("height", (cellDim*15)*(1/gridSize))
.attr('fill', '#b5b5b5')
.style("stroke-width", "0.05em");
// Place the window
var WX = 5;
var WY = 3;
// Get inputs
var Mon = parseFloat($("#mon").val());
var Day = parseFloat($("#day").val());
var Width = parseFloat($("#width").val());
var Height = parseFloat($("#height").val());
var v_depth = parseFloat($("#vd").val());
var h_depth = parseFloat($("#hd").val());
// Dictionaries for specific outputs.
var monDict = {1:"Jan", 2:"Feb", 3:"Mar", 4:"Apr", 5:"May", 6:"Jun", 7:"Jul", 8:"Aug", 9:"Sep", 10:"Oct", 11:"Nov", 12:"Dec"}
// Update the display as inputs change
$("#mon").on("input", function(event) {
Mon = parseFloat($(this).val());
$("#monoutput").text(monDict[Mon]);
sunVecs = updateSunVecs(solarObject, Mon, Day)
sunResult = threeDIntersect(sunVecs, pointGrid, windowExtrusions, [], 1/timestep)
updateChart(sunResult)
});
$("#day").on("input", function(event) {
Day = parseFloat($(this).val());
$("#dayoutput").text(Day.toString());
sunVecs = updateSunVecs(solarObject, Mon, Day)
sunResult = threeDIntersect(sunVecs, pointGrid, windowExtrusions, [], 1/timestep)
updateChart(sunResult)
});
$("#width").on("input", function(event) {
Width = parseFloat($(this).val());
$("#widthoutput").text(Width.toString() + 'ft');
windowExtrusions = updateWinDim()
sunResult = threeDIntersect(sunVecs, pointGrid, windowExtrusions, [], 1/timestep)
updateChart(sunResult)
});
$("#height").on("input", function(event) {
Height = parseFloat($(this).val());
$("#heightoutput").text(Height.toString() + 'ft');
windowExtrusions = updateWinDim()
sunResult = threeDIntersect(sunVecs, pointGrid, windowExtrusions, [], 1/timestep)
updateChart(sunResult)
});
$("#wx").on("input", function(event) {
WX = parseFloat($(this).val());
$("#xoutput").text(WX.toString() + 'ft');
windowExtrusions = updateWinDim()
sunResult = threeDIntersect(sunVecs, pointGrid, windowExtrusions, [], 1/timestep)
updateChart(sunResult)
});
$("#wy").on("input", function(event) {
WY = parseFloat($(this).val());
$("#youtput").text(WY.toString() + 'ft');
windowExtrusions = updateWinDim()
sunResult = threeDIntersect(sunVecs, pointGrid, windowExtrusions, [], 1/timestep)
updateChart(sunResult)
});
// Set up solar position for Boston.
var solarObject = solarCalculator([-71, 42])
var TimeZone = -5
var offset = (new Date().getTimezoneOffset())/60
var timestep = 2
// Function to update sun vectors.
var updateSunVecs = function (solarObj, Mon, Day){
// Convert sphereical to cartesian.
var RAD2DEG = 180 / Math.PI
var DEG2RAD = Math.PI / 180
function polarToCartesian(lon, lat) {
var phi = ( 90 - lat ) * DEG2RAD
var theta = ( -lon ) * DEG2RAD
return {
x: -(Math.sin(phi) * Math.sin(theta)),
y: Math.sin(phi) * Math.cos(theta),
z: Math.cos(phi),
}
}
var dates = []
var sunvecs = []
for (i = 1; i <= 24*timestep; i++) {
hour = i/timestep
dates.push(new Date(2000, Mon-1, Day, hour - TimeZone - offset, (hour%parseInt(hour))*60))
}
for (i = 0; i < dates.length; i++) {
var posit = solarObj.position(dates[i])
if (posit[1] > 0){
sunvecs.push(polarToCartesian(posit[0], posit[1]))
}
}
return sunvecs
}
// Function to update window dimensions.
function updateWinDim(){
facadeSvg.selectAll('.window').remove();
facadeSvg.append("rect")
.attr("x", padding.left + (cellDim*WX)/gridSize)
.attr("y", padding.top + (cellDim*15) - (cellDim*WY) - (cellDim*Height))
.attr("width", cellDim*Width*(1/gridSize))
.attr("height", cellDim*Height)
.attr('fill', "#bee9ee")
.attr("class", "window");
var windowSrfs = [{xy:[[WX, facadeY], [WX+Width, facadeY]], yz:[[facadeY, WY], [facadeY, WY+Height]]}]
return windowSrfs
}
color = d3.scale.linear().domain([1,12])
.interpolate(d3.interpolateHcl)
.range([d3.rgb("#007AFF"), d3.rgb('#FFF500')]);
// Function to update the colored chart
function updateChart(dataset) {
svg.selectAll('rect')
.data(dataset)
.attr('fill', function(d){return color(d)})
}
// Run the functions to generate everything.
var sunVecs = updateSunVecs(solarObject, Mon, Day)
var windowExtrusions = updateWinDim()
var sunResult = threeDIntersect(sunVecs, pointGrid, windowExtrusions, [], 1/timestep)
updateChart(sunResult)
d3.select(self.frameElement).style("height", 960 + "px");
</script>
</body>
// Convert degrees to radians.
var RAD2DEG = 180 / Math.PI
var DEG2RAD = Math.PI / 180
// Simple 2D Vector Math
function subtract(a, b){
return [a[0]-b[0], a[1]-b[1]];
}
function dotProduct(a, b){
return a[0] * b[0] + a[1] * b[1];
}
function crossProduct(a, b){
return a[0] * b[1] - b[0] * a[1]
}
// Check for the intersection of ray and a line in 2D.
function rayLineIntersect(rayOrigin, rayDirection, point1, point2){
v1 = subtract(rayOrigin, point1);
v2 = subtract(point2, point1);
v3 = [-rayDirection[1], rayDirection[0]];
dot = dotProduct(v2, v3);
if (Math.abs(dot) < 0.000001) {
return false;
}
t1 = (crossProduct(v2, v1)) / dot;
t2 = dotProduct(v1,v3) / dot;
if (t1 >= 0.0 && (t2 >= 0.0 && t2 <= 1.0)){
return true;
}
return false;
}
// Perform intersection calculation for a grid of points and set of extrusions in three dimensions.
function threeDIntersect(sunVectors, pointGrid, windowExtrusions, shadeExtrusions, timeinterval = 1){
// Create an empty list of results for the point grid.
var results = []
for(var i = 0; i < pointGrid.length; i++) {
results.push(0)
}
// Loop through all of the vectors and extrusions to find which points see the sun
for (j = 0; j < sunVectors.length; j++) {
// Convert the sun vector into a series of 2D rays
sunVec = sunVectors[j]
xySunVec = [sunVec.x, sunVec.y]
yzSunVec = [sunVec.y, sunVec.z]
xzSunVec = [sunVec.x, sunVec.z]
for (var i = 0; i < pointGrid.length; i++) {
rayOriginxy = [pointGrid[i].x,pointGrid[i].y]
rayOriginyz = [pointGrid[i].y,pointGrid[i].z]
rayOriginxz = [pointGrid[i].x,pointGrid[i].z]
for (var k = 0; k < windowExtrusions.length; k++) {
xyIntersect = rayLineIntersect(rayOriginxy, xySunVec, windowExtrusions[k].xy[0], windowExtrusions[k].xy[1])
yzIntersect = rayLineIntersect(rayOriginyz, yzSunVec, windowExtrusions[k].yz[0], windowExtrusions[k].yz[1])
if (xyIntersect == true && yzIntersect == true){
results[i] = results[i] + timeinterval
}
}
}
}
return results
}
// Equations based on NOAA’s Solar Calculator; all angles in radians.
// http://www.esrl.noaa.gov/gmd/grad/solcalc/
(function() {
var J2000 = Date.UTC(2000, 0, 1, 12),
π = Math.PI,
τ = 2 * π,
radians = π / 180,
degrees = 180 / π;
solarCalculator = function(location) {
var longitude = location[0],
minutesOffset = 720 - longitude * 4,
λ = location[0] * radians,
φ = location[1] * radians,
cosφ = Math.cos(φ),
sinφ = Math.sin(φ);
function position(date) {
var centuries = (date - J2000) / (864e5 * 36525),
θ = solarDeclination(centuries),
cosθ = Math.cos(θ),
sinθ = Math.sin(θ),
azimuth = ((date - d3.time.day.utc.floor(date)) / 864e5 * τ + equationOfTime(centuries) + λ) % τ - π,
zenith = Math.acos(Math.max(-1, Math.min(1, sinφ * sinθ + cosφ * cosθ * Math.cos(azimuth)))),
azimuthDenominator = cosφ * Math.sin(zenith);
if (azimuth < -π) azimuth += τ;
if (Math.abs(azimuthDenominator) > 1e-6) azimuth = (azimuth > 0 ? -1 : 1) * (π - Math.acos(Math.max(-1, Math.min(1, (sinφ * Math.cos(zenith) - sinθ) / azimuthDenominator))));
if (azimuth < 0) azimuth += τ;
// Correct for atmospheric refraction.
var atmosphere = 90 - zenith * degrees;
if (atmosphere <= 85) {
var te = Math.tan(atmosphere * radians);
zenith -= (atmosphere > 5 ? 58.1 / te - .07 / (te * te * te) + .000086 / (te * te * te * te * te)
: atmosphere > -.575 ? 1735 + atmosphere * (-518.2 + atmosphere * (103.4 + atmosphere * (-12.79 + atmosphere * .711)))
: -20.774 / te) / 3600 * radians;
}
// Note: if zenith > 108°, it’s dark.
return [azimuth * degrees, 90 - zenith * degrees];
}
function noon(date) {
var centuries = (d3.time.day.utc.floor(date) - J2000) / (864e5 * 36525),
minutes = (minutesOffset - (equationOfTime(centuries + (minutesOffset - (equationOfTime(centuries - longitude / (360 * 365.25 * 100)) * degrees * 4)) / (1440 * 365.25 * 100)) * degrees * 4) - date.getTimezoneOffset()) % 1440;
if (minutes < 0) minutes += 1440;
return new Date(+d3.time.day.floor(date) + minutes * 60 * 1000);
}
return {
position: position,
noon: noon
};
};
function equationOfTime(centuries) {
var e = eccentricityEarthOrbit(centuries),
m = solarGeometricMeanAnomaly(centuries),
l = solarGeometricMeanLongitude(centuries),
y = Math.tan(obliquityCorrection(centuries) / 2);
y *= y;
return y * Math.sin(2 * l)
- 2 * e * Math.sin(m)
+ 4 * e * y * Math.sin(m) * Math.cos(2 * l)
- 0.5 * y * y * Math.sin(4 * l)
- 1.25 * e * e * Math.sin(2 * m);
}
function solarDeclination(centuries) {
return Math.asin(Math.sin(obliquityCorrection(centuries)) * Math.sin(solarApparentLongitude(centuries)));
}
function solarApparentLongitude(centuries) {
return solarTrueLongitude(centuries) - (0.00569 + 0.00478 * Math.sin((125.04 - 1934.136 * centuries) * radians)) * radians;
}
function solarTrueLongitude(centuries) {
return solarGeometricMeanLongitude(centuries) + solarEquationOfCenter(centuries);
}
function solarGeometricMeanAnomaly(centuries) {
return (357.52911 + centuries * (35999.05029 - 0.0001537 * centuries)) * radians;
}
function solarGeometricMeanLongitude(centuries) {
var l = (280.46646 + centuries * (36000.76983 + centuries * 0.0003032)) % 360;
return (l < 0 ? l + 360 : l) / 180 * π;
}
function solarEquationOfCenter(centuries) {
var m = solarGeometricMeanAnomaly(centuries);
return (Math.sin(m) * (1.914602 - centuries * (0.004817 + 0.000014 * centuries))
+ Math.sin(m + m) * (0.019993 - 0.000101 * centuries)
+ Math.sin(m + m + m) * 0.000289) * radians;
}
function obliquityCorrection(centuries) {
return meanObliquityOfEcliptic(centuries) + 0.00256 * Math.cos((125.04 - 1934.136 * centuries) * radians) * radians;
}
function meanObliquityOfEcliptic(centuries) {
return (23 + (26 + (21.448 - centuries * (46.8150 + centuries * (0.00059 - centuries * 0.001813))) / 60) / 60) * radians;
}
function eccentricityEarthOrbit(centuries) {
return 0.016708634 - centuries * (0.000042037 + 0.0000001267 * centuries);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment