Built with blockbuilder.org
forked from chriswmackey's block: Explore Facade Options
forked from chriswmackey's block: 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
| <!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>Window X</label> | |
| <input type="range" name="wx" id="wx" value="4" min="0" max="15" step = "0.5"><p id="xoutput">4 ft</p></div> | |
| <div class="slider2"> | |
| <label>Window Y</label> | |
| <input type="range" name="wy" id="wy" value="3" min="0" max="15" step = "0.5"><p id="youtput">3 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)) | |
| .attr('fill', '#b5b5b5') | |
| .style("stroke-width", "0.05em"); | |
| // Get inputs | |
| var Mon = parseFloat($("#mon").val()); | |
| var Day = parseFloat($("#day").val()); | |
| var Width = parseFloat($("#width").val()); | |
| var Height = parseFloat($("#height").val()); | |
| var WX = parseFloat($("#wx").val()); | |
| var WY = parseFloat($("#wy").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); | |
| } | |
| })(); |