Skip to content

Instantly share code, notes, and snippets.

@dwtkns
Last active May 28, 2019 06:59
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dwtkns/2e2083c0114d1f970232 to your computer and use it in GitHub Desktop.
Save dwtkns/2e2083c0114d1f970232 to your computer and use it in GitHub Desktop.
Sun and Earth

Experiments faking 3d rotation of a globe and sun in SVG. Illusion is helped by dynamic gradients, two nested orthographic projections, scaling the sun circle's radius based on distance from the 'camera', and toggling a clipping mask that makes the sun appear to move "behind" the earth.

Based on Mike Bostock's Solar Terminator and this past experiment.

This example stems from drafts of maps for Norway the Slow Way. Also see Mike's Pencil Sketch example.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg { stroke-linejoin: round; }
.shell {
width:960px;
position:relative;
}
.tally {
width:100%;
margin: 0 auto;
text-align:center;
font-family: sans-serif;
color:#bbb;
position: absolute;
top: 60px;
}
/*map*/
.horizon {
fill: none;
stroke: #eee;
opacity: 0.5;
stroke-width: 2px;
}
.ocean {
fill: url(#oceanGradient);
}
.graticule {
fill: none;
stroke: #aaa;
stroke-width: .5px;
stroke-opacity: .1;
}
.land {
stroke-opacity:0;
stroke: #2e414c;
stroke-width: .5px;
fill: #fcfcff;
fill-opacity:1;
}
.land.thickhalo {
opacity: 1;
fill:none;
stroke-opacity:0.04;
stroke: #2e414c;
stroke-width: 8px;
}
.land.thinhalo {
opacity: 1;
fill:none;
stroke-opacity:0.1;
stroke: #2e414c;
stroke-width: 2.5px;
}
.night {
stroke: none;
stroke-width: 3px;
stroke-opacity: 0.5;
fill: #1f2a33;
fill: url(#nightGradient);
}
.sun {
fill: url(#sunGradient);
stroke: #ffc800;
stroke-opacity:0.2;
transition:fill .3s, stroke .3s;
}
.sun {
mask: url(#globeMask);
}
/* */
#globeMask rect { fill: #fff; }
/*
the opacity of the mask's circle
determines whether the shape of the globe
shows up in the opacity mask or not
so, by 'toggling' the circle as part of the mask
we can choose when to occlude an element using
the globe's shape, creating a false 3d effect
when an element passes 'behind' the globe.
the globeMaskCircle's fill color determines the extent to which
it masks out the things 'behind' it. #000 = fully hidden
*/
#globeMaskCircle {
fill: #000;
opacity: 0;
}
#globeMaskCircle.behind {
opacity: 1;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<body>
<div class="shell">
<div class="tally"></div>
<svg width="960" height="500" id="globe"></svg>
</div>
<script>
// begin code for your graphic here:
var mapW = 960,
mapH = 500;
// utility
var π = Math.PI,
radians = π / 180,
degrees = 180 / π;
var parseDate = d3.time.format("%Y-%m-%d-%H-%Z").parse,
logDate = d3.time.format("%B %e");
// map setup
var earth = d3.geo.orthographic()
.scale(0.25 *mapH)
.translate([mapW /2, mapH /2])
.clipAngle(90)
// .rotate([-8, -61, 0])
.rotate([10, 150, 23.4])
.precision(.1);
var sky = d3.geo.orthographic()
.scale(0.49 *mapH)
.translate(earth.translate())
.clipAngle(earth.clipAngle())
.rotate(earth.rotate())
.precision(earth.precision());
var circle = d3.geo.circle()
.angle(90);
var path = d3.geo.path()
.projection(earth);
var skypath = d3.geo.path()
.projection(sky);
var graticule = d3.geo.graticule();
var svg = d3.select("svg")
.attr('class','map')
.attr("width", mapW)
.attr("height", mapH)
.attr('viewBox', '' + 0 + ' ' + 0 + ' ' + mapW + ' ' + mapH );
var defs = svg.append("defs");
var sunScale = d3.scale.linear().domain([0,1]).range([5,25]);
var jun6 = parseDate('2014-06-01-00--0000');
// read data files
queue()
.defer(d3.json, "/d/4090846/world-110m.json")
.await(ready);
function ready(error, world) {
var num = 0;
var start = earth.rotate();
d3.timer(function() {
var dt = addMinutes(jun6,num);
d3.select('.tally').text(logDate(dt))
var newRot = [
(num/1440/0.8)+start[0],
start[1],
start[2]
];
earth.rotate(newRot);
sky.rotate(newRot)
redraw(dt)
num+=1440; // increment time in minutes (1440 = 1 day)
});
buildDefs();
buildGlobe();
function buildDefs() {
// draw map
defs.append("path")
.datum({type: "Sphere"})
.attr("id", "sphere")
.attr("d", path);
defs.append("clipPath")
.attr("id", "clip")
.append("use")
.attr("xlink:href", "#sphere");
var globeMask = defs.append('mask')
.attr('id','globeMask')
.attr('x',0).attr('y',0)
.attr('width',mapW)
.attr('height',mapH)
globeMask.append('rect')
.attr('x',0).attr('y',0)
.attr('width',mapW)
.attr('height',mapH)
globeMask.append('use')
.attr('id','globeMaskCircle')
.attr("xlink:href", "#sphere");
var sunGradient = defs.append("radialGradient")
.attr("id", "sunGradient")
.attr("cx", "75%")
.attr("cy", "25%");
sunGradient.append("stop").attr("offset", "5%" ).attr("stop-color", "#fffffa");
sunGradient.append("stop").attr("offset", "150%").attr("stop-color", "#fdf2ce");
var oceanGradient = defs.append("radialGradient")
.attr("id", "oceanGradient")
.attr("cx", "75%")
.attr("cy", "25%");
oceanGradient.append("stop").attr("offset", "5%" ).attr("stop-color", "#fff");
oceanGradient.append("stop").attr("offset", "150%").attr("stop-color", "#d4dfe8");
var nightGradient = defs.append("radialGradient")
.attr("id", "nightGradient")
.attr("r", "110%")
.attr("cx", "75%")
.attr("cy", "25%");
nightGradient.append("stop").attr("offset", "0%" ).attr("stop-color", "rgba(31, 42, 51,0.3)");
nightGradient.append("stop").attr("offset", "100%").attr("stop-color", "rgba(31, 42, 51,0.0)");
}
function buildGlobe() {
var landMasses = topojson.feature(world, world.objects.land);
svg.append("use")
.attr("class", "horizon")
.attr("xlink:href", "#sphere");
svg.append("use")
.attr("class", "ocean")
.attr("xlink:href", "#sphere");
svg.append("path")
.datum(graticule)
.attr("class", "graticule");
svg.append("path")
.datum(landMasses)
.attr("class", "land thickhalo");
svg.append("path")
.datum(landMasses)
.attr("class", "land thinhalo");
svg.append("path")
.datum(landMasses)
.attr("class", "land");
svg.append("path").attr("class", "night");
d3.selectAll('.land, .night')
.attr("clip-path", "url(#clip)");
svg.append('circle').datum([])
.attr('class','sun');
}
function redraw(date) {
date = date || new Date;
var night = d3.selectAll('.night'),
sunshine = d3.selectAll('.sun'),
globeMaskCircle = d3.selectAll('#globeMaskCircle');
var sunPos = solarPosition(date),
antiSunPos = antipode(sunPos);
var noon = {
earth: earth(sunPos),
sky: sky(sunPos)
}
var midnight = {
earth: earth(antiSunPos),
sky: sky(antiSunPos)
}
night.datum(circle.origin(antiSunPos)).attr("d", path);
d3.select('#nightGradient')
/*
this nastiness roughly calculates gradient center as a % of the shape it's being applied to
based on where the projected sun 'location' falls within the viewport.
*/
.attr('cx', ((midnight.earth[0]-(mapW-mapH)/2)/mapH*100)+'%')
.attr('cy', (midnight.earth[1]/mapH*100)+'%')
d3.select('#oceanGradient')
.attr('cx', ((noon.earth[0]-(mapW-mapH)/2)/mapH*100)+'%')
.attr('cy', (noon.earth[1]/mapH*100)+'%')
var sunSizeFactor = 1-(degrees_from_center(sunPos)/180);
sunshine
.attr("cx", noon.sky[0])
.attr("cy", noon.sky[1])
.attr('r', sunScale(sunSizeFactor));
d3.selectAll('.land').attr('d',path);
d3.selectAll('.graticule').attr("d", path);
globeMaskCircle
.classed('behind',function(){
// "behindGlobe" meaning 'appearing more distant from the viewer than the globe'
return isSunBehindGlobe = degrees_from_center(sunPos) > 90 ? true : false;;
})
}
}
function degrees_from_center(d) {
var distanceBetween = d3.geo.distance,
centerPos = [
-earth.rotate()[0],
-earth.rotate()[1]
];
return distanceBetween(d,centerPos)*degrees;
}
function fade_at_edge(d) {
var sunPos = d;
// distance in degrees from centerpoint of globe rotation
// to pt "underneath" the sun
var distance = degrees_from_center(sunPos)
// degrees on either side of 90 during which fading of the revolving 'sun' happens
var fade_range = 10;
var fade = d3.scale.linear()
.domain([90+fade_range,90-fade_range])
.range([0.1,1]) // opacity range
.clamp(true)
return fade(distance)
}
// http://stackoverflow.com/questions/1197928/how-to-add-30-minutes-to-a-javascript-date-object
function addMinutes(date, minutes) {
return new Date(date.getTime() + minutes*60000);
}
// from http://bl.ocks.org/mbostock/4597134
function antipode(position) {
return [position[0] + 180, -position[1]];
}
function solarPosition(time) {
var centuries = (time - Date.UTC(2000, 0, 1, 12)) / 864e5 / 36525, // since J2000
longitude = (d3.time.day.utc.floor(time) - time) / 864e5 * 360 - 180;
return [
longitude - equationOfTime(centuries) * degrees,
solarDeclination(centuries) * degrees
];
}
// Equations based on NOAA’s Solar Calculator; all angles in radians.
// http://www.esrl.noaa.gov/gmd/grad/solcalc/
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);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment