Built with blockbuilder.org
forked from chriswmackey's block: Explore Facade Options
forked from chriswmackey's block: Direct Sun Through a Facade
forked from chriswmackey's block: Daily Direct Sun Through a Facade
license: mit |
Built with blockbuilder.org
forked from chriswmackey's block: Explore Facade Options
forked from chriswmackey's block: Direct Sun Through a Facade
forked from chriswmackey's block: Daily Direct Sun Through a Facade
<!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); | |
} | |
})(); |