From d0d8369c10b8c79b97ecb19411272d83160b05d4 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Fri, 20 Apr 2018 09:05:02 -0700 Subject: [PATCH] Add AsciiLayer demo (#1715) --- examples/showcases/ascii/README.md | 9 + examples/showcases/ascii/app.js | 148 ++++++++++++ .../ascii/ascii-layer/ascii-filter.js | 214 ++++++++++++++++++ .../ascii/ascii-layer/ascii-layer.js | 131 +++++++++++ examples/showcases/ascii/ascii-layer/utils.js | 51 +++++ .../showcases/ascii/components/constants.js | 54 +++++ .../ascii/components/control-panel.js | 182 +++++++++++++++ examples/showcases/ascii/index.html | 80 +++++++ examples/showcases/ascii/package.json | 18 ++ examples/showcases/ascii/webpack.config.js | 31 +++ examples/webpack.config.local.js | 5 +- 11 files changed, 919 insertions(+), 4 deletions(-) create mode 100644 examples/showcases/ascii/README.md create mode 100644 examples/showcases/ascii/app.js create mode 100644 examples/showcases/ascii/ascii-layer/ascii-filter.js create mode 100644 examples/showcases/ascii/ascii-layer/ascii-layer.js create mode 100644 examples/showcases/ascii/ascii-layer/utils.js create mode 100644 examples/showcases/ascii/components/constants.js create mode 100644 examples/showcases/ascii/components/control-panel.js create mode 100644 examples/showcases/ascii/index.html create mode 100644 examples/showcases/ascii/package.json create mode 100644 examples/showcases/ascii/webpack.config.js diff --git a/examples/showcases/ascii/README.md b/examples/showcases/ascii/README.md new file mode 100644 index 00000000000..ac6cf360dea --- /dev/null +++ b/examples/showcases/ascii/README.md @@ -0,0 +1,9 @@ +This is a minimal standalone version of the Ascii Video Player example +on [deck.gl](http://deck.gl) website. + +### Usage +Copy the content of this folder to your project. Run +``` +npm install +npm start +``` diff --git a/examples/showcases/ascii/app.js b/examples/showcases/ascii/app.js new file mode 100644 index 00000000000..f6d630aa03d --- /dev/null +++ b/examples/showcases/ascii/app.js @@ -0,0 +1,148 @@ +/* global window,document */ +import React, {Component} from 'react'; +import {render} from 'react-dom'; + +import DeckGL, {OrthographicView} from 'deck.gl'; +import {isWebGL2} from 'luma.gl'; +import ControlPanel from './components/control-panel'; +import AsciiLayer from './ascii-layer/ascii-layer'; + +class Root extends Component { + constructor(props) { + super(props); + this.state = { + width: 500, + height: 500, + settings: { + fontFamily: 'Monaco', + isPlaying: true, + sizeScale: 1, + videoSource: 0 + }, + timestamp: Date.now(), + + webgl2Supported: true, + videoLoading: true + }; + + this._onResize = this._onResize.bind(this); + this._onUpdate = this._onUpdate.bind(this); + this._onLoad = this._onLoad.bind(this); + this._onInitialize = this._onInitialize.bind(this); + this._updateSettings = this._updateSettings.bind(this); + } + + componentDidMount() { + this._onResize(); + + window.addEventListener('resize', this._onResize); + this._timer = window.requestAnimationFrame(this._onUpdate); + } + + componentWillUnmount() { + window.removeEventListener('resize', this._onResize); + window.cancelAnimationFrame(this._timer); + } + + _onResize() { + this.setState({ + width: window.innerWidth, + height: window.innerHeight + }); + } + + _onUpdate() { + if (this.state.settings.isPlaying) { + this.setState({ + timestamp: Date.now() + }); + } + + this._timer = window.requestAnimationFrame(this._onUpdate); + } + + _onLoad(video) { + if (video) { + video.crossOrigin = 'anonymouse'; + } + this._video = video; + } + + _onInitialize(gl) { + this.setState({ + webgl2Supported: isWebGL2(gl) + }); + } + + _updateSettings(settings) { + this.setState({ + settings: {...this.state.settings, ...settings} + }); + } + + _renderDeckGLOverlay({width, height, video}) { + const {timestamp, settings} = this.state; + + return ( + + + + ); + } + + render() { + const {width, height, settings, webgl2Supported} = this.state; + + if (!webgl2Supported) { + return
WebGL2 is not supported in your browser.
; + } + + let canvasWidth = 0; + let canvasHeight = 0; + let canvasStyle = null; + + if (this._video && this._video.videoWidth) { + const {videoWidth, videoHeight} = this._video; + const scale = Math.min(width / videoWidth, height / videoHeight); + + canvasWidth = videoWidth * scale; + canvasHeight = videoHeight * scale; + + canvasStyle = { + width: canvasWidth, + height: canvasHeight, + top: (height - canvasHeight) / 2, + left: (width - canvasWidth) / 2 + }; + } + + return ( +
+
+ ); + } +} + +render(, document.body.appendChild(document.createElement('div'))); diff --git a/examples/showcases/ascii/ascii-layer/ascii-filter.js b/examples/showcases/ascii/ascii-layer/ascii-filter.js new file mode 100644 index 00000000000..53976a1f798 --- /dev/null +++ b/examples/showcases/ascii/ascii-layer/ascii-filter.js @@ -0,0 +1,214 @@ +import {Texture2D, Model, Buffer, Framebuffer, Geometry, TransformFeedback, GL} from 'luma.gl'; +import {sortCharactersByBrightness} from './utils'; + +const vs = ` +#define SHADER_NAME feedback-vertex-shader + +uniform sampler2D video; +uniform sampler2D pixelMapTexture; +attribute vec2 uv; + +varying vec4 instanceIconFrames; +varying vec4 instanceColors; + +float bitColor(float x) { + return floor(x * 4. + 0.5) * 64.; +} + +void main(void) { + vec4 pixel = texture2D(video, uv); + float luminance = 0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b; + + instanceColors = vec4( + bitColor(pixel.r), + bitColor(pixel.g), + bitColor(pixel.b), + 255.0 + ); + + instanceIconFrames = texture2D(pixelMapTexture, vec2(luminance + 0.5 / 256., 0.5)); + + gl_Position = vec4(0.0); +} +`; + +const fs = ` +#define SHADER_NAME feedback-fragment-shader + +#ifdef GL_ES +precision highp float; +#endif + +varying vec4 instanceIconFrames; +varying vec4 instanceColors; + +void main(void) { + gl_FragColor = vec4(0.0); +} +`; + +function getPixelMapTexture(gl, {iconMapping, iconAtlas}) { + // Read the number of dark pixels by character + const darkPixelsByCharacter = {}; + const {width, height} = iconAtlas; + const frameBuffer = new Framebuffer(gl, { + width, + height, + attachments: { + [gl.COLOR_ATTACHMENT0]: iconAtlas + } + }); + + for (const char in iconMapping) { + const bbox = iconMapping[char]; + + const pixels = frameBuffer.readPixels({ + x: bbox.x, + y: height - bbox.y - bbox.height, + width: bbox.width, + height: bbox.height + }); + const len = pixels.length; + let sum = 0; + for (let i = 0; i < len; i += 4) { + const r = pixels[i]; + const a = pixels[i + 3]; + if (a) { + sum += 1 - r / 255 * (a / 255); + } + } + darkPixelsByCharacter[char] = sum; + } + + const chars = sortCharactersByBrightness(darkPixelsByCharacter); + + const data = new Float32Array(4 * 256); + let index = 0; + for (let i = 0; i < 256; i++) { + const icon = iconMapping[chars[i]]; + data[index++] = icon.x; + data[index++] = icon.y; + data[index++] = icon.width; + data[index++] = icon.height; + } + + return new Texture2D(gl, { + width: 256, + height: 1, + dataFormat: GL.RGBA, + format: GL.RGBA32F, + type: GL.FLOAT, + mipmaps: false, + parameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST + }, + data + }); +} + +export default class AsciiFilter { + constructor(gl, {iconMapping, iconAtlas}) { + this.gl = gl; + this.transformFeedback = new TransformFeedback(gl); + + this.model = new Model(gl, { + id: 'ascii-filter', + vs, + fs, + varyings: ['instanceIconFrames', 'instanceColors'], + geometry: new Geometry({ + drawMode: GL.POINTS + }), + vertexCount: 0, + isIndexed: true + }); + + this.videoTexture = new Texture2D(gl, { + mipmaps: false, + parameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST + } + }); + this._updateDimension({width: 1, height: 1}); + + this.model.setUniforms({ + pixelMapTexture: getPixelMapTexture(gl, {iconMapping, iconAtlas}), + video: this.videoTexture + }); + } + + getBuffers({width, height, video}) { + const {model, videoTexture, transformFeedback} = this; + + this._updateDimension({width, height, video}); + videoTexture.setSubImageData({width: video.videoWidth, height: video.videoHeight, data: video}); + + model.draw({ + uniforms: { + video: videoTexture + }, + transformFeedback, + parameters: { + [GL.RASTERIZER_DISCARD]: true + } + }); + + return this.buffers; + } + + _updateDimension({width, height, video}) { + const {videoTexture, model, transformFeedback, gl} = this; + + if (width === videoTexture.width && height === videoTexture.height) { + return; + } + + // video.width = width; + // video.height = height; + videoTexture.resize({width: video.videoWidth, height: video.videoHeight}); + + const vertexCount = width * height; + model.setVertexCount(vertexCount); + + // update attribute + const uv = new Float32Array(2 * vertexCount); + let i = 0; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + uv[i++] = (x + 0.5) / width; + uv[i++] = (y + 0.5) / height; + } + } + + model.setAttributes({uv: {size: 2, value: uv}}); + + const iconFrameBuffer = new Buffer(gl, { + size: 4, + instanced: 1, + data: new Float32Array(4 * vertexCount), + usage: GL.DYNAMIC_COPY + }); + + const colorBuffer = new Buffer(gl, { + size: 4, + instanced: 1, + data: new Float32Array(4 * vertexCount), + usage: GL.DYNAMIC_COPY + }); + + this.buffers = { + instanceIconFrames: iconFrameBuffer, + instanceColors: colorBuffer + }; + + transformFeedback.bindBuffers( + { + 0: iconFrameBuffer, + 1: colorBuffer + }, + {} + ); + } +} diff --git a/examples/showcases/ascii/ascii-layer/ascii-layer.js b/examples/showcases/ascii/ascii-layer/ascii-layer.js new file mode 100644 index 00000000000..b12e0c993fc --- /dev/null +++ b/examples/showcases/ascii/ascii-layer/ascii-layer.js @@ -0,0 +1,131 @@ +import {CompositeLayer, IconLayer, COORDINATE_SYSTEM} from 'deck.gl'; +import {makeFontAtlas} from '@deck.gl/core/core-layers/text-layer/font-atlas'; +import AsciiFilter from './ascii-filter'; + +const LETTER_HEIGHT = 32; +const PADDING = -4; + +const defaultProps = { + video: null, + fontFamily: '"Lucida Console", Monaco, monospace', + timestamp: 0, + sizeScale: 1 +}; + +export default class AsciiLayer extends CompositeLayer { + updateState({props, oldProps}) { + if (props.fontFamily !== oldProps.fontFamily) { + this._createFontTexture(props.fontFamily); + } + + if (!props.video || !props.video.readyState) { + return; + } + + const {viewport} = this.context; + const {dimension} = this._updateDimension({ + sizeScale: props.sizeScale, + width: viewport.width, + height: viewport.height + }); + // Update texture + const attributeBuffers = this.state.filter.getBuffers({ + width: dimension[0], + height: dimension[1], + video: props.video + }); + + this.setState({attributeBuffers}); + } + + _createFontTexture(fontFamily) { + const {gl} = this.context; + const {mapping: iconMapping, texture: iconAtlas} = makeFontAtlas(gl, {fontFamily}); + + let maxAspectRatio = 0; + for (const char in iconMapping) { + const {width, height} = iconMapping[char]; + const aspectRatio = width / height; + maxAspectRatio = aspectRatio > maxAspectRatio ? aspectRatio : maxAspectRatio; + } + + this.setState({ + iconAtlas, + iconMapping, + letterAspectRatio: maxAspectRatio, + filter: new AsciiFilter(gl, {iconMapping, iconAtlas}) + }); + } + + _updateDimension({width, height, sizeScale}) { + if ( + width === this.state.width && + height === this.state.height && + sizeScale === this.state.sizeScale + ) { + return this.state; + } + + const {letterAspectRatio} = this.state; + const vSpacing = sizeScale * (LETTER_HEIGHT + PADDING); + const hSpacing = vSpacing * letterAspectRatio; + + const xCount = Math.ceil(width / hSpacing); + const yCount = Math.ceil(height / vSpacing); + const grid = []; + + for (let y = 0; y < yCount; y++) { + for (let x = 0; x < xCount; x++) { + grid.push({ + x: (x + 0.5) * hSpacing - width / 2, + y: (y + 0.5) * vSpacing - height / 2, + u: x / xCount, + v: y / yCount + }); + } + } + const newState = { + grid, + dimension: [xCount, yCount], + width, + height, + sizeScale + }; + this.setState(newState); + + return newState; + } + + renderLayers() { + if (!this.props.video || !this.props.video.readyState) { + return null; + } + + const {grid, iconAtlas, iconMapping, attributeBuffers} = this.state; + const {sizeScale, timestamp} = this.props; + + return new IconLayer({ + id: 'text', + coordinateSystem: COORDINATE_SYSTEM.IDENTITY, + data: grid, + opacity: 1, + iconAtlas, + iconMapping, + sizeScale: LETTER_HEIGHT * 2 * sizeScale, + + getIcon: d => ' ', + // getColor: d => d.color, + getPosition: d => [d.x, d.y], + + ...attributeBuffers, + + updateTriggers: { + getIcon: timestamp, + getColor: timestamp + } + }); + } +} + +AsciiLayer.layerName = 'AsciiLayer'; +AsciiLayer.defaultProps = defaultProps; diff --git a/examples/showcases/ascii/ascii-layer/utils.js b/examples/showcases/ascii/ascii-layer/utils.js new file mode 100644 index 00000000000..b014e2a3a6e --- /dev/null +++ b/examples/showcases/ascii/ascii-layer/utils.js @@ -0,0 +1,51 @@ +// const BRIGHTNESS = {'0': 14, '1': 126, '2': 102, '3': 95, '4': 95, '5': 81, '6': 57, '7': 131, '8': 43, '9': 57, ' ': 255, '!': 176, '"': 198, '#': 69, '$': 57, '%': 43, '&': 31, '\'': 214, '(': 122, ')': 124, '*': 148, '+': 169, ',': 210, '-': 222, '.': 231, '/': 172, ':': 210, ';': 186, '<': 167, '=': 162, '>': 167, '?': 148, '@': 43, 'A': 69, 'B': 45, 'C': 119, 'D': 57, 'E': 93, 'F': 122, 'G': 79, 'H': 64, 'I': 112, 'J': 133, 'K': 69, 'L': 143, 'M': 14, 'N': 33, 'O': 60, 'P': 86, 'Q': 33, 'R': 48, 'S': 98, 'T': 126, 'U': 79, 'V': 91, 'W': 5, 'X': 76, 'Y': 124, 'Z': 88, '[': 112, '\\': 172, ']': 110, '^': 167, '_': 210, '`': 236, 'a': 95, 'b': 71, 'c': 143, 'd': 71, 'e': 107, 'f': 129, 'g': 50, 'h': 98, 'i': 162, 'j': 131, 'k': 95, 'l': 143, 'm': 81, 'n': 119, 'o': 107, 'p': 76, 'q': 76, 'r': 157, 's': 124, 't': 129, 'u': 119, 'v': 141, 'w': 83, 'x': 122, 'y': 112, 'z': 117, '{': 117, '|': 184, '}': 117, '~': 198, '': 255}; + +function normalizeCharacterBrightness(darkPixelsByCharacter) { + let min = Infinity; + let max = 0; + for (const char in darkPixelsByCharacter) { + const b = darkPixelsByCharacter[char]; + min = b < min ? b : min; + max = b > max ? b : max; + } + + if (max === 0) { + throw Error('Characters are blank'); + } + + const brightness = {}; + for (const char in darkPixelsByCharacter) { + const b = darkPixelsByCharacter[char]; + brightness[char] = Math.floor((max - b) / (max - min) * 255); + } + return brightness; +} + +export function sortCharactersByBrightness(darkPixelsByCharacter) { + const brightness = normalizeCharacterBrightness(darkPixelsByCharacter); + const sortedBrightness = new Array(256); + + for (const char in brightness) { + const b = brightness[char]; + let group = sortedBrightness[b]; + + if (!group) { + group = {options: [], char}; + sortedBrightness[b] = group; + } + group.options.push(char); + } + + let lastGroup; + for (let i = 256; i--; ) { + const group = sortedBrightness[i]; + + if (group) { + lastGroup = group; + } else { + sortedBrightness[i] = {char: lastGroup.options[i % lastGroup.options.length]}; + } + } + + return sortedBrightness.map(b => b.char); +} diff --git a/examples/showcases/ascii/components/constants.js b/examples/showcases/ascii/components/constants.js new file mode 100644 index 00000000000..03e3f0c42cd --- /dev/null +++ b/examples/showcases/ascii/components/constants.js @@ -0,0 +1,54 @@ +import React from 'react'; + +const DATA_URL = 'https://raw.githubusercontent.com/uber-common/deck.gl-data/master/examples/ascii'; + +/* global window */ +function getWebcamStream(callback) { + window.navigator.mediaDevices + .getUserMedia({video: true}) + .then(stream => callback(stream)) + .catch(console.error); // eslint-disable-line +} + +export const MIN_SIZE_SCALE = 0.125; +export const MAX_SIZE_SCALE = 4; + +export const VIDEOS = [ + { + source: `${DATA_URL}/Sintel_Opening.mp4`, + name: 'Sintel', + description: ( + + {' '} + © copyright Blender Foundation | durian.blender.org + + ) + }, + { + source: `${DATA_URL}/Felix_BoldKingCole.mp4`, + name: 'Felix the Cat in Bold King Cole', + description: ( + + {' '} + Van Beuren Studios (1936) |{' '} + archive.org + + ) + }, + { + source: getWebcamStream, + name: 'Webcam' + } +]; + +export const FONTS = [ + 'Monaco', + 'Helvetica', + 'Courier', + 'Arial', + 'Times', + 'Impact', + 'Georgia', + 'Bookman', + '"Comic Sans MS"' +]; diff --git a/examples/showcases/ascii/components/control-panel.js b/examples/showcases/ascii/components/control-panel.js new file mode 100644 index 00000000000..e2d143f5ac0 --- /dev/null +++ b/examples/showcases/ascii/components/control-panel.js @@ -0,0 +1,182 @@ +/* global document */ +import React, {PureComponent} from 'react'; +import {MIN_SIZE_SCALE, MAX_SIZE_SCALE, FONTS, VIDEOS} from './constants'; + +export default class ControlPanel extends PureComponent { + constructor(props) { + super(props); + + this._onKeydown = this._onKeydown.bind(this); + this._setVideoSource = this._setVideoSource.bind(this); + this._togglePlay = this._togglePlay.bind(this); + this._onVideoSourceChange = this._onVideoSourceChange.bind(this); + this._onSizeScaleChange = this._onSizeScaleChange.bind(this); + this._onFontFamilyChange = this._onFontFamilyChange.bind(this); + } + + componentDidMount() { + document.addEventListener('keydown', this._onKeydown); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.video && !this.props.video) { + // video element is just loaded + this._setVideoSource(nextProps.video, nextProps.videoSource); + } + } + + componentWillUnmount() { + document.removeEventListener('keydown', this._onKeydown); + } + + _onKeydown(event) { + switch (event.keyCode) { + case 38: + // up: increase font size + this._nextSizeScale(1); + break; + + case 40: + // down: reduce font size + this._nextSizeScale(-1); + break; + + case 32: + // spacebar: toggle playback + this._togglePlay(); + break; + + case 39: + // right: use next video + this._nextVieoSource(1); + break; + + case 37: + // left: use prev video + this._nextVieoSource(-1); + break; + + default: + } + } + + _togglePlay() { + const isPlaying = !this.props.isPlaying; + this.props.video[isPlaying ? 'play' : 'pause'](); + + this.props.updateSettings({isPlaying}); + } + + _nextSizeScale(delta) { + let {sizeScale} = this.props; + + sizeScale *= Math.pow(2, delta / 2); + if (sizeScale < MIN_SIZE_SCALE) { + sizeScale = MIN_SIZE_SCALE; + } + if (sizeScale > MAX_SIZE_SCALE) { + sizeScale = MAX_SIZE_SCALE; + } + + this.props.updateSettings({sizeScale}); + } + + _nextVieoSource(delta) { + let {videoSource} = this.props; + + videoSource = (videoSource + delta + VIDEOS.length) % VIDEOS.length; + + this._setVideoSource(this.props.video, videoSource); + + this.props.updateSettings({isPlaying: true, videoSource}); + } + + _setVideoSource(video, sourceIndex) { + const videoSourceInfo = VIDEOS[sourceIndex]; + + switch (typeof videoSourceInfo.source) { + case 'function': + videoSourceInfo.source(obj => { + video.srcObject = obj; + }); + break; + + case 'string': + video.srcObject = null; + video.src = videoSourceInfo.source; + break; + + default: + video.srcObject = videoSourceInfo.source; + } + + video.play(); + } + + _onVideoSourceChange(event) { + const videoSource = event.target.value; + this._setVideoSource(this.props.video, videoSource); + this.props.updateSettings({isPlaying: true, videoSource}); + } + + _onSizeScaleChange(event) { + this.props.updateSettings({ + sizeScale: Math.pow(2, event.target.value) + }); + } + + _onFontFamilyChange(event) { + this.props.updateSettings({ + fontFamily: event.target.value + }); + } + + render() { + const {fontFamily, isPlaying, videoSource, sizeScale} = this.props; + const videoSourceInfo = VIDEOS[videoSource]; + + return ( +
+
+ +
+ {videoSourceInfo.name} + {videoSourceInfo.description} +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ ); + } +} diff --git a/examples/showcases/ascii/index.html b/examples/showcases/ascii/index.html new file mode 100644 index 00000000000..247ee0b13fb --- /dev/null +++ b/examples/showcases/ascii/index.html @@ -0,0 +1,80 @@ + + + + + deck.gl example + + + + + + diff --git a/examples/showcases/ascii/package.json b/examples/showcases/ascii/package.json new file mode 100644 index 00000000000..e76032c2644 --- /dev/null +++ b/examples/showcases/ascii/package.json @@ -0,0 +1,18 @@ +{ + "scripts": { + "start-local": "webpack-dev-server --env.local --progress --hot --open", + "start": "webpack-dev-server --progress --hot --open", + "build": "webpack --env.local -p --output-filename bundle.js" + }, + "dependencies": { + "deck.gl": ">=5.2.0-beta.1", + "luma.gl": ">=5.2.0-beta.1", + "react": "^16.2.0", + "react-dom": "^16.2.0" + }, + "devDependencies": { + "buble-loader": "^0.4.0", + "webpack": "^2.2.0", + "webpack-dev-server": "^2.2.0" + } +} diff --git a/examples/showcases/ascii/webpack.config.js b/examples/showcases/ascii/webpack.config.js new file mode 100644 index 00000000000..ec75db1b3c9 --- /dev/null +++ b/examples/showcases/ascii/webpack.config.js @@ -0,0 +1,31 @@ +// NOTE: To use this example standalone (e.g. outside of deck.gl repo) +// delete the local development overrides at the bottom of this file + +// avoid destructuring for older Node version support +const resolve = require('path').resolve; + +const CONFIG = { + entry: { + app: resolve('./app.js') + }, + + devtool: 'source-map', + + module: { + rules: [ + { + // Compile ES2015 using buble + test: /\.js$/, + loader: 'buble-loader', + include: [resolve('.')], + exclude: [/node_modules/], + options: { + objectAssign: 'Object.assign' + } + } + ] + } +}; + +// This line enables bundling against src in this repo rather than installed deck.gl module +module.exports = env => (env ? require('../../webpack.config.local')(CONFIG)(env) : CONFIG); diff --git a/examples/webpack.config.local.js b/examples/webpack.config.local.js index ebac58a1e7d..3878ad03046 100644 --- a/examples/webpack.config.local.js +++ b/examples/webpack.config.local.js @@ -7,7 +7,6 @@ // avoid destructuring for older Node version support const resolve = require('path').resolve; -const webpack = require('webpack'); const LIB_DIR = resolve(__dirname, '..'); const SRC_DIR = resolve(LIB_DIR, './modules'); @@ -50,9 +49,7 @@ function makeLocalDevConfig(EXAMPLE_DIR = LIB_DIR) { enforce: 'pre' } ] - }, - // Optional: Enables reading mapbox token from environment variable - plugins: [new webpack.EnvironmentPlugin(['MapboxAccessToken'])] + } }; }