Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/animated arrow layer #60

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ node_modules
.env
.env.build
.env*.local

.tern-port
96 changes: 96 additions & 0 deletions components/ArrowPath/arrow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {Layer} from '@deck.gl/core';
import {Model, Geometry} from '@luma.gl/core';
import vs from '~/shaders/arrow-vertex.glsl';
import fs from '~/shaders/arrow-fragment.glsl';

const defaultProps = {
getEnd: {type: 'accessor', value: (x: any) => x.path && x.path.length ? x.path.slice(-1)[0] : [0, 0]},
getColor: {type: 'accessor', value: [255, 255, 255, 255]},
getWidth: {type: 'accessor', value: 3},
getRotation: {type: 'accessor', value: 0},
widthUnits: 'pixels',
widthScale: {type: 'number', value: 1, min: 0},
};

class ArrowLayer extends Layer {
constructor(props: any) {
super(props);
}

initializeState() {
const {gl} = this.context;

const attributeManager = this.getAttributeManager();
attributeManager.addInstanced({
instancePositions: {
size: 3,
type: gl.DOUBLE,
accessor: 'getEnd'
},
instanceColors: {
size: 3,
normalized: true,
accessor: 'getColor',
defaultValue: [0, 0, 0, 255]
},
instanceWidths: {
size: 1,
accessor: 'getWidth',
defaultValue: 3
},
instanceRotations: {
size: 1,
accessor: 'getRotation',
defaultValue: 0
}
});

this.setState({model: this._getModel(gl)});
}

updateState({props, oldProps, changeFlags}: {props: any, oldProps: any, changeFlags: any}) {
super.updateState({props, oldProps, changeFlags});

if (changeFlags.extensionsChanged || changeFlags.viewportChanged) {
const {gl} = this.context;
if (this.state.model) {
this.state.model.delete();
}
this.setState({model: this._getModel(gl)});
this.getAttributeManager().invalidateAll();
}
}


_getModel(gl: any) {
const positions = new Float32Array([-1, -1, 1, -1, 0, 1]);
const geometry = new Geometry({
drawMode: gl.TRIANGLE_FAN,
vertexCount: 3,
attributes: {
positions: {size: 2, value: positions}
}
});

return new Model(gl, {...this.getShaders(), id: this.props.id, vs, fs, geometry, isInstanced: true});
}

draw({uniforms}: {uniforms: any}) {
const {viewport} = this.context;
const {widthUnits, widthScale} = this.props;

const widthMultiplier = widthUnits === 'pixels' ? viewport.metersPerPixel : 1;

this.state.model
.setUniforms({
...uniforms,
widthScale: widthScale * widthMultiplier
})
.draw();
}
}

ArrowLayer.layerName = 'ArrowLayer';
ArrowLayer.defaultProps = defaultProps;

export default ArrowLayer
122 changes: 122 additions & 0 deletions components/ArrowPath/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {TripsLayer} from '@deck.gl/geo-layers';
import {CompositeLayer} from '@deck.gl/core';
import ArrowLayer from './arrow';

interface Waypoint {
coordinates: number[][];
timestamp: number;
}

interface PathData {
waypoints: Waypoint[];
name: string;
color: [number, number, number];
}

export const toAngle = ([x0, y0]: [number, number], [x1, y1]: [number, number]) => {
if (x1 - x0 > 0) {
return 90
} else if (x1 - x0 < 0) {
return 270
} else if (y1 - y0 < 0) {
return 180
} else {
return 0
}
}

const defaultProps = {
...TripsLayer.defaultProps,
...ArrowLayer.defaultProps,
loopEvery: 3000,
disableAnimation: false,
trailLength: 3000,
opacity: 1
};

const FRAME_INTERVAL = 1000 / 60;

const truncateWaypoints = (time: number) => (pathData: PathData) => {
// TODO: implement
if (!time || !pathData) {
return []
}
return []
}

class ArrowPathLayer extends CompositeLayer {
constructor(props: any) {
super(props)
}

shouldUpdateState({changeFlags}: {changeFlags: any}) {
if (changeFlags.propsChanged || changeFlags.dataChanged || changeFlags.stateChanged) {
return true;
}

return false;
}

initializeState() {
super.initializeState()
const {loopEvery} = this.props;
const {disableAnimation, data}: {disableAnimation: boolean, data: PathData[]} = this.props;
if (!disableAnimation) {
this.setState({currentTime: 0, arrowTruncatedPaths: data.map(x => ({...x, waypoints: []}))});
this.animate()
} else {
this.setState({currentTime: loopEvery, arrowTruncatedPaths: data.map(truncateWaypoints(loopEvery))})
}
}

animate() {
const {loopEvery} = this.props;
const {currentTime} = this.state;
if (currentTime > loopEvery) {
this.setState({
currentTime: 0
})
} else {
this.setState({
currentTime: currentTime + FRAME_INTERVAL
})
}

requestAnimationFrame(() => this.animate())
}

renderLayers() {
const {data, getEnd, getRotation, getPath, getColor, getWidth, getTimestamps, trailLength, opacity} = this.props
const {currentTime, arrowTruncatedPaths} = this.state;

return [
new ArrowLayer(this.getSubLayerProps({
id: "arrow",
data: arrowTruncatedPaths,
getEnd,
getColor,
getWidth,
getRotation
})),
new TripsLayer(this.getSubLayerProps({
id: "path",
data,
getPath,
getColor,
getWidth,
getTimestamps,
currentTime,
trailLength,
opacity,
updateTriggers: {
getPath: [data],
}
}))
];
}
}

