|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<style> |
|
body { |
|
margin: 0; |
|
} |
|
.boundary, .polygon { |
|
fill: none; |
|
stroke: white; |
|
} |
|
.boundary { |
|
stroke-opacity: .6; |
|
} |
|
.polygon { |
|
stroke-opacity: .2; |
|
stroke-dasharray: 5, 5; |
|
} |
|
svg, canvas, div { |
|
position: absolute; |
|
} |
|
|
|
div { |
|
z-index: 1; |
|
background: rgba(255, 255, 255, .8); |
|
padding: 6px 12px 6px 4px; |
|
font-family: "Helvetica Neue", sans-serif; |
|
font-size: .9em; |
|
} |
|
|
|
.mask { |
|
display: none; |
|
} |
|
.mask.show { |
|
display: block; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div><input type="checkbox" checked />Mask</div> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script src="https://unpkg.com/topojson@3"></script> |
|
<script> |
|
var width = window.innerWidth, height = window.innerHeight; |
|
var canvas = d3.select("body").append("canvas").attr("width", width).attr("height", height); |
|
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height); |
|
var context = canvas.node().getContext("2d"); |
|
|
|
var projection = d3.geoOrthographic(); |
|
var path = d3.geoPath().projection(projection); |
|
|
|
var mask = svg.append("defs") |
|
.append("mask") |
|
.attr("id", "hole"); |
|
|
|
var mask_rect = mask.append("rect").style("fill", "white").attr("width", width).attr("height", height); |
|
var mask_circle = mask.append("path").datum({type: "Sphere"}).style("fill", "black"); |
|
var rect = svg.append("rect").attr("class", "mask").attr("mask", "url(#hole)").style("fill", "white").attr("width", width).attr("height", height); |
|
|
|
d3.json("countries.json", (error, world) => { |
|
if (error) throw error; |
|
|
|
var mesh = topojson.mesh(world, world.objects.land, (a, b) => a === b); |
|
var feature = topojson.feature(world, world.objects.countries); |
|
projection.fitSize([width, height], mesh); |
|
mask_circle.attr("d", path); |
|
|
|
var image = new Image; |
|
image.src = "raster.jpg"; |
|
image.onload = () => draw(); |
|
|
|
window.onresize = () => { |
|
width = window.innerWidth, height = window.innerHeight; |
|
projection.fitSize([width, height], mesh); |
|
canvas.attr("width", width).attr("height", height); |
|
svg.attr("width", width).attr("height", height); |
|
mask_rect.attr("width", width).attr("height", height); |
|
mask_circle.attr("d", path); |
|
rect.attr("width", width).attr("height", height); |
|
draw(); |
|
}; |
|
|
|
d3.timer((t) => { |
|
projection.rotate([t / 90, t / 90, t / 90]); |
|
draw(); |
|
}); |
|
|
|
rect.classed("show", true); |
|
d3.select("input").on("click", () => { |
|
rect.classed("show", d3.select("input").property("checked")); |
|
}); |
|
|
|
function draw(){ |
|
// See: https://bl.ocks.org/mbostock/4329423 |
|
context.drawImage(image, 0, 0, width, height); |
|
var sourceData = context.getImageData(0, 0, width, height).data, |
|
target = context.createImageData(width, height), |
|
targetData = target.data; |
|
for (var y = 0, i = -1; y < height; ++y) { |
|
for (var x = 0; x < width; ++x) { |
|
var p = projection.invert([x, y]), lambda = p[0], phi = p[1]; |
|
if (lambda > 180 || lambda < -180 || phi > 90 || phi < -90) { i += 4; continue; } |
|
var q = ((90 - phi) / 180 * height | 0) * width + ((180 + lambda) / 360 * width | 0) << 2; |
|
targetData[++i] = sourceData[q]; |
|
targetData[++i] = sourceData[++q]; |
|
targetData[++i] = sourceData[++q]; |
|
targetData[++i] = 255; |
|
} |
|
} |
|
context.putImageData(target, 0, 0); |
|
|
|
var polygons = svg.selectAll(".polygon") |
|
.data(feature.features); |
|
|
|
polygons.enter().append("path") |
|
.attr("class", "polygon") |
|
.merge(polygons) |
|
.attr("d", path); |
|
|
|
var boundary = svg.selectAll(".boundary") |
|
.data([mesh]); |
|
|
|
boundary.enter().append("path") |
|
.attr("class", "boundary") |
|
.merge(boundary) |
|
.attr("clip-path", "url(#clip)") |
|
.attr("d", path); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |