Public
Edited
Nov 26, 2023
Fork of DeckGL
Insert cell
Insert cell
container = html `<div style="height:450px;"></div>`
Insert cell
deckgl.layerManager.layers
Insert cell
Insert cell
square = turf.polygon([[[-20, -20], [-20, 20], [20, 20], [20, -20], [-20, -20]]])
Insert cell
Insert cell
deckgl.deckRenderer.pickLayersPass
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
window.luma
Insert cell
OutlineMaskExtension = {
class OutlineMaskExtension extends deck.LayerExtension {
static {
OutlineMaskExtension.extensionName = "OutlineMaskExtension";
OutlineMaskExtension.defaultProps = {
maskId: '',
maskByInstance: undefined,
maskInverted: false
}
}
initializeState() {
this.context.deck?._addDefaultEffect(new OutlineMaskEffect());
}
getShaders() {
// Infer by geometry if 'maskByInstance' prop isn't explictly set
let maskByInstance = 'instancePositions' in this.getAttributeManager().attributes;
// Users can override by setting the `maskByInstance` prop
if (this.props.maskByInstance !== undefined) {
maskByInstance = Boolean(this.props.maskByInstance);
}
this.state.maskByInstance = maskByInstance;
return {
modules: [outlineMaskShaders]
};
}
/* eslint-disable camelcase */
draw({uniforms, context, moduleParameters}) {
uniforms.mask_maskByInstance = this.state.maskByInstance;
const {maskId, maskInverted} = this.props;
const {maskChannels} = moduleParameters;
const {viewport} = context;
if (maskChannels && maskChannels[maskId]) {
const {index, bounds, coordinateOrigin: fromCoordinateOrigin} = maskChannels[maskId];
let {coordinateSystem: fromCoordinateSystem} = maskChannels[maskId];
uniforms.mask_enabled = true;
uniforms.mask_channel = index;
uniforms.mask_inverted = maskInverted;
if (fromCoordinateSystem === deck.COORDINATE_SYSTEM.DEFAULT) {
fromCoordinateSystem = viewport.isGeospatial
? deck.COORDINATE_SYSTEM.LNGLAT
: deck.COORDINATE_SYSTEM.CARTESIAN;
}
const opts = {modelMatrix: null, fromCoordinateOrigin, fromCoordinateSystem};
const bl = this.projectPosition([bounds[0], bounds[1], 0], opts);
const tr = this.projectPosition([bounds[2], bounds[3], 0], opts);
uniforms.mask_bounds = [bl[0], bl[1], tr[0], tr[1]];
} else {
if (maskId) {
console.warn(`Could not find a mask layer with id: ${maskId}`)();
}
uniforms.mask_enabled = false;
}
}
}

return OutlineMaskExtension;
}
Insert cell
OutlineMaskEffect = {
const { joinLayerBounds, getViewportBounds, makeViewport, getRenderBounds, arraysEqual } = omeUtil;
/*
type Mask = {
index: number;
bounds: Bounds;
coordinateOrigin: [number, number, number];
coordinateSystem: CoordinateSystem;
};
type Channel = {
id: string;
index: number;
layers: Layer[];
bounds: Bounds | null;
maskBounds: Bounds;
layerBounds: Bounds[];
coordinateOrigin: [number, number, number];
coordinateSystem: CoordinateSystem;
};
export type MaskPreRenderStats = {
didRender: boolean;
};
*/
// Class to manage mask effect
class OutlineMaskEffect {
constructor() {
this.id = 'outline-mask-effect';
this.props = null;
this.useInPicking = true;
this.order = 0;
this.channels = [];
}
preRender(device, preRenderOptions) {
const {layers, layerFilter, viewports, onViewportActive, views, isPicking} = preRenderOptions
let didRender = false;
if (!this.dummyMaskMap) {
this.dummyMaskMap = device.createTexture({
width: 1,
height: 1
});
}
if (isPicking) {
// Do not update on picking pass
return {didRender};
}

// TODO
const maskLayers = layers.filter(l => l.props.visible && l.props.operation.includes('mask'));
if (maskLayers.length === 0) {
this.masks = null;
this.channels.length = 0;
return {didRender};
}
this.masks = {};
if (!this.maskPass) {
this.maskPass = new OutlineMaskPass(device, {id: 'default-mask'});
this.maskMap = this.maskPass.maskMap;
}
// Map layers to channels
const channelMap = this._sortMaskChannels(maskLayers);
// TODO - support multiple views
const viewport = viewports[0];
const viewportChanged = !this.lastViewport || !this.lastViewport.equals(viewport);
if (viewport.resolution !== undefined) {
console.warn('MaskExtension is not supported in GlobeView')();
return {didRender};
}
for (const maskId in channelMap) {
const result = this._renderChannel(channelMap[maskId], {
layerFilter,
onViewportActive,
views,
viewport,
viewportChanged
});
didRender ||= result;
}
// debugFBO(this.maskMap, {opaque: true});
return {didRender};
}
/* eslint-disable-next-line complexity */
_renderChannel(
channelInfo,
{
layerFilter,
onViewportActive,
views,
viewport,
viewportChanged
}
) {
let didRender = false;
const oldChannelInfo = this.channels[channelInfo.index];
if (!oldChannelInfo) {
return didRender;
}
const maskChanged =
// If a channel is new
channelInfo === oldChannelInfo ||
// If sublayers have changed
channelInfo.layers.length !== oldChannelInfo.layers.length ||
channelInfo.layers.some(
(layer, i) =>
// Layer instance is updated
// Layer props might have changed
// Undetermined props could have an effect on the output geometry of a mask layer,
// for example getRadius+updateTriggers, radiusScale, modelMatrix
layer !== oldChannelInfo.layers[i] ||
// Some prop is in transition
layer.props.transitions
) ||
// If a sublayer's positions have been updated, the cached bounds will change shallowly
channelInfo.layerBounds.some((b, i) => b !== oldChannelInfo.layerBounds[i]);
channelInfo.bounds = oldChannelInfo.bounds;
channelInfo.maskBounds = oldChannelInfo.maskBounds;
this.channels[channelInfo.index] = channelInfo;
if (maskChanged || viewportChanged) {
// Recalculate mask bounds
this.lastViewport = viewport;
const layerBounds = joinLayerBounds(channelInfo.layers, viewport);
channelInfo.bounds = layerBounds && getRenderBounds(layerBounds, viewport);
if (maskChanged || !arraysEqual(channelInfo.bounds, oldChannelInfo.bounds)) {
// Rerender mask FBO
const {maskPass, maskMap} = this;
const maskViewport =
layerBounds &&
makeViewport({
bounds: channelInfo.bounds,
viewport,
width: maskMap.width,
height: maskMap.height,
border: 1
});
channelInfo.maskBounds = maskViewport ? maskViewport.getBounds() : [0, 0, 1, 1];
// @ts-ignore (2532) This method is only called from preRender where maskPass is defined
maskPass.render({
pass: 'mask',
channel: channelInfo.index,
layers: channelInfo.layers,
layerFilter,
viewports: maskViewport ? [maskViewport] : [],
onViewportActive,
views,
moduleParameters: {
devicePixelRatio: 1
}
});
didRender = true;
}
}
// @ts-ignore (2532) This method is only called from preRender where masks is defined
this.masks[channelInfo.id] = {
index: channelInfo.index,
bounds: channelInfo.maskBounds,
coordinateOrigin: channelInfo.coordinateOrigin,
coordinateSystem: channelInfo.coordinateSystem
};
return didRender;
}
/**
* Find a channel to render each mask into
* If a maskId already exists, diff and update the existing channel
* Otherwise replace a removed mask
* Otherwise create a new channel
* Returns a map from mask layer id to channel info
*/
_sortMaskChannels(maskLayers) {
const channelMap = {};
let channelCount = 0;
for (const layer of maskLayers) {
const {id} = layer.root;
let channelInfo = channelMap[id];
if (!channelInfo) {
if (++channelCount > 4) {
console.warn('Too many mask layers. The max supported is 4')();
continue; // eslint-disable-line no-continue
}
channelInfo = {
id,
index: this.channels.findIndex(c => c?.id === id),
layers: [],
layerBounds: [],
coordinateOrigin: layer.root.props.coordinateOrigin,
coordinateSystem: layer.root.props.coordinateSystem
};
channelMap[id] = channelInfo;
}
channelInfo.layers.push(layer);
channelInfo.layerBounds.push(layer.getBounds());
}
for (let i = 0; i < 4; i++) {
const channelInfo = this.channels[i];
if (!channelInfo || !(channelInfo.id in channelMap)) {
// The mask id at this channel no longer exists
this.channels[i] = null;
}
}
for (const maskId in channelMap) {
const channelInfo = channelMap[maskId];
if (channelInfo.index < 0) {
channelInfo.index = this.channels.findIndex(c => !c);
this.channels[channelInfo.index] = channelInfo;
}
}
return channelMap;
}
getModuleParameters({ maskMap, maskChannels }) {
return {
maskMap: this.masks ? this.maskMap : this.dummyMaskMap,
maskChannels: this.masks
};
}
cleanup() {
if (this.dummyMaskMap) {
this.dummyMaskMap.delete();
this.dummyMaskMap = undefined;
}
if (this.maskPass) {
this.maskPass.delete();
this.maskPass = undefined;
this.maskMap = undefined;
}
this.lastViewport = undefined;
this.masks = null;
this.channels.length = 0;
}
}

return OutlineMaskEffect;
}
Insert cell
OutlineMaskPass = {
const { GL, withParameters } = luma;
class OutlineMaskPass extends deck._LayersPass {
constructor(device, props) {
let {id, mapSize} = props;
super(device, props);
mapSize ||= 2048;
debugger;
this.maskMap = device.createTexture({
format: 'rgba8unorm',
width: mapSize,
height: mapSize,
sampler: {
minFilter: 'linear',
magFilter: 'linear',
addressModeU: 'clamp-to-edge',
addressModeV: 'clamp-to-edge'
}
});
this.maskMap.width = mapSize;
this.maskMap.height = mapSize;
this.fbo = device.createFramebuffer({
id: 'maskmap',
width: mapSize,
height: mapSize,
colorAttachments: [this.maskMap]
});
}
render(options) {
const colorMask = [false, false, false, false];
colorMask[options.channel] = true;
return this.withGLParameters(
this.device,
{
clearColor: [255, 255, 255, 255],
blend: true,
blendFunc: [GL.ZERO, GL.ONE],
blendEquation: GL.FUNC_SUBTRACT,
colorMask,
depthTest: false
},
() => super.render({...options, target: this.fbo, pass: 'mask'})
);
}
shouldDrawLayer(layer) {
return layer.props.operation.includes('mask');
}
delete() {
this.fbo.delete();
this.maskMap.delete();
}
}
return OutlineMaskPass;
}