ArrowPathLayer.layerName = 'ArrowPathLayer';
ArrowPathLayer.defaultProps = defaultProps;

export default ArrowPathLayer
1 change: 1 addition & 0 deletions components/Map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const Map: FunctionComponent<IMapProps> = ({
onBuildingClicked,
selectedTourNode
}) => {

const [hash, setHash] = useState<number>(0)

const buildingIds: { [key: string]: true } = {}
Expand Down
48 changes: 44 additions & 4 deletions components/Map/layers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IMapLayerParams } from '~/types/components/Map';
import * as DeckGLLayers from "@deck.gl/layers";
import ArrowPathLayer, {toAngle} from '../ArrowPath';
const PolygonLayer: any = DeckGLLayers.PolygonLayer;
const SolidPolygonLayer: any = DeckGLLayers.SolidPolygonLayer;
const GeoJsonLayer: any = DeckGLLayers.GeoJsonLayer;
Expand Down Expand Up @@ -37,17 +38,18 @@ class CustomSolidPolygonLayer extends SolidPolygonLayer {

this.state.attributeManager.addInstanced({
instanceScaleOrigins: { size: 2, accessor: get_building_center }
});
});

this.state.attributeManager.add({
scaleOrigins: { size: 2, accessor: get_building_center }
});
});

this.state.attributeManager.add({
scaleFactor: { size: 1, accessor: "getScaleFactor"}
});
});

}

}

class CustomGeoJsonLayer extends GeoJsonLayer {
Expand Down Expand Up @@ -77,7 +79,6 @@ CustomGeoJsonLayer.defaultProps = {


export function GetLayers(params: IMapLayerParams) {

/// creates a dynamic polygon just big enough to cover all visible land area regardless of zoom level
const size = Math.pow(2, (19 - params.zoom)) * .004
const landCover = [
Expand All @@ -91,7 +92,45 @@ export function GetLayers(params: IMapLayerParams) {
})
}

const pathData = [
{
waypoints: [
{coordinates: [-90.2099571, 32.3044686], timestamp: 0},
{coordinates: [-90.2100170, 32.3044686], timestamp: 500},
{coordinates: [-90.2100170, 32.3040226], timestamp: 1000},
{coordinates: [-90.2094470, 32.3040226], timestamp: 1500},
{coordinates: [-90.2094470, 32.3037944], timestamp: 2000},
{coordinates: [-90.2093966, 32.3037944], timestamp: 2500}
],
name: 'Resource Flow',
color: [255, 255, 255]
}
]

/// creating layers for a base, and to input geojson(our map data)
const layers = [
new ArrowPathLayer({
id: "arrow-paths",
data: pathData,
pickable: true,
widthScale: 1,
widthMinPixels: 1,
// disableAnimation: true,
getPath: (d: any) => d.waypoints.map((w: {coordinates: number[]}) => w.coordinates),
getColor: (d: any) => d.color,
getTimestamps: (d: any) => d.waypoints.map((w: {timestamp: number}) => w.timestamp),
getWidth: () => 3,
getEnd: (d: any) => {
return d.coordinates && d.coordinates.length ? d.coordinates.slice(-1)[0] : [0, 0]
},
getRotation: (d: any) => {
if (!d.waypoints?.coordinates || d.waypoints?.coordinates.length < 2) {
return 0
}
const [[x0, y0], [x1, y1]] = d.waypoints?.coordinates.slice(-2);
return toAngle([x0, y0], [x1, y1]);
}
}),
new PolygonLayer({ /// required for shadows to project onto
id: "ground",
data: landCover,
Expand Down Expand Up @@ -139,5 +178,6 @@ export function GetLayers(params: IMapLayerParams) {
}
})
];

return layers
}
2 changes: 2 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ declare module '*.styl' {
declare module '@deck.gl/core'
declare module '@deck.gl/react'
declare module '@deck.gl/layers'
declare module '@deck.gl/geo-layers'
declare module '@luma.gl/core'

declare var __CLIENT__: boolean
Loading