Published
Edited
Jul 20, 2020
1 star
Insert cell
md`
# WIP: Aggregation with BitmapLayer

TODO:
- Add uniform for number of pixels to aggregate in x and y dimensions (uAggX: 1/2/4, uAggY: 1/2/4)
- Update this value based on the texture size and current view size & current zoom level (is cell width/height < 1px or < 0.5px)
`
Insert cell
container = html `<div style="height:400px;width:400px;"></div>`
Insert cell
viewWidth = 400
Insert cell
viewHeight = 400
Insert cell
fs = `
#define SHADER_NAME bitmap-layer-fragment-shader

#ifdef GL_ES
precision highp float;
#endif

uniform sampler2D uBitmapTexture;
uniform vec2 uTextureSize;

varying vec2 vTexCoord;

void main(void) {
// compute 1 pixel in texture coordinates
// 1 / size of texture
vec2 onePixel = vec2(1.0, 1.0) / vec2(4.0, 3.0);
vec2 viewCoord = vec2(floor(vTexCoord.x * uTextureSize.x), floor(vTexCoord.y * uTextureSize.y));
if(mod(viewCoord.x, 2.0) == 0.0) {
vec4 bitmapColor = texture2D(uBitmapTexture, vTexCoord);
gl_FragColor = vec4(bitmapColor.r, 0.0, 0.0, 255);
} else {
gl_FragColor = vec4(255.0, 255.0, 255.0, 255.0);
}


geometry.uv = vTexCoord;
DECKGL_FILTER_COLOR(gl_FragColor, geometry);
}
`;
Insert cell
deckgl = {
return new deck.DeckGL({
container,
views: new deck.OrthographicView({controller: true}),
initialViewState: {target: [0, 0, 0], zoom: 0}
});
}
Insert cell
Insert cell
layer0 = {
const { Layer, project32, picking, BitmapLayer } = deck;
const { Model, Geometry, Texture2D, isWebGL2 } = luma;
const DEFAULT_TEXTURE_PARAMETERS = {
[GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
[GL.TEXTURE_MAG_FILTER]: GL.NEAREST,
[GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
[GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE,
};
class HeatmapBitmapLayer extends Layer {
getShaders() {
return super.getShaders({ vs, fs, modules: [ project32, picking ] });
}
initializeState() {
const attributeManager = this.getAttributeManager();

attributeManager.remove(['instancePickingColors']);
const noAlloc = true;

attributeManager.add({
indices: {
size: 1,
isIndexed: true,
update: attribute => (attribute.value = this.state.mesh.indices),
noAlloc
},
positions: {
size: 3,
type: GL.DOUBLE,
fp64: this.use64bitPositions(),
update: attribute => (attribute.value = this.state.mesh.positions),
noAlloc
},
texCoords: {
size: 2,
update: attribute => (attribute.value = this.state.mesh.texCoords),
noAlloc
}
});
}

updateState({ props, oldProps, changeFlags }) {
// setup model first
if (changeFlags.extensionsChanged) {
const {gl} = this.context;
if (this.state.model) {
this.state.model.delete();
}
this.setState({model: this._getModel(gl)});
this.getAttributeManager().invalidateAll();
}

if (props.image !== oldProps.image) {
this.loadTexture(props.image);
}

const attributeManager = this.getAttributeManager();

if (props.bounds !== oldProps.bounds) {
const oldMesh = this.state.mesh;
const mesh = this._createMesh();
this.state.model.setVertexCount(mesh.vertexCount);
for (const key in mesh) {
if (oldMesh && oldMesh[key] !== mesh[key]) {
attributeManager.invalidate(key);
}
}
this.setState({mesh});
}
}

finalizeState() {
super.finalizeState();

if (this.state.bitmapTexture) {
this.state.bitmapTexture.delete();
}
}

_createMesh() {
const { bounds } = this.props;

let normalizedBounds = bounds;
// bounds as [minX, minY, maxX, maxY]
if (Number.isFinite(bounds[0])) {
/*
(minX0, maxY3) ---- (maxX2, maxY3)
| |
| |
| |
(minX0, minY1) ---- (maxX2, minY1)
*/
normalizedBounds = [
[bounds[0], bounds[1]],
[bounds[0], bounds[3]],
[bounds[2], bounds[3]],
[bounds[2], bounds[1]]
];
}

return createMesh(normalizedBounds, this.context.viewport.resolution);
}

_getModel(gl) {
if (!gl) {
return null;
}

/*
0,0 --- 1,0
| |
0,1 --- 1,1
*/
return new Model(
gl,
Object.assign({}, this.getShaders(), {
id: this.props.id,
geometry: new Geometry({
drawMode: GL.TRIANGLES,
vertexCount: 6
}),
isInstanced: false
})
);
}

draw(opts) {
const { uniforms } = opts;
const { bitmapTexture, model } = this.state;
const { image } = this.props;
const { bounds } = this.props;
console.log(bounds);

// // TODO fix zFighting
// Render the image
if (bitmapTexture && model) {
model
.setUniforms(
Object.assign({}, uniforms, {
uBitmapTexture: bitmapTexture,
uTextureSize: [0.0 + bounds[2] - bounds[0], 0.0 + bounds[3] - bounds[1]],
})
)
.draw();
}
}
loadTexture(image) {
const { gl } = this.context;
const noWebGL2 = !isWebGL2(gl);
if(this.state.bitmapTexture) {
this.state.bitmapTexture.delete();
}
if(image instanceof Texture2D) {
this.setState({ bitmapTexture: image });
} else if(image) {
this.setState({
bitmapTexture: new Texture2D(gl, {
data: image,
parameters: DEFAULT_TEXTURE_PARAMETERS,
//format: noWebGL2 ? GL.LUMINANCE : GL.R8UI,
format: GL.LUMINANCE,
width: 4,
height: 3
}),
});
}
}
}
HeatmapBitmapLayer.layerName = 'HeatmapBitmapLayer';
HeatmapBitmapLayer.defaultProps = defaultProps;
const layer = new HeatmapBitmapLayer({
id: `bitmap-${Date.now()}`,
image: imageData,
bounds: [-viewWidth/2, -viewHeight/2, viewWidth/2, viewHeight/2]
});
deckgl.setProps({ layers: [layer] });
return layer;
}
Insert cell
layer0.getAttributeManager().getAttributes();
Insert cell
vs = `
#define SHADER_NAME bitmap-layer-vertex-shader

attribute vec2 texCoords;
attribute vec3 positions;
attribute vec3 positions64Low;

varying vec2 vTexCoord;

const vec3 pickingColor = vec3(1.0, 0.0, 0.0);

void main(void) {
geometry.worldPosition = positions;
geometry.uv = texCoords;
geometry.pickingColor = pickingColor;

gl_Position = project_position_to_clipspace(positions, positions64Low, vec3(0.0), geometry.position);
DECKGL_FILTER_GL_POSITION(gl_Position, geometry);

vTexCoord = texCoords;

vec4 color = vec4(0.0);
DECKGL_FILTER_COLOR(color, geometry);
}
`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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