class TriMap {
constructor(div, layers) {
this.div = div
this.regl = wrapRegl({canvas: div, extensions: ["OES_element_index_uint"]})
for (let layer of layers) {
layer.bind_to_regl(this.regl)
}
this.layers = layers;
const {width, height} = div
this.width = width
this.height = height
this.set_magic_numbers()
this.prepare_div(width, height)
this.set_renderer()
}
add_layer(layer) {
layer.bind_to_regl(this.regl)
this.layers.push(layer)
}
fbo(name) {
this.buffers = this.buffers || new Map()
if (this.buffers.get(name)) {
return this.buffers.get(name)
}
const fbo = this.regl.framebuffer({
width: this.width,
height: this.height,
stencil: false
})
this.buffers.set(name, fbo)
return this.buffers.get(name)
}
set_magic_numbers() {
const { layers, width, height } = this;
const extent = layers[0].bbox;
const scales = {};
const scale_dat = {'x': {}, 'y': {}}
for (let [name, dim] of [['x', width], ['y', height]]) {
const limits = extent[name]
scale_dat[name].limits = limits;
scale_dat[name].size_range = limits[1] - limits[0]
scale_dat[name].pixels_per_unit = dim / scale_dat[name].size_range
}
const data_aspect_ratio =
scale_dat.x.pixels_per_unit / scale_dat.y.pixels_per_unit
let x_buffer_size = 0, y_buffer_size = 0,
x_target_size = width, y_target_size = height;
if (data_aspect_ratio > 1) {
// There are more pixels in the x dimension, so we need a buffer
// around it.
x_target_size = width / data_aspect_ratio;
x_buffer_size = (width - x_target_size)/2
} else {
y_target_size = height * data_aspect_ratio;
y_buffer_size = (height - y_target_size)/2
}
scales.x =
d3.scaleLinear()
.domain(scale_dat.x.limits)
.range([x_buffer_size, width-x_buffer_size])
scales.y =
d3.scaleLinear()
.domain(scale_dat.y.limits)
.range([y_buffer_size, height-y_buffer_size])
this.magic_numbers = window_transform(
scales.x,
scales.y, width, height)
.map(d => d.flat())
}
prepare_div(width, height) {
this.zoom = {transform: {k: 1, x: 0, y:0}}
d3.select(this.div)
.call(d3.zoom().extent([[0, 0], [width, height]]).on("zoom", (event, g) => {
this.zoom.transform = event.transform
}));
return div;
}
get size_func() {
return this._size_function ? this._size_function : () => 1
}
set size_func(f) {
this._size_function = f
}
set color_func(f) {
this._color_function = f
}
get index_color() {
return function(f) {
if (f._index_color) {return f._index_color}
f._index_color = [0, 1, 2].map(d => 1 / 255 * Math.floor(Math.random() * 255))
return f._index_color
}
}
get color_func() {
return this._color_function ? this._color_function : () => [.8, .8, .8]
}
single_blur_pass(fbo1, fbo2, direction) {
const { regl } = this;
fbo2.use( () => {
regl.clear({color: [0, 0, 0, 0]});
regl(
{
frag: gaussian_blur,
uniforms: {
iResolution: ({viewportWidth, viewportHeight}) => [viewportWidth, viewportHeight],
iChannel0: fbo1,
direction
},
/* blend: {
enable: true,
func: {
srcRGB: 'one',
srcAlpha: 'one',
dstRGB: 'one minus src alpha',
dstAlpha: 'one minus src alpha',
},
}, */
vert: `
precision mediump float;
attribute vec2 position;
varying vec2 uv;
void main() {
uv = 0.5 * (position + 1.0);
gl_Position = vec4(position, 0, 1);
}`,
attributes: {
position: [ -4, -4, 4, -4, 0, 4 ]
},
depth: { enable: false },
count: 3,
})()
})
}
blur(fbo1, fbo2, passes = 5
) {
let remaining = passes - 1
while (remaining > -1) {
this.single_blur_pass(fbo1, fbo2, [2 ** remaining, 0])
this.single_blur_pass(fbo2, fbo1, [0, 2 ** remaining])
remaining -= 1
}
}
draw_edges(layer) {
const {regl} = this;
const colors = this.fbo("colorpicker")
const edges = this.fbo("edges")
colors.use(d => {
this.regl.clear({color: [0, 0, 0, 0]})
this.poly_tick(layer)
})
edges.use(() => {
this.regl.clear({color: [1, 1, 1, 1]})
const shader = simple_shader(this.regl, edge_detection)
shader({layer: colors})
})
// Copy the edges to a ping-pong shader to be blurred.
const pingpong = [this.fbo("ping"), this.fbo("pong")]
const copier = simple_shader(this.regl, copy_shader)
let alpha = state.decay;
const decay = state.decay;
pingpong[0].use(() => {
regl.clear({color: [0, 0, 0, 0]})
copier({layer: edges})
})
const edge_propagator = simple_shader(this.regl, edge_propagation)
while (alpha > 1/255) {
pingpong[1].use(() => {
regl.clear({color: [0, 0, 0, 0]})
edge_propagator({layer: pingpong[0], decay: decay})
})
// swap the buffers.
alpha *= decay
pingpong.reverse()
}
const final_shade = simple_shader(this.regl, alpha_color_merge, true)
// First copy the blur
final_shade({alpha: pingpong[0], color: colors})
}
point_tick() {
const { regl } = this;
const calls = []
// multiple interleaved tranches prevent Trump or Biden from always being on top. This is
// an issue with Williamson's maps, which over-represent the Hispanic population of DC because it
// gets plotted last.
const alpha_scale = d3.scaleSqrt().domain([0, 500]).range([0, 1])
const n_tranches = 1
for (let offset of d3.range(n_tranches)) {
for (let pointset of this.random_points) {
calls.push({
position: pointset.buffer,
offset: offset * 8,
stride: 8 * n_tranches,
transform: this.zoom.transform,
// Drops the last point in each tranch--needs a modulo operation to know how
// many to expect.
count: Math.floor(pointset.count/n_tranches),
color: color_scale(pointset.label),
centroid: [0, 0],
size: this.point_size ? this.point_size : 1,
alpha: this.point_opacity > 1/255 ? this.point_opacity : 1/255
})
}
}
const random = d3.randomLcg(0.9051667019185816);
const shuffle = d3.shuffler(random);
shuffle(calls)
this.render_points(calls)
}
tick(wut) {
const { regl } = this
regl.clear({
color: [1, 1, 1, 1],
})
const alpha = 1
if (wut === "points") {
this.point_tick()
} else {
this.draw_edges(this.layers[0])
/* this.fbo("points").use(d => {
regl.clear({
color: [0, 0, 0, 0]
})
// this.point_tick()
})
*/
//const copier = simple_shader(this.regl, copy_shader, true)
//copier({layer: this.fbo("points")})
}
}
poly_tick(layer) {
const { regl } = this;
const calls = []
for (let feature of layer) {
//if (feature.properties['2020_tot'] === null) {continue}
const {vertices, coords} = feature;
calls.push({
transform: this.zoom.transform,
color: this.color_func(feature),
centroid: [feature.properties.centroid_x, feature.properties.centroid_y],
size: this.size_func(feature),
alpha: 1,
vertices: vertices,
coords: coords
})
}
this.render_polygons(calls)
}
get vertex_shader() {return `
precision mediump float;
attribute vec2 position;
uniform float u_size;
uniform vec2 u_centroid;
varying vec4 fragColor;
uniform float u_k;
uniform float u_time;
uniform vec3 u_color;
varying vec4 fill;
// Transform from data space to the open window.
uniform mat3 u_window_scale;
// Transform from the open window to the d3-zoom.
uniform mat3 u_zoom;
uniform mat3 u_untransform;
uniform float u_scale_factor;
// We can bundle the three matrices together here for all shaders.
mat3 from_coord_to_gl = u_window_scale * u_zoom * u_untransform;
void main () {
// scale to normalized device coordinates
// gl_Position is a special variable that holds the position
// of a vertex
vec2 from_center = position-u_centroid;
vec3 p = vec3(from_center * u_size + u_centroid, 1.) * from_coord_to_gl;
gl_Position = vec4(p, 1.0);
gl_PointSize = u_size * (exp(log(u_k)*u_scale_factor));
fragColor = vec4(u_color.rgb, 1.);
//gl_Position = vec4(position / vec2(1., u_aspect), 1., 1.);
}
`}
set_renderer() {
this.render_polygons = this.regl(this.renderer())
this.render_points = this.regl(this.renderer("points"))
}
get point_frag() { return `
precision highp float;
uniform float u_alpha;
varying vec4 fragColor;
void main() {
vec2 coord = gl_PointCoord;
vec2 cxy = 2.0 * coord - 1.0;
float r_sq = dot(cxy, cxy);
if (r_sq > 1.0) {discard;}
gl_FragColor = fragColor * u_alpha;
}`}
get triangle_frag() { return `
precision highp float;
uniform float u_alpha;
varying vec4 fragColor;
void main() {
gl_FragColor = fragColor * u_alpha;
}`}
renderer(wut = "polygons") {
const { regl, magic_numbers } = this;
const definition = {
depth: {
enable: false
},
blend: {enable: true, func: {
srcRGB: 'one',
srcAlpha: 'one',
dstRGB: 'one minus src alpha',
dstAlpha: 'one minus src alpha',
}
},
vert: this.vertex_shader,
frag: wut == 'polygons' ? this.triangle_frag : this.point_frag,
attributes: {
position: wut == "polygons" ?
(_, {coords}) => coords:
(_, {position, stride, offset}) => {return {buffer: position, offset , stride}}
},
count: regl.prop("count"),
elements: wut == "polygons" ? (_, {vertices}) => vertices : undefined,
uniforms: {
u_time: (context, _) => performance.now()/500,
u_scale_factor: () => this.scale_factor ? this.scale_factor : .5,
u_k: function(context, props) {
return props.transform.k
},
u_centroid: propd("centroid", [0, 0]),
u_color: (_, {color}) => color ? color : [.8, .9, .2],
u_window_scale: magic_numbers[0].flat(),
u_untransform: magic_numbers[1].flat(),
u_zoom: function(context, props) {
const g = [
// This is how you build a transform matrix from d3 zoom.
[props.transform.k, 0, props.transform.x],
[0, props.transform.k, props.transform.y],
[0, 0, 1],
].flat()
return g
},
u_alpha: (_, {alpha}) => alpha ? alpha : 1,
u_size: (_, {size}) => size || 1,
},
primitive: wut == "polygons" ? "triangles" : "points"
}
if (wut === "polygons") {
delete definition['count']
}
return definition
}
}