Insert cell
luma
Insert cell
outlineMaskShaders = {
const vs = `
uniform vec4 mask_bounds;
uniform bool mask_maskByInstance;
vec2 mask_getCoords(vec4 position) {
return (position.xy - mask_bounds.xy) / (mask_bounds.zw - mask_bounds.xy);
}
`;
const fs = `
uniform sampler2D mask_texture;
uniform int mask_channel;
uniform bool mask_enabled;
uniform bool mask_inverted;
bool mask_isInBounds(vec2 texCoords) {
if (!mask_enabled) {
return true;
}
vec4 maskColor = texture2D(mask_texture, texCoords);
float maskValue = 1.0;
if (mask_channel == 0) {
maskValue = maskColor.r;
} else if (mask_channel == 1) {
maskValue = maskColor.g;
} else if (mask_channel == 2) {
maskValue = maskColor.b;
} else if (mask_channel == 3) {
maskValue = maskColor.a;
}
if (mask_inverted) {
return maskValue >= 0.5;
} else {
return maskValue < 0.5;
}
}
`;
const inject = {
'vs:#decl': `
varying vec2 mask_texCoords;
`,
'vs:#main-end': `
vec4 mask_common_position;
if (mask_maskByInstance) {
mask_common_position = project_position(vec4(geometry.worldPosition, 1.0));
} else {
mask_common_position = geometry.position;
}
mask_texCoords = mask_getCoords(mask_common_position);
`,
'fs:#decl': `
varying vec2 mask_texCoords;
`,
'fs:#main-start': `
if (mask_enabled) {
bool mask = mask_isInBounds(mask_texCoords);
// Debug: show extent of render target
// gl_FragColor = vec4(mask_texCoords, 0.0, 1.0);
gl_FragColor = texture2D(mask_texture, mask_texCoords);
if (!mask) discard;
}
`
};

/* eslint-disable camelcase */
const getMaskUniforms = (opts) => {
if (opts && 'maskMap' in opts) {
return {
mask_texture: opts.maskMap
};
}
return {};
};
return {
name: 'mask',
dependencies: [deck.project],
vs,
fs,
inject,
getUniforms: getMaskUniforms
};
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
w8r = {
const Offset = (await import('https://cdn.skypack.dev/polygon-offset@0.3.2?min')).default;
const { intersection, diff, union, xor } = await import('https://cdn.skypack.dev/martinez-polygon-clipping@0.7.3?min');
return { Offset, intersection, diff, union, xor };
}
Insert cell
/*
lumaVersion = {
await deck;
return window.luma.VERSION;
}
*/
Insert cell
// import('https://cdn.skypack.dev/@luma.gl/webgl@8.5.21?min')
Insert cell
// import('https://cdn.skypack.dev/@luma.gl/core@8.5.21?min')
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