Published
Edited
Nov 16, 2020
Importers
3 stars
Insert cell
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
country_feather.schema.fields//.filter(d => arrow.Float.isInt(d.type))
Insert cell
myCanvas = DOM.canvas(width, width)
Insert cell
viewof distort_around = radio({title: "Distort shapes around", options: ['world centroid', 'country centroid'], value: 'world centroid'})
Insert cell
renderer = (
//Starting point: https://observablehq.com/@marcom13/mesh-rendering-using-webgl-regl
{
vert: `
precision mediump float;
attribute vec2 position;
uniform float aspect;
uniform float u_zoom;
uniform float u_scale;
uniform float u_incidence;
uniform float u_theta;
uniform vec3 color;
uniform vec2 translate;
uniform vec2 u_centroid;
varying vec4 fragColor;

mat2 rotate2d(float _angle){
return mat2(cos(_angle),-sin(_angle),
sin(_angle),cos(_angle));
}


void main () {
gl_PointSize = 2.;
vec2 rot;
fragColor = vec4(color.r, color.g, color.b, .7);
vec2 p_from_centroid = position - u_centroid;
if (u_incidence == 0.) {
rot=p_from_centroid*rotate2d(u_theta);
} else {
float angle = atan(p_from_centroid.x, p_from_centroid.y);
float magnitude = sqrt(dot(p_from_centroid, p_from_centroid));
float adjust = (6. + cos(angle) * cos(u_theta) + sin(angle) * sin(u_theta))/4.;
rot = vec2(magnitude * adjust * sin(angle), magnitude* adjust * cos(angle));
}
vec2 rot_adj = rot * u_scale + u_centroid;
gl_Position = vec4(
rot_adj.x *u_zoom + translate.x * u_zoom,
-rot_adj.y * u_zoom * aspect - translate.y*u_zoom*aspect, 0., 1.);
}
`,
frag: `
precision mediump float;
varying vec4 fragColor;
void main () {
gl_FragColor = fragColor;
}
`,
attributes: {
position: (state, props) => props.position
},
elements: function (state, props) {return props.elements},
uniforms: {
u_centroid: gl.prop('centroid'),
u_theta: (_, {angle}) => angle,
u_zoom: (_, {zoom}) => zoom * zoom,
u_scale: (_, {scale}) => scale,
u_incidence: (_, {incidence}) => incidence == "distortion" ? 1 : 0,
aspect: width/height,
translate: (state, props) => props.translate,
color: (state, props) => props.color
},
primitive: "triangle",
}

)
Insert cell
height = width
Insert cell
render_loop = {
let ts = []
let start = Date.now()
while(true) {
tick()
ts.push(Date.now()-start)
start = Date.now()
if (ts.length > 30) {
ts = ts.slice(ts.length-10, ts.length)
}
yield md`Average render time = ${d3.format(".4")(d3.mean(ts))}`
}
}
Insert cell
distort_around
Insert cell
country_feather = arrow.Table.from(await FileAttachment("countries (1).gleofeather").arrayBuffer())
Insert cell
viewof zoom = slider({title: "zoom (square root)", min: .5, max: 15, value: 1.5, step: .1})
Insert cell
Insert cell
colorscale = d3.scaleOrdinal(d3.schemeCategory10).domain(d3.range(10))
Insert cell
function tick(features = country_feather) {

const overall_radius = jitter_radius / 100 * (Math.sin(Date.now()/1000) + 1)/2

gl.clear({color: [0.9, 0.9, .9, 1]});

const render_list = []
let ix = -1;
const base_angle = start - Date.now()/1000
for (let feature of features) {
ix++;
if (handler.element_handler.get(ix) === undefined) {
handler.element_handler.set(ix, gl.elements({
primitive: 'triangles',
usage: 'static',
data: feature.vertices,
type: "uint" + feature.coord_resolution,
length: feature.vertices.length, // in bytes
count: feature.vertices.length / feature.coord_resolution * 8
}))
handler.coord_handler.post_data(ix, feature.coordinates)
}
let random = handler.get_prop("random", ix) || handler.set_prop("random", ix, Math.random())
let color = d3.rgb(colorscale(feature.MAPCOLOR8))
let angle = (random + base_angle)
render_list.push({
centroid: distort_around === 'world centroid' ? [0, 0] : [feature.centroid_x, feature.centroid_y],
color: [color.r/255, color.g/255, color.b/255],
zoom,
scale: distort_around === 'world centroid' ? 1 : .75 * Math.exp(random),
angle: distort_around === 'world centroid' ? base_angle : angle ,
position: handler.coord_handler.get(ix),
elements: handler.element_handler.get(ix),
incidence: "distortion",
jitter: [0, 0],
translate: [0, 0],
})
}

render(render_list)

}
Insert cell
handler = new ReglBufferHandler(gl)
Insert cell
class ReglBufferHandler {
constructor(regl) {
this.element_handler = new Map()
this.coord_handler = new BufferHandler(regl)
this.props = new Map()
}
get_prop(prop, id) {
if (this.props.get(prop) === undefined) {
this.props.set(prop, new Map())
}
return this.props.get(prop).get(id)
}
set_prop(prop, id, value) {
if (this.props.get(prop) === undefined) {
this.props.set(prop, new Map())
}
return this.props.get(prop).set(id, value)
}
}
Insert cell
class BufferHandler {
// simple data structure to post blocks of data to regl buffers.
// Rather than allocate a new buffer for each polygon, which is kind of wasteful,
// just set them up in 2 MB blocks and keep using until the next call will overflow.
// Something is wrong with the regl scoping here, so it breaks if you have more than one buffer.
// Currently, I just make sure that the buffer is crazy big--would be worth fixing, though.
constructor(regl, size = 2**26) {
this.regl = regl;
this.size = size;
this.buffers = [regl.buffer({length: this.size, type: "float", usage: "static"})]
this.current_buffer = 0;
this.current_position = 0;
this.lookup = new Map()
}
get(id) {
return this.lookup.get(id)
}
post_data(id, data, stride = 8) {
// Must post as a uint8Array, because that's what it looks like
// in Arrow.
if (data.length + this.current_position > this.size) {
this.current_buffer += 1
this.buffers[this.current_buffer] = this.regl.buffer({length: this.size, type: "float", usage: "static"});
this.current_position = 0;
}
const buffer = this.buffers[this.current_buffer]
// regl docs -- 'typedarrays are copied bit-for-bit into the buffer
// with no type conversion.' So we can send a UintArray8 to a float array no problem.
buffer.subdata(data, this.current_position)
const description = {
buffer: buffer,
stride: stride ? stride : 8,
offset: this.current_position
}
this.lookup.set(id, description)
this.current_position += data.length * 4;
return description;
}
}
Insert cell
start = Date.now()/1000
Insert cell
gl = regl({canvas:myCanvas, extensions: ["OES_element_index_uint"]});
Insert cell
arrow = require('apache-arrow@2.0.0')
Insert cell
import { radio, slider } from '@jashkenas/inputs'
Insert cell
render = gl(renderer);
Insert cell
regl = require("regl") // Use the latest Version
Insert cell
d3 = require("d3@v6", "d3-geo-projection")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more