This is an experiment to see what’s the most performant way of animating hundreds of lines over time.
This one uses canvas to draw the lines. Every tick, the pre-projected lines are sliced by a given pct, and then drawn. This approach is quite slow, and the bottleneck is the thousands of context.lineTo
/ context.moveTo
canvas calls, not the line interpolation.
Use the slider to manually adjust the line lengths.
Also see Line interpolation with SVG
import setupCanvas from './setupCanvas.js'
import getFeatures from './getFeatures.js'
import draw from './draw.js'
import interpolateLineStrings from './interpolateLineStrings.js'
// Use body's offset width as the canvas overall width.
const width = document.body.offsetWidth
d3.json('./all.json', topology => {
// Get geo features.
const { all, ocean, lines } = getFeatures(topology)
// Create a null path.
const path = d3.geoPath()
// Use the null path to get overall topology aspect.
const b = path.bounds(all)
const aspect = (b[1][0] - b[0][0])/(b[1][1] - b[0][1])
// Set height as a multiple of width.
const height = width / aspect
// Use geoIdentity so we can use fitSize.
const projection = d3.geoIdentity().fitSize([width, height], all)
// Set the projection to geoIdentity.
path.projection(projection)
// Setup canvas.
const { ctx, canvas, dpRatio } = setupCanvas({
container: document.body,
width,
height,
})
// Set canvas context on path.
path.context(ctx)
// Clear the canvas.
draw.reset({ dpRatio, canvas, ctx })
// Draw ocean.
draw.ocean({ ctx, ocean, path })
const drawLines = pct => {
// Clear the canvas.
draw.reset({ dpRatio, canvas, ctx })
// Draw the ocean.
draw.ocean({ ctx, ocean, path })
// Slice all the lines by a given pct.
const slices = interpolateLineStrings({
lines,
pct,
})
// Draw the lines.
draw.lines({
ctx,
path,
lines: slices,
})
}
// Set the duration of a complete loop.
const duration = 4000
let resetTime = 0
const timer = d3.timer((elapsed) => {
let delta = elapsed - resetTime
if (delta > duration) {
resetTime = elapsed
delta = elapsed - resetTime
}
const pct = delta / duration
const adjustedPct = pct < 0.5 ? 2 * pct : 2 * (1 - pct)
drawLines(adjustedPct)
})
d3.select('input').on('input', function() {
timer.stop()
const pct = this.value
drawLines(pct)
})
})
<!DOCTYPE html>
<title>blockup</title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<link href='dist.css' rel='stylesheet' />
<body>
<canvas></canvas>
<script src='d3.v4.min.js'></script>
<script src='topojson.min.js'></script>
<script src='dist.js'></script>
<input type='range' min=0 max=1 step=0.01 value=0.5>
</body>
W := 960
H := 500
bin := ./node_modules/.bin
all: clean all.json
clean:
rm -rf output
output/ne_110m_ocean.zip:
mkdir -p $(dir $@)
curl -Lo $@ http://naciscdn.org/naturalearth/110m/physical/$(notdir $@)
output/ne_110m_ocean.shp: output/ne_110m_ocean.zip
unzip -od $(dir $@) $<
touch $@
output/ocean.json: output/ne_110m_ocean.shp
${bin}/shp2json -n $< \
| ${bin}/geostitch -n \
> $@
ls -lh $@
output/lines.json:
cp $(notdir $@) $@
all.json: output/ocean.json output/lines.json
bash -c "${bin}/geo2topo -n \
all=<(cat $^ | ${bin}/geoproject 'd3.geoMollweide()' -n) \
| ${bin}/toposimplify -p 100 -f \
| ${bin}/topoquantize 100 \
> $@"
ls -lh $@
*{box-sizing:border-box}body{margin:0;padding:0}span{position:absolute;top:0;left:0}input{width:100%}canvas{display:block}
const ocean = ({ ocean, ctx, path }) => {
ctx.fillStyle = 'rgba(0,111,145,0.15)'
ctx.beginPath()
path({
type: 'FeatureCollection',
features: ocean,
})
ctx.fill()
}
const lines = ({ lines, ctx, path }) => {
ctx.strokeStyle = '#00485e'
ctx.beginPath()
path({
type: 'FeatureCollection',
features: lines,
})
ctx.stroke()
}
const reset = ({ dpRatio, canvas, ctx }) => {
ctx.setTransform(dpRatio, 0, 0, dpRatio, 0, 0)
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
}
const draw = {
ocean,
lines,
reset,
}
export default draw
import { lineDistance } from './util.js'
const getFeatures = topology => {
const all = topojson.feature(topology, topology.objects.all)
const ocean = all.features.filter(d => d.properties.featurecla)
const lines = all.features
// Choose features that are not `featurecla` (ocean),
.filter(d => !d.properties.featurecla)
// limit to `LineString` (we don't need to focus on `MultiLineString` for now),
.filter(d => d.geometry.type === 'LineString')
// and calculate overall distance.
.map(d => ({
...d,
properties: {
...d.properties,
distance: lineDistance(d.geometry.coordinates),
},
}))
return {
all,
ocean,
// Return 10 times the amount of lines (so we can debug performance).
lines: lines
.concat(lines)
.concat(lines)
.concat(lines)
.concat(lines)
.concat(lines)
.concat(lines)
.concat(lines)
.concat(lines)
.concat(lines),
}
}
export default getFeatures
import { lineSliceAlong } from './util.js'
const interpolateLineStrings = ({ lines, pct }) =>
lines
.map(line => ({
...line,
geometry: {
...line.geometry,
coordinates: lineSliceAlong({
stop: pct * line.properties.distance,
line: line.geometry.coordinates,
}),
},
}))
export default interpolateLineStrings
{
"dependencies": {
"d3-geo-projection": "^2.1.2",
"shapefile": "^0.6.2",
"topojson": "^3.0.0",
"topojson-client": "^3.0.0",
"topojson-server": "^3.0.0",
"topojson-simplify": "^3.0.1"
}
}
const setupCanvas = ({ container, width, height }) => {
const dpRatio = window.devicePixelRatio || 1
const canvas = container.querySelector('canvas')
canvas.width = width * dpRatio
canvas.height = height * dpRatio
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
const ctx = canvas.getContext('2d')
ctx.scale(dpRatio, dpRatio)
return { canvas, ctx, dpRatio }
}
export default setupCanvas
*
box-sizing border-box
body
margin 0
padding 0
input
width 100%
canvas
display block
// The following is significantly based on
// https://github.com/mapbox/cheap-ruler/blob/master/index.js.
const distance = (a, b) => {
const dx = (a[0] - b[0])
const dy = (a[1] - b[1])
return Math.sqrt(dx * dx + dy * dy)
}
const lineSliceAlongMultiple = (stop, lines) => {
}
const lineSliceAlong = ({ line, stop }) => {
let sum = 0
const slice = []
for (let i = 0; i < line.length - 1; i++) {
let p0 = line[i]
let p1 = line[i + 1]
const d = distance(p0, p1)
sum += d
if (sum >= stop) {
slice.push(interpolate(p0, p1, (stop - (sum - d)) / d))
return slice
} else {
slice.push(p1)
}
}
return slice
}
const interpolate = (a, b, t) => {
const dx = b[0] - a[0]
const dy = b[1] - a[1]
return [
a[0] + dx * t,
a[1] + dy * t,
]
}
const lineDistance = points => {
let total = 0
for (let i = 0; i < points.length - 1; i++) {
total += distance(points[i], points[i + 1])
}
return total
}
export {
lineSliceAlong,
lineDistance,
}
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
array-source@0.0:
version "0.0.3"
resolved "https://registry.yarnpkg.com/array-source/-/array-source-0.0.3.tgz#6ee635763c4fb4cd9990f876321cb29e6d7dded1"
commander@2:
version "2.11.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
d3-array@1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.0.tgz#147d269720e174c4057a7f42be8b0f3f2ba53108"
d3-geo-projection@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/d3-geo-projection/-/d3-geo-projection-2.1.2.tgz#7df8e1e9d046d631c6509f7e531357d4adc24aa3"
dependencies:
commander "2"
d3-array "1"
d3-geo "^1.1.0"
d3-geo@^1.1.0:
version "1.6.4"
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.6.4.tgz#f20e1e461cb1845f5a8be55ab6f876542a7e3199"
dependencies:
d3-array "1"
file-source@0.6:
version "0.6.1"
resolved "https://registry.yarnpkg.com/file-source/-/file-source-0.6.1.tgz#ae189d4993766b865a77f83adcf9b9a504cd37dc"
dependencies:
stream-source "0.3"
path-source@0.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/path-source/-/path-source-0.1.2.tgz#c5a26c44fb92cd32b930e8e49b1fe5750f2ca70b"
dependencies:
array-source "0.0"
file-source "0.6"
shapefile@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/shapefile/-/shapefile-0.6.2.tgz#3e232674290234a6474b6ccaca8899a796fe98ed"
dependencies:
array-source "0.0"
commander "2"
path-source "0.1"
slice-source "0.4"
stream-source "0.3"
text-encoding "0.6.1"
slice-source@0.4:
version "0.4.1"
resolved "https://registry.yarnpkg.com/slice-source/-/slice-source-0.4.1.tgz#40a57ac03c6668b5da200e05378e000bf2a61d79"
stream-source@0.3:
version "0.3.4"
resolved "https://registry.yarnpkg.com/stream-source/-/stream-source-0.3.4.tgz#0427c1fb128b0bd2d884d868dcc894208f9bda3b"
text-encoding@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.1.tgz#4de1130e61d50dd867040428aa15656efddcb1c8"
topojson-client@3, topojson-client@3.0.0, topojson-client@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.0.0.tgz#1f99293a77ef42a448d032a81aa982b73f360d2f"
dependencies:
commander "2"
topojson-server@3.0.0, topojson-server@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/topojson-server/-/topojson-server-3.0.0.tgz#378e78e87c3972a7b5be2c5d604369b6bae69c5e"
dependencies:
commander "2"
topojson-simplify@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/topojson-simplify/-/topojson-simplify-3.0.0.tgz#58527d26ca85bd8589b5800f6c10e93885245ae5"
dependencies:
commander "2"
topojson-client "3"
topojson-simplify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/topojson-simplify/-/topojson-simplify-3.0.1.tgz#bd631938f405d283c8d846e5c1fad4085699ad51"
dependencies:
commander "2"
topojson-client "3"
topojson@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/topojson/-/topojson-3.0.0.tgz#c2aca1b76435e801dce3f229586bbd5767115ce3"
dependencies:
topojson-client "3.0.0"
topojson-server "3.0.0"
topojson-simplify "3.0.0"