├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .stylelintrc ├── .yarnrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── circle.yml ├── electron.js ├── gulpfile.js ├── karma.conf.js ├── meta ├── example-scenes │ ├── README.md │ ├── basic.png │ ├── bubble-wrap-style.png │ ├── cache-scenes.js │ ├── cache-thumbnails.js │ ├── sources.js │ └── tron-style.gif └── screenshot.png ├── package-lock.json ├── package.json ├── public ├── data │ ├── changelog.html │ ├── fonts │ │ ├── ms-sans-serif-bold │ │ │ ├── MS Sans Serif Bold.ttf │ │ │ ├── license.txt │ │ │ └── readme.txt │ │ ├── ms-sans-serif │ │ │ ├── MS Sans Serif.ttf │ │ │ ├── license.txt │ │ │ └── readme.txt │ │ ├── roboto │ │ │ ├── LICENSE.txt │ │ │ ├── Roboto-Black.ttf │ │ │ ├── Roboto-BlackItalic.ttf │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-BoldItalic.ttf │ │ │ ├── Roboto-Italic.ttf │ │ │ ├── Roboto-Light.ttf │ │ │ ├── Roboto-LightItalic.ttf │ │ │ ├── Roboto-Medium.ttf │ │ │ ├── Roboto-MediumItalic.ttf │ │ │ ├── Roboto-Regular.ttf │ │ │ ├── Roboto-Thin.ttf │ │ │ └── Roboto-ThinItalic.ttf │ │ └── source_code_pro │ │ │ ├── OFL.txt │ │ │ ├── SourceCodePro-Black.ttf │ │ │ ├── SourceCodePro-Bold.ttf │ │ │ ├── SourceCodePro-ExtraLight.ttf │ │ │ ├── SourceCodePro-Light.ttf │ │ │ ├── SourceCodePro-Medium.ttf │ │ │ ├── SourceCodePro-Regular.ttf │ │ │ └── SourceCodePro-Semibold.ttf │ ├── imgs │ │ ├── arrow_down.png │ │ ├── docs_inspect.png │ │ ├── docs_save_menu.png │ │ ├── docs_share_menu.png │ │ ├── docs_toolbars.png │ │ ├── globey.gif │ │ ├── globey_speech_bubble.png │ │ └── question.gif │ └── scenes │ │ ├── basic.yaml │ │ ├── blank.yaml │ │ └── thumbnails │ │ ├── 9845c.png │ │ ├── basic.png │ │ ├── blueprint.png │ │ ├── bubble-wrap.png │ │ ├── cinnabar.png │ │ ├── crosshatch.png │ │ ├── gotham.png │ │ ├── grain-area.png │ │ ├── grain-roads.png │ │ ├── grain.png │ │ ├── ikeda.gif │ │ ├── lego.png │ │ ├── matrix.png │ │ ├── patterns.png │ │ ├── pericoli.png │ │ ├── press.png │ │ ├── radar.gif │ │ ├── refill.png │ │ ├── tron-legacy.png │ │ ├── tron.png │ │ ├── walkabout.png │ │ └── zinc.png ├── docs │ └── index.html ├── embed │ └── index.html ├── iframe.html └── index.html ├── src ├── css │ ├── _application.css │ ├── _buttons.css │ ├── _call-to-action.css │ ├── _camera.css │ ├── _color-palette.css │ ├── _color.css │ ├── _divider.css │ ├── _docs-panel.css │ ├── _editor-context-menu.css │ ├── _editor-hidden-tooltip.css │ ├── _editor.css │ ├── _embedded-play.css │ ├── _errors.css │ ├── _filedrop.css │ ├── _floating-panel.css │ ├── _globey.css │ ├── _icon.css │ ├── _inputs.css │ ├── _leaflet-overrides.css │ ├── _map-inspection.css │ ├── _map-loading.css │ ├── _map-panel-search-bookmarks.css │ ├── _map-panel.css │ ├── _map.css │ ├── _menu-bar.css │ ├── _modals.code-snippet.css │ ├── _modals.css │ ├── _modals.open-scene.css │ ├── _modals.welcome.css │ ├── _overlay.css │ ├── _sandbox.css │ ├── _shield.css │ ├── _sign-in-overlay.css │ ├── _textmarkers.css │ ├── _tooltip.css │ ├── _typography.css │ ├── _ui.css │ ├── _workspace.css │ ├── main.css │ └── vendor │ │ ├── black-tie.css │ │ └── bootstrap.css └── js │ ├── components │ ├── App.jsx │ ├── AppEmbedded.jsx │ ├── Camera.jsx │ ├── ColorPalette.jsx │ ├── Divider.jsx │ ├── DocsPanel.jsx │ ├── DraggableModal.jsx │ ├── Editor.jsx │ ├── EditorCallToAction.jsx │ ├── EditorContextMenu.jsx │ ├── EditorHiddenTooltip.jsx │ ├── EditorPane.jsx │ ├── EditorTabBar.jsx │ ├── EditorTabs.jsx │ ├── ErrorsPanel.jsx │ ├── FloatingPanel.jsx │ ├── Globey.jsx │ ├── Icon.jsx │ ├── IconButton.jsx │ ├── Map.jsx │ ├── MapPanel.jsx │ ├── MapPanelBookmarks.jsx │ ├── MapPanelLocationBar.jsx │ ├── MapPanelZoom.jsx │ ├── MenuBar.jsx │ ├── MenuFullscreen.jsx │ ├── RefreshButton.jsx │ ├── SignInButton.jsx │ ├── SignInOverlay.jsx │ ├── UserAvatar.jsx │ ├── event-emitter.js │ ├── glsl-pickers │ │ ├── NumberPicker.jsx │ │ ├── Vec2Picker.jsx │ │ ├── glsl-pickers.js │ │ └── vector.js │ └── textmarkers │ │ ├── BooleanMarker.jsx │ │ ├── DropdownMarker.jsx │ │ ├── color │ │ ├── ColorMarker.jsx │ │ ├── ColorPicker.jsx │ │ ├── ColorPickerInputFields.jsx │ │ ├── ColorPickerSaturation.jsx │ │ └── color.js │ │ └── vector │ │ ├── TrackballControls.js │ │ └── VectorPicker.jsx │ ├── config.js │ ├── editor │ ├── api-keys.js │ ├── codemirror.js │ ├── codemirror │ │ ├── glsl-tangram.js │ │ ├── hint-tangram.js │ │ ├── tools.js │ │ ├── yaml-parser.js │ │ └── yaml-tangram.js │ ├── editor.js │ ├── errors.js │ ├── highlight.js │ ├── imports.js │ ├── io.js │ ├── keymap.js │ ├── suggest.js │ ├── textmarkers.js │ └── yaml-ast.js │ ├── embedded.js │ ├── file │ └── FileDrop.jsx │ ├── main.js │ ├── map │ ├── SceneLoading.jsx │ ├── actions.js │ ├── bookmarks.js │ ├── inspection.js │ ├── leaflet-hash.js │ ├── map.js │ ├── screenshot.js │ └── video.js │ ├── modals │ ├── AboutModal.jsx │ ├── CodeSnippetModal.jsx │ ├── ConfirmDialogModal.jsx │ ├── ErrorModal.jsx │ ├── ExamplesModal.jsx │ ├── LoadingSpinner.jsx │ ├── Modal.jsx │ ├── ModalRoot.jsx │ ├── ModalShield.jsx │ ├── OpenFromCloudModal.jsx │ ├── OpenGistModal.jsx │ ├── OpenUrlModal.jsx │ ├── SaveExistingToCloudModal.jsx │ ├── SaveGistModal.jsx │ ├── SaveGistSuccessModal.jsx │ ├── SaveToCloudModal.jsx │ ├── SceneItem.jsx │ ├── SceneSelectModal.jsx │ ├── ShareHostedMapModal.jsx │ ├── SupportModal.jsx │ ├── WelcomeModal.jsx │ ├── WhatsNewModal.jsx │ └── examples.json │ ├── storage │ ├── gist.js │ ├── mapzen.js │ └── migrate.js │ ├── store │ ├── actions │ │ ├── app.js │ │ ├── index.js │ │ └── settings.js │ ├── index.js │ └── reducers │ │ ├── app.js │ │ ├── errors.js │ │ ├── index.js │ │ ├── map.js │ │ ├── modals.js │ │ ├── persistence.js │ │ ├── scene.js │ │ ├── settings.js │ │ ├── system.js │ │ └── user.js │ ├── tangram-api.json │ ├── tangram-docs.json │ ├── tangram-play.js │ ├── tools │ ├── analytics.js │ ├── gist-url.js │ ├── helpers.js │ ├── thumbnail.js │ ├── url-state.js │ └── version.js │ ├── ui │ ├── fullscreen.js │ └── welcome.js │ ├── user │ ├── sign-in-window.js │ └── sign-in.js │ └── version.json ├── test ├── .eslintrc ├── api-key.spec.js ├── components │ └── Icon.spec.jsx ├── gist-url.spec.js ├── helpers.spec.js ├── karma.spec.js ├── url-state.spec.js ├── version.spec.js └── yaml-ast.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{Makefile,makefile}] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/**/*.min.js 2 | src/js/vendor/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Compiled files 4 | public/scripts/ 5 | public/stylesheets/ 6 | 7 | # Cache 8 | browserify-cache.json 9 | 10 | # Dependency directory 11 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 12 | node_modules 13 | npm-debug.log 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "at-rule-no-vendor-prefix": true, 5 | "comment-empty-line-before": null, 6 | "font-family-name-quotes": "always-where-recommended", 7 | "function-url-quotes": "always", 8 | "max-nesting-depth": 2, 9 | "media-feature-name-no-vendor-prefix": true, 10 | "no-browser-hacks": true, 11 | "property-no-vendor-prefix": true, 12 | "selector-max-compound-selectors": 3, 13 | "selector-no-qualifying-type": null, 14 | "selector-no-vendor-prefix": null, 15 | "string-quotes": "single", 16 | "value-no-vendor-prefix": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Mapzen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://img.shields.io/circleci/project/tangrams/tangram-play.svg?style=flat-square)](https://circleci.com/gh/tangrams/tangram-play/) 2 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/tangrams/tangram-chat) 3 | 4 | ![](meta/screenshot.png) 5 | 6 | # Editor for Tangram scene files 7 | 8 | Tangram Play is an interactive text editor for creating maps using Mapzen’s [Tangram](https://mapzen.com/products/tangram/). With Play, you can write and edit map styles in YAML and preview the changes live in the web browser. Start with one of Mapzen’s base maps or create your own style! 9 | 10 | Here is [a clip of Patricio's live demo](https://twitter.com/ajturner/status/652186516194762752/video/1) at [JS.Geo](http://www.jsgeo.com/) (October 2015) ([notes are here](https://github.com/mapzen/presentations/tree/master/08-2015-JSGEO)). 11 | 12 | ## Support 13 | 14 | Learn more about using [Tangram](https://mapzen.com/documentation/tangram) and [Mapzen vector tiles](https://mapzen.com/documentation/vector-tiles/) in documentation. 15 | 16 | Having a problem with Tangram Play? Do you have feedback to share? Contact Mapzen Support by emailing [tangram@mapzen.com](mailto:tangram@mapzen.com). 17 | 18 | Tangram Play is still in active development and you can have a role in it! Add bugs or feature requests as an issue on the project’s [GitHub repository](https://github.com/tangrams/tangram-play/issues). 19 | 20 | ## Contributing 21 | 22 | We welcome contributions from the community. For more information how to run Tangram Play in your local environment and get started, please see [CONTRIBUTING.md](https://github.com/tangrams/tangram-play/blob/master/CONTRIBUTING.md). 23 | 24 | ## Query string API 25 | 26 | * ```lines=[number]/[number-number]```: you highlight a line or a range of lines. Example ```lines=10-12```. 27 | 28 | * ```scene=[url.yaml]```: load a specific ```.yaml``` file using a valid url 29 | 30 | ## Keys 31 | 32 | * ```Ctrl``` + ```[number]```: Fold indentation level ```[number]``` 33 | * ```Alt``` + ```F```: fold/unfold line 34 | * ```Alt``` + ```P```: screenshot of the map 35 | 36 | Sublime-like hotkeys 37 | * ```Ctrl``` + ```F```: Search 38 | * ```Ctrl``` + ```D```: Select next occurrence 39 | * ```Alt``` + ```ArrowKeys```: move word by word 40 | * ```Shift``` + ```ArrowKeys```: Select character by character 41 | * ```Shift``` + ```Alt``` + ```ArrowKeys```: Select word by word 42 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 8.1.0 4 | environment: 5 | NODE_ENV: test 6 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" 7 | 8 | dependencies: 9 | # Yarn is pre-installed on CircleCI Ubuntu 14.04 image. 10 | override: 11 | - yarn install --ignore-optional 12 | cache_directories: 13 | - ~/.cache/yarn 14 | 15 | test: 16 | override: 17 | - yarn test 18 | 19 | # For hosting on mapzen.com. Only deploy if tests pass. Compiled files are 20 | # rebuilt for a production environment. 21 | #deployment: 22 | # # Production environment will only deploy when a release is tagged in the 23 | # # correct format (semantic version, e.g. release-v0.5.0) 24 | # production: 25 | # tag: /release-v[0-9]+\.[0-9]+\.[0-9]+/ 26 | # commands: 27 | # - aws s3 sync $CIRCLE_ARTIFACTS $AWS_PROD_DESTINATION --delete 28 | # # The latest `master` branch will auto-deploy to dev. Unstable "test" code 29 | # # should go on `staging` and will also deploy to dev. 30 | # next: 31 | # branch: master 32 | # commands: 33 | # - aws s3 sync $CIRCLE_ARTIFACTS $AWS_DEV_DESTINATION --delete 34 | # staging: 35 | # branch: staging 36 | # commands: 37 | # - aws s3 sync $CIRCLE_ARTIFACTS $AWS_DEV_DESTINATION --delete 38 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function setKarmaConfig(config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['browserify', 'mocha', 'sinon'], 5 | files: [ 6 | // Source 7 | // 'src/js/**/*.js', 8 | // // Application 9 | // 'public/stylesheets/main.css', 10 | // 'public/index.html', 11 | // Test suites 12 | 'test/**/*.js', 13 | 'test/**/*.jsx', 14 | ], 15 | 16 | exclude: [], 17 | preprocessors: { 18 | 'test/**/*.{js,jsx}': ['browserify'], 19 | }, 20 | 21 | browserify: { 22 | debug: true, 23 | extensions: ['.jsx'], 24 | transform: [ 25 | ['babelify', { presets: ['es2015', 'react'] }], 26 | 'brfs', 27 | ], 28 | // Configuration required for enzyme to work; see 29 | // http://airbnb.io/enzyme/docs/guides/browserify.html 30 | configure(bundle) { 31 | bundle.on('prebundle', () => { 32 | bundle.external('react/addons'); 33 | bundle.external('react/lib/ReactContext'); 34 | bundle.external('react/lib/ExecutionEnvironment'); 35 | }); 36 | }, 37 | }, 38 | 39 | plugins: [ 40 | 'karma-mocha', 41 | 'karma-sinon', 42 | 'karma-phantomjs-launcher', 43 | 'karma-mocha-reporter', 44 | 'karma-browserify', 45 | ], 46 | reporters: ['mocha'], 47 | 48 | port: 9876, 49 | colors: true, 50 | 51 | logLevel: config.LOG_INFO, 52 | autoWatch: false, 53 | browsers: ['PhantomJS'], 54 | 55 | singleRun: true, 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /meta/example-scenes/README.md: -------------------------------------------------------------------------------- 1 | # example scenes cache 2 | 3 | This directory contains Node scripts for fetching and saving example scene files 4 | displayed in Tangram Play's "Open an example" modal. 5 | 6 | Most of the scene files come from https://github.com/tangrams/tangram-sandbox, 7 | but can be located anywhere. Previously, Tangram Play loaded scene files and 8 | image thumbnails from the Sandbox's Rawgit cache, but this created some 9 | noticeable problems: first, Rawgit (and GitHub itself) may occasionally go 10 | down, and secondly, the image files were not optimized, resulting in long 11 | loading times. (We were downloading 18MB of images when the examples modal 12 | we opened, and it could take ten seconds to load fully, even on a good Internet 13 | connection.) 14 | 15 | So we want to (a) remove the dependency on third-party, free & unsupported 16 | content delivery services, and (b) optimize the thumbnail images for display 17 | in Tangram Play. We can automate this instead of having to download and optimize 18 | images ourselves. Two scripts are provided here. One downloads scene files 19 | and stores them in Tangram Play, the other downloads screenshot images and 20 | converts them to thumbnails, optimizing for file size. These files become 21 | static resources that will be stored directly in this repository, and deployed 22 | along with Tangram Play 23 | 24 | These scripts are designed to run only when needed, (whenever we update the 25 | examples list) so it is not part of the build or deploy process. Project 26 | maintainers are expected to run these scripts manually. When scene files or 27 | thumbnail images are updated or created, commit the changes to the repository. 28 | Be sure to update Tangram Play's `examples.json` so that the correct images 29 | appear in the "Open an example" menu. 30 | 31 | An npm script is provided for convenience. 32 | 33 | ``` 34 | npm run examples 35 | ``` 36 | 37 | ## sources 38 | 39 | One script exports an object whose keys are the filename to save to, and 40 | values are external URLs of the scene file or the screenshot image. Because this 41 | script runs rarely, and when needed, linking to Rawgit caches or GitHub URLs 42 | are acceptable here. 43 | 44 | When this script is run, all valid URLs will replace their destination files. 45 | URLs that are not valid for any reason (e.g. 404, 500 errors) are skipped with 46 | a warning. If you remove an entry or rename it, you will also have to remove 47 | the old destination files, scripts will not take care of that for you. This is 48 | because some examples may not have third-party sources so it will not delete 49 | anything that looks unfamiliar. 50 | 51 | ## scene files 52 | 53 | This is pretty simple; it downloads and saves each scene file locally. Not all 54 | scene files need to be saved; if it's hosted on mapzen.com, we can retrieve if 55 | from its canonical source. (We might revisit this if we need offline-first 56 | Tangram Play.) 57 | 58 | You can run this script by itself with this npm script: 59 | 60 | ``` 61 | npm run examples:scenes 62 | ``` 63 | 64 | ## thumbnail images 65 | 66 | To run this script, you will need a native installation of ImageMagick. 67 | On Mac OS X, you can use Homebrew: 68 | 69 | ``` 70 | brew install imagemagick 71 | ``` 72 | 73 | Each screenshot image is downloaded, resized, and cropped to the dimensions that 74 | Tangram Play expects (and at Retina resolution), optimized for file size and 75 | rendering, and saved to the `/data/scenes/thumbnails` path in this repository. 76 | 77 | You may edit this script update image dimensions. 78 | 79 | You can run this script by itself with this npm script: 80 | 81 | ``` 82 | npm run examples:thumbnails 83 | ``` 84 | -------------------------------------------------------------------------------- /meta/example-scenes/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/meta/example-scenes/basic.png -------------------------------------------------------------------------------- /meta/example-scenes/bubble-wrap-style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/meta/example-scenes/bubble-wrap-style.png -------------------------------------------------------------------------------- /meta/example-scenes/cache-scenes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A node script for fetching and saving example scene files for Tangram Play 3 | * locally. For more information, see README.md in this directory. 4 | */ 5 | 'use strict'; 6 | 7 | // Keep dependencies low, only use Node.js native modules 8 | const path = require('path'); 9 | const http = require('http'); 10 | const https = require('https'); 11 | const fs = require('fs'); 12 | 13 | const IMAGE_SOURCES = require('./sources'); 14 | const WRITE_PATH = '../../public/data/scenes'; 15 | 16 | for (let name in IMAGE_SOURCES) { 17 | const url = IMAGE_SOURCES[name].scene; 18 | 19 | // Not all scenes have urls to cache; move on to the next if that's the case 20 | if (!url) { 21 | continue; 22 | } 23 | 24 | const destination = path.resolve(__dirname, WRITE_PATH, name + '.yaml'); 25 | 26 | // Depending on the URL's protocol, Node either needs the `http` or `https` 27 | // module. 28 | const protocol = url.startsWith('https') ? https : http; 29 | 30 | protocol.get(url, (response) => { 31 | // Continuously update stream with data 32 | let body = ''; 33 | response.on('data', function (data) { 34 | body += data; 35 | }); 36 | response.on('end', function () { 37 | // Data reception is done, save it to destination 38 | fs.writeFile(destination, body, (err) => { 39 | if (err) { 40 | throw err; 41 | } 42 | console.log(`${destination} - done.`); 43 | }); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /meta/example-scenes/cache-thumbnails.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A node script for making the thumbnails for scene files displayed in 3 | * Tangram Play's "Open an example" modal. For more information, see 4 | * README.md in this directory. 5 | * 6 | * You may edit this script to update image dimensions. 7 | * 8 | * To run this script, you will need a native installation of ImageMagick. 9 | * On Mac OS X, you can use Homebrew: 10 | * 11 | * brew install imagemagick 12 | * 13 | * Then run this script with node 14 | * 15 | * node ./meta/example-scenes/cache-thumbnails.js 16 | * 17 | * Can this be done in a shell script? Probably! 18 | */ 19 | /* eslint-disable no-console */ 20 | const path = require('path'); 21 | const gm = require('gm').subClass({ imageMagick: true }); 22 | const imagemin = require('imagemin'); 23 | const imageminPngquant = require('imagemin-pngquant'); 24 | const imageminGifsicle = require('imagemin-gifsicle'); 25 | 26 | const IMAGE_SOURCES = require('./sources'); 27 | 28 | const THUMBNAIL_WIDTH = 144; 29 | const THUMBNAIL_HEIGHT = 81; 30 | const RETINA_MULTIPLIER = 2; 31 | const WRITE_PATH = '../../public/data/scenes/thumbnails'; 32 | 33 | Object.keys(IMAGE_SOURCES).forEach((name) => { 34 | const url = IMAGE_SOURCES[name].image; 35 | const forcePNG = IMAGE_SOURCES[name].forcePNG || false; 36 | const destination = path.resolve(__dirname, WRITE_PATH); 37 | 38 | // If we want to force an animated GIF to be a one-frame PNG, we fetch the 39 | // url with a `[0]` 40 | const fetchURL = forcePNG ? `${url}[0]` : url; 41 | 42 | gm(fetchURL) 43 | .format(function (err, value) { 44 | if (err) { 45 | console.log(`Error for ${url}:\n${err}`); 46 | return; 47 | } 48 | 49 | let ext = value.toLowerCase(); 50 | if (forcePNG === true) { 51 | ext = 'png'; 52 | } 53 | 54 | const filename = path.resolve(destination, `${name}.${ext}`); 55 | const width = THUMBNAIL_WIDTH * RETINA_MULTIPLIER; 56 | const height = THUMBNAIL_HEIGHT * RETINA_MULTIPLIER; 57 | 58 | this.coalesce() // This is required to fix resizing problems with animated GIFs 59 | .resize(width, height, '^') 60 | .gravity('Center') 61 | .extent(width, height) 62 | .interlace('Line') 63 | .write(filename, (error) => { 64 | if (error) { 65 | console.log(`Error writing ${filename}:\n ${error}`); 66 | return; 67 | } 68 | 69 | // Success, optimize now 70 | // Save as PNG or animated GIF, depending on source. 71 | if (ext === 'png') { 72 | imagemin([filename], destination, { 73 | plugins: [ 74 | imageminPngquant({ quality: '65-80' }), 75 | ], 76 | }).then((files) => { 77 | console.log(`${filename} - done.`); 78 | }); 79 | } else if (ext === 'gif') { 80 | imagemin([filename], destination, { 81 | use: [ 82 | imageminGifsicle({ optimizationLevel: 3 }), 83 | ], 84 | }).then(() => { 85 | console.log(`${filename} - done.`); 86 | }); 87 | } 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /meta/example-scenes/tron-style.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/meta/example-scenes/tron-style.gif -------------------------------------------------------------------------------- /meta/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/meta/screenshot.png -------------------------------------------------------------------------------- /public/data/fonts/ms-sans-serif-bold/MS Sans Serif Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/ms-sans-serif-bold/MS Sans Serif Bold.ttf -------------------------------------------------------------------------------- /public/data/fonts/ms-sans-serif-bold/license.txt: -------------------------------------------------------------------------------- 1 | The FontStruction “MS Sans Serif Bold” 2 | (https://fontstruct.com/fontstructions/show/1384862) by “lou” is licensed 3 | under a Creative Commons Attribution Share Alike license 4 | (http://creativecommons.org/licenses/by-sa/3.0/). 5 | -------------------------------------------------------------------------------- /public/data/fonts/ms-sans-serif-bold/readme.txt: -------------------------------------------------------------------------------- 1 | The font file in this archive was created using Fontstruct the free, online 2 | font-building tool. 3 | This font was created by “lou”. 4 | This font has a homepage where this archive and other versions may be found: 5 | https://fontstruct.com/fontstructions/show/1384862 6 | 7 | Try Fontstruct at http://fontstruct.com 8 | It’s easy and it’s fun. 9 | 10 | NOTE FOR FLASH USERS: Fontstruct fonts (fontstructions) are optimized for Flash. 11 | If the font in this archive is a pixel font, it is best displayed at a font-size 12 | of 11. 13 | 14 | Fontstruct is sponsored by FontShop. 15 | Visit them at https://fontshop.com 16 | FontShop is the original independent font retailer. We’ve been around since 17 | the dawn of digital type. Whether you need the right font or need to create the 18 | right font from scratch, let our 26 years of experience work for you. 19 | 20 | Fontstruct is copyright ©2017 Rob Meek 21 | 22 | LEGAL NOTICE: 23 | In using this font you must comply with the licensing terms described in the 24 | file “license.txt” included with this archive. 25 | If you redistribute the font file in this archive, it must be accompanied by all 26 | the other files from this archive, including this one. 27 | -------------------------------------------------------------------------------- /public/data/fonts/ms-sans-serif/MS Sans Serif.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/ms-sans-serif/MS Sans Serif.ttf -------------------------------------------------------------------------------- /public/data/fonts/ms-sans-serif/license.txt: -------------------------------------------------------------------------------- 1 | The FontStruction “MS Sans Serif” 2 | (https://fontstruct.com/fontstructions/show/1384746) by “lou” is licensed 3 | under a Creative Commons Attribution Share Alike license 4 | (http://creativecommons.org/licenses/by-sa/3.0/). 5 | -------------------------------------------------------------------------------- /public/data/fonts/ms-sans-serif/readme.txt: -------------------------------------------------------------------------------- 1 | The font file in this archive was created using Fontstruct the free, online 2 | font-building tool. 3 | This font was created by “lou”. 4 | This font has a homepage where this archive and other versions may be found: 5 | https://fontstruct.com/fontstructions/show/1384746 6 | 7 | Try Fontstruct at http://fontstruct.com 8 | It’s easy and it’s fun. 9 | 10 | NOTE FOR FLASH USERS: Fontstruct fonts (fontstructions) are optimized for Flash. 11 | If the font in this archive is a pixel font, it is best displayed at a font-size 12 | of 11. 13 | 14 | Fontstruct is sponsored by FontShop. 15 | Visit them at https://fontshop.com 16 | FontShop is the original independent font retailer. We’ve been around since 17 | the dawn of digital type. Whether you need the right font or need to create the 18 | right font from scratch, let our 26 years of experience work for you. 19 | 20 | Fontstruct is copyright ©2017 Rob Meek 21 | 22 | LEGAL NOTICE: 23 | In using this font you must comply with the licensing terms described in the 24 | file “license.txt” included with this archive. 25 | If you redistribute the font file in this archive, it must be accompanied by all 26 | the other files from this archive, including this one. 27 | -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /public/data/fonts/roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /public/data/fonts/source_code_pro/SourceCodePro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/source_code_pro/SourceCodePro-Black.ttf -------------------------------------------------------------------------------- /public/data/fonts/source_code_pro/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/source_code_pro/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /public/data/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf -------------------------------------------------------------------------------- /public/data/fonts/source_code_pro/SourceCodePro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/source_code_pro/SourceCodePro-Light.ttf -------------------------------------------------------------------------------- /public/data/fonts/source_code_pro/SourceCodePro-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/source_code_pro/SourceCodePro-Medium.ttf -------------------------------------------------------------------------------- /public/data/fonts/source_code_pro/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/source_code_pro/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /public/data/fonts/source_code_pro/SourceCodePro-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/fonts/source_code_pro/SourceCodePro-Semibold.ttf -------------------------------------------------------------------------------- /public/data/imgs/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/imgs/arrow_down.png -------------------------------------------------------------------------------- /public/data/imgs/docs_inspect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/imgs/docs_inspect.png -------------------------------------------------------------------------------- /public/data/imgs/docs_save_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/imgs/docs_save_menu.png -------------------------------------------------------------------------------- /public/data/imgs/docs_share_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/imgs/docs_share_menu.png -------------------------------------------------------------------------------- /public/data/imgs/docs_toolbars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/imgs/docs_toolbars.png -------------------------------------------------------------------------------- /public/data/imgs/globey.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/imgs/globey.gif -------------------------------------------------------------------------------- /public/data/imgs/globey_speech_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/imgs/globey_speech_bubble.png -------------------------------------------------------------------------------- /public/data/imgs/question.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/imgs/question.gif -------------------------------------------------------------------------------- /public/data/scenes/basic.yaml: -------------------------------------------------------------------------------- 1 | sources: 2 | nextzen: 3 | type: TopoJSON 4 | url: https://tile.nextzen.org/tilezen/vector/v1/all/{z}/{x}/{y}.topojson 5 | 6 | layers: 7 | water: 8 | data: { source: nextzen } 9 | draw: 10 | polygons: 11 | order: 2 12 | color: '#353535' 13 | earth: 14 | data: { source: nextzen } 15 | draw: 16 | polygons: 17 | order: 1 18 | color: '#555555' 19 | landuse: 20 | data: { source: nextzen } 21 | draw: 22 | polygons: 23 | order: 3 24 | color: '#666666' 25 | roads: 26 | data: { source: nextzen} 27 | filter: { not: { kind: [rail, ferry] } } 28 | draw: 29 | lines: 30 | order: 4 31 | color: '#ffffff' 32 | width: [[7,0.0px], [10, .5px], [15, .75px], [17, 5m]] 33 | highway: 34 | filter: { kind: highway } 35 | draw: 36 | lines: 37 | order: 5 38 | width: [[8,0px], [8,.25px], [11, 1.5px], [14, 2px], [16, 4px], [17, 10m]] 39 | link: 40 | filter: { is_link: true } # on- and off-ramps, etc 41 | draw: 42 | lines: 43 | width: [[8,0px], [14, 3px], [16, 5px], [18, 10m]] 44 | tunnel-link: 45 | filter: {is_tunnel: true, $zoom: {min: 13} } 46 | tunnel: 47 | filter: {is_tunnel: true } 48 | draw: 49 | lines: 50 | order: 6 51 | buildings: 52 | data: { source: nextzen } 53 | draw: 54 | polygons: 55 | order: 7 56 | color: '#999999' 57 | extrude: true 58 | -------------------------------------------------------------------------------- /public/data/scenes/blank.yaml: -------------------------------------------------------------------------------- 1 | sources: 2 | nextzen: 3 | type: TopoJSON 4 | url: https://tile.nextzen.org/tilezen/vector/v1/all/{z}/{x}/{y}.topojson 5 | 6 | layers: 7 | earth: 8 | data: { source: nextzen } 9 | draw: 10 | polygons: 11 | order: 0 12 | color: grey 13 | water: 14 | data: { source: nextzen } 15 | draw: 16 | polygons: 17 | order: 1 18 | color: lightblue 19 | -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/9845c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/9845c.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/basic.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/blueprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/blueprint.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/bubble-wrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/bubble-wrap.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/cinnabar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/cinnabar.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/crosshatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/crosshatch.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/gotham.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/gotham.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/grain-area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/grain-area.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/grain-roads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/grain-roads.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/grain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/grain.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/ikeda.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/ikeda.gif -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/lego.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/lego.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/matrix.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/patterns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/patterns.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/pericoli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/pericoli.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/press.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/radar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/radar.gif -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/refill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/refill.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/tron-legacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/tron-legacy.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/tron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/tron.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/walkabout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/walkabout.png -------------------------------------------------------------------------------- /public/data/scenes/thumbnails/zinc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangrams/tangram-play/a2d9b67d1cbe1880ab7580a802b6a8873ecf017e/public/data/scenes/thumbnails/zinc.png -------------------------------------------------------------------------------- /public/embed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tangram Play 8 | 9 | 18 | 19 | 20 |
21 | 22 | 23 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /public/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tangram Play 8 | 9 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/css/_application.css: -------------------------------------------------------------------------------- 1 | /* APPLICATION */ 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | *::before, 8 | *::after { 9 | box-sizing: border-box; 10 | } 11 | 12 | /* 13 | Elements are fixed in position to prevent layout bugs from accidentally 14 | scrolling content to weird positions. There is also a theory that this 15 | improves performance (since it never needs to move and position is never 16 | recalculated) 17 | */ 18 | html, 19 | body, 20 | .application-container { 21 | position: fixed; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | overflow: hidden; 27 | } 28 | 29 | html { 30 | font-family: var(--font-family); 31 | font-size: var(--root-font-size); 32 | 33 | /* fix Safari font weight being too light sometimes */ 34 | -webkit-font-smoothing: subpixel-antialiased; 35 | } 36 | 37 | body { 38 | margin: 0; 39 | border: 0; 40 | padding: 0; 41 | background-color: var(--ui-base-color); 42 | } 43 | 44 | /* SCROLLBARS - WEBKIT ONLY */ 45 | ::-webkit-scrollbar { 46 | width: 12px; 47 | height: 12px; 48 | } 49 | 50 | ::-webkit-scrollbar-track { 51 | background-color: var(--ui-element-color); 52 | } 53 | 54 | ::-webkit-scrollbar-thumb { 55 | background-color: #b9b9bd; 56 | } 57 | 58 | ::-webkit-scrollbar-corner { 59 | background-color: var(--ui-base-color); 60 | } 61 | 62 | /* MOBILE MESSAGE */ 63 | 64 | .mobile-message, 65 | .mobile-message * { 66 | box-sizing: border-box; 67 | } 68 | 69 | .mobile-message { 70 | display: none; 71 | position: relative; 72 | padding: 10px; 73 | padding-right: 48px; 74 | font-size: 16px; 75 | font-family: var(--font-family); 76 | font-weight: 400; 77 | background-color: lightyellow; 78 | z-index: var(--z10-mobile-message); 79 | } 80 | 81 | .mobile-message strong { 82 | font-weight: normal; 83 | color: darkred; 84 | } 85 | 86 | .mobile-message-close { 87 | position: absolute; 88 | top: 0; 89 | right: 0; 90 | width: 48px; 91 | height: 48px; 92 | padding-top: 8px; 93 | font-size: 20px; 94 | text-align: center; 95 | color: darkred; 96 | cursor: pointer; 97 | } 98 | 99 | /* APP LOADER */ 100 | /* via http://projects.lukehaas.me/css-loaders/ */ 101 | .loader-container { 102 | position: fixed; 103 | top: 0; 104 | left: 0; 105 | width: 100%; 106 | height: 100%; 107 | display: flex; 108 | justify-content: center; 109 | align-items: center; 110 | visibility: hidden; /* Turned on via js */ 111 | } 112 | 113 | .loader { 114 | font-size: 4px; 115 | margin: 50px auto; 116 | text-indent: -9999em; 117 | width: 11em; 118 | height: 11em; 119 | border-radius: 50%; 120 | background: linear-gradient(to right, var(--ui-component-text-color) 10%, rgba(255, 255, 255, 0) 42%); 121 | position: relative; 122 | animation: load3 1.4s infinite linear; 123 | transform: translateZ(0); 124 | } 125 | 126 | .loader::before { 127 | width: 50%; 128 | height: 50%; 129 | background: var(--ui-component-text-color); 130 | border-radius: 100% 0 0; 131 | position: absolute; 132 | top: 0; 133 | left: 0; 134 | content: ''; 135 | } 136 | 137 | .loader::after { 138 | background: var(--ui-base-color); 139 | width: 75%; 140 | height: 75%; 141 | border-radius: 50%; 142 | content: ''; 143 | margin: auto; 144 | position: absolute; 145 | top: 0; 146 | left: 0; 147 | bottom: 0; 148 | right: 0; 149 | } 150 | 151 | @keyframes load3 { 152 | 0% { 153 | transform: rotate(0deg); 154 | } 155 | 156 | 100% { 157 | transform: rotate(360deg); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/css/_buttons.css: -------------------------------------------------------------------------------- 1 | button { 2 | margin: 0 0.4em; 3 | border: var(--ui-border); 4 | border-radius: var(--ui-border-radius); 5 | padding: 0.8em 0.75em; 6 | font-family: var(--font-family); 7 | font-weight: 200; 8 | font-size: 0.9rem; 9 | color: var(--ui-component-text-color); 10 | background-color: var(--ui-element-color); 11 | box-shadow: var(--ui-emboss-shadow); 12 | cursor: pointer; 13 | user-select: none; 14 | } 15 | 16 | /* Buttons that are only icons */ 17 | .button-icon { 18 | text-align: center; 19 | padding: 0; 20 | margin: 0; 21 | 22 | /* Default size. Can be overridden by other classes to fit UI. */ 23 | width: 30px; 24 | height: 30px; 25 | 26 | /* Override focus style */ 27 | &:focus { 28 | border: var(--ui-border); 29 | outline: none; 30 | } 31 | 32 | .btm { 33 | padding-right: 0; 34 | margin: 0; 35 | font-size: 0.9em; 36 | font-weight: 200; 37 | -webkit-font-smoothing: subpixel-antialiased; 38 | -moz-osx-font-smoothing: auto; 39 | } 40 | } 41 | 42 | button:last-of-type { 43 | margin-right: 0; 44 | } 45 | 46 | button:first-of-type { 47 | margin-left: 0; 48 | } 49 | 50 | button:hover { 51 | background-color: var(--ui-active-color); 52 | color: var(--ui-highlight-color); 53 | } 54 | 55 | button:focus { 56 | outline: none; 57 | border: 1px solid var(--ui-highlight-color); 58 | background-color: var(--ui-active-color); 59 | } 60 | 61 | button:disabled { 62 | background-color: rgba(0, 0, 0, 0.1); 63 | color: gray; 64 | cursor: default; 65 | } 66 | 67 | button:disabled:hover { 68 | border: var(--ui-border); 69 | background-color: rgba(0, 0, 0, 0.1); 70 | color: gray; 71 | } 72 | 73 | /* Icons on buttons */ 74 | button > .btm { 75 | margin-right: 0.5em; 76 | margin-left: 0.15em; 77 | } 78 | 79 | button:disabled > .btm { 80 | color: gray !important; 81 | } 82 | 83 | /* Any icon of confirm button is green */ 84 | .button-confirm > .btm { 85 | color: green; 86 | font-weight: 600; 87 | font-size: 0.9em; 88 | } 89 | 90 | /* Any icon of cancel button is red */ 91 | .button-cancel > .btm { 92 | color: red; 93 | font-weight: 600; 94 | font-size: 0.9em; 95 | } 96 | -------------------------------------------------------------------------------- /src/css/_call-to-action.css: -------------------------------------------------------------------------------- 1 | .call-to-action { 2 | text-align: center; 3 | padding: 3em; 4 | overflow-y: auto; 5 | background-color: #26282e; 6 | border-radius: 3px; 7 | font-size: 1rem; 8 | min-width: 300px; 9 | 10 | button:first-of-type { 11 | margin-top: 2em; 12 | } 13 | 14 | button { 15 | display: block; 16 | margin: 0; 17 | margin-top: 1em; 18 | width: 100%; 19 | padding: 1.5em; 20 | font-size: 1.1em; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/css/_camera.css: -------------------------------------------------------------------------------- 1 | .camera-component { 2 | position: absolute; 3 | left: 0; 4 | top: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | background: transparent; 11 | pointer-events: none; 12 | user-select: none; 13 | z-index: var(--z04-camera); 14 | text-align: center; 15 | color: var(--ui-component-text-color); 16 | } 17 | 18 | .camera-controls { 19 | display: inline-block; 20 | padding: 1.25em; 21 | width: auto; 22 | bottom: 3em; 23 | align-self: flex-end; 24 | 25 | /* Initially not visible */ 26 | opacity: 0; 27 | visibility: hidden; 28 | } 29 | 30 | .camera-close-button, 31 | .camera-screenshot-button, 32 | .camera-record-button { 33 | width: 40px; 34 | height: 40px; 35 | margin-left: 10px; 36 | } 37 | 38 | .camera-record-button { 39 | width: 40px; 40 | height: 40px; 41 | 42 | .btm { 43 | font-weight: 800; 44 | color: var(--ui-error-color); /* Not an error, but use the same red */ 45 | -webkit-text-stroke-width: 1px; 46 | -webkit-text-stroke-color: white; 47 | } 48 | } 49 | 50 | .camera-info { 51 | margin-top: 1em; 52 | font-size: 0.8em; 53 | } 54 | 55 | /* Animation to indicate that recording is currently happening */ 56 | .camera-is-recording { 57 | .camera-record-button { 58 | animation-name: pulse; 59 | animation-duration: 1s; 60 | animation-iteration-count: infinite; 61 | } 62 | } 63 | 64 | .camera-animate-enter { 65 | .camera-controls { 66 | animation-duration: 120ms; 67 | animation-fill-mode: both; 68 | animation-name: fade-in, slide-in-up; 69 | } 70 | } 71 | 72 | .camera-animate-leave { 73 | .camera-controls { 74 | animation-duration: 180ms; 75 | animation-fill-mode: both; 76 | animation-name: fade-out, slide-out-down; 77 | } 78 | } 79 | 80 | @keyframes fade-in { 81 | from { 82 | opacity: 0; 83 | visibility: hidden; 84 | } 85 | 86 | to { 87 | opacity: 1; 88 | visibility: visible; 89 | } 90 | } 91 | 92 | @keyframes fade-out { 93 | from { 94 | opacity: 1; 95 | visibility: visible; 96 | } 97 | 98 | to { 99 | opacity: 0; 100 | visibility: hidden; 101 | } 102 | } 103 | 104 | @keyframes slide-in-up { 105 | from { transform: translate3d(0, 80%, 0); } 106 | to { transform: translate3d(0, 0, 0); } 107 | } 108 | 109 | @keyframes slide-out-down { 110 | from { transform: translate3d(0, 0, 0); } 111 | to { transform: translate3d(0, 80%, 0); } 112 | } 113 | 114 | @keyframes pulse { 115 | 0% { 116 | border-color: var(--ui-highlight-color); 117 | box-shadow: 0 0 9px #333; 118 | } 119 | 120 | 50% { 121 | border-color: var(--ui-highlight-color-2); 122 | box-shadow: 0 0 12px #ffb515; 123 | } 124 | 125 | 100% { 126 | border-color: var(--ui-highlight-color); 127 | box-shadow: 0 0 9px #333; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/css/_color-palette.css: -------------------------------------------------------------------------------- 1 | /* React: Color Palette */ 2 | 3 | .colorpalette { 4 | position: absolute; 5 | z-index: var(--z01-colorpalette); 6 | right: 18px; 7 | bottom: 12px; 8 | display: flex; 9 | flex-direction: row; 10 | } 11 | 12 | .colorpalette-color { 13 | display: inline-block; 14 | position: relative; 15 | padding-left: 2px; 16 | padding-right: 2px; 17 | } 18 | 19 | .colorpalette-square { 20 | width: 20px; 21 | height: 20px; 22 | cursor: pointer; 23 | } 24 | -------------------------------------------------------------------------------- /src/css/_divider.css: -------------------------------------------------------------------------------- 1 | /* DRAGGABLE LINE */ 2 | 3 | .divider { 4 | position: absolute; 5 | left: 0; 6 | top: 0; 7 | width: var(--divider-width); 8 | height: 100%; 9 | opacity: 1; 10 | background-color: var(--ui-component-color); 11 | z-index: var(--z01-divider); 12 | cursor: col-resize; 13 | user-select: none; 14 | transform: translate(0, 0) !important; /* Override react-draggable transform */ 15 | border-right: var(--ui-border); 16 | border-left: 1px solid var(--ui-editor-background-color); /* slightly lighter for 3dness */ 17 | box-shadow: var(--ui-emboss-shadow); 18 | } 19 | 20 | .divider:hover { 21 | background-color: var(--ui-active-color); 22 | } 23 | 24 | .divider.divider-is-dragging { 25 | background-color: var(--ui-active-color); 26 | 27 | .divider-affordance, 28 | .divider-affordance::after, 29 | .divider-affordance::before { 30 | background-color: var(--ui-highlight-color); 31 | } 32 | } 33 | 34 | .divider-affordance, 35 | .divider-affordance::after, 36 | .divider-affordance::before { 37 | display: inline-block; 38 | position: absolute; 39 | width: 4px; 40 | height: 4px; 41 | border-radius: 50%; 42 | top: 50%; 43 | background-color: var(--ui-component-text-color); 44 | } 45 | 46 | .divider-affordance { 47 | left: 2px; 48 | } 49 | 50 | .divider-affordance::after, 51 | .divider-affordance::before { 52 | content: ''; 53 | position: absolute; 54 | top: 0; /* Reset position */ 55 | } 56 | 57 | .divider-affordance::after { 58 | transform: translateY(10px); 59 | } 60 | 61 | .divider-affordance::before { 62 | transform: translateY(-10px); 63 | } 64 | -------------------------------------------------------------------------------- /src/css/_docs-panel.css: -------------------------------------------------------------------------------- 1 | /* React DocsPanel */ 2 | 3 | .docs-panel { 4 | position: absolute; 5 | z-index: var(--z04-map-toolbar); 6 | bottom: 0; 7 | right: 0; 8 | width: 100%; 9 | } 10 | 11 | /* Style for map panel button to show it */ 12 | .docs-panel .docs-panel-button-show { 13 | position: absolute; 14 | right: 0; 15 | bottom: 0; 16 | } 17 | 18 | /* Docs divider */ 19 | 20 | .docs-divider { 21 | height: 8px; 22 | cursor: row-resize; 23 | display: flex; 24 | text-align: center; 25 | justify-content: center; 26 | user-select: none; 27 | background-color: var(--ui-component-color); 28 | border-bottom: var(--ui-border); 29 | border-top: 1px solid var(--ui-editor-background-color); /* slightly lighter for 3dness */ 30 | box-shadow: var(--ui-emboss-shadow); 31 | } 32 | 33 | .docs-divider:hover { 34 | background-color: var(--ui-active-color); 35 | } 36 | 37 | .docs-divider-affordance, 38 | .docs-divider-affordance::after, 39 | .docs-divider-affordance::before { 40 | display: flex; 41 | width: 4px; 42 | height: 4px; 43 | border-radius: 50%; 44 | background-color: var(--ui-component-text-color); 45 | margin: auto; 46 | } 47 | 48 | .docs-divider-affordance::after, 49 | .docs-divider-affordance::before { 50 | content: ''; 51 | position: absolute; 52 | } 53 | 54 | .docs-divider-affordance::after { 55 | transform: translateX(10px); 56 | } 57 | 58 | .docs-divider-affordance::before { 59 | transform: translateX(-10px); 60 | } 61 | 62 | /* Style for map panel */ 63 | .docs-panel .panel { 64 | border: none; 65 | margin: 0; 66 | z-index: var(--z07-menu); 67 | } 68 | 69 | .docs-panel-collapsible { 70 | color: var(--ui-component-text-color); 71 | /* background-color: var(--ui-component-color); */ 72 | background-color: #2e3033; 73 | transform: translate(0, 0) !important; /* This is to prevent React Draggable from changing panel position */ 74 | 75 | /* Ease in and out of doc panel */ 76 | /* transition-timing-function: ease-in-out; 77 | transition: 0.1s; */ 78 | 79 | .panel-body { 80 | padding: 0; 81 | height: 100%; 82 | } 83 | 84 | .docs-panel-toolbar { 85 | display: flex; 86 | /* padding-top: 6px; */ 87 | padding-bottom: 6px; 88 | flex-direction: row; 89 | height: 100%; 90 | } 91 | } 92 | 93 | .docs-panel button { 94 | margin: 4px; 95 | height: 30px; 96 | width: 30px; 97 | } 98 | 99 | .docs-panel-toolbar-content { 100 | width: 100%; 101 | } 102 | 103 | .docs-panel-toolbar-content .container { 104 | position: relative; 105 | height: 100%; 106 | padding: 20px 16px 20px 20px; 107 | overflow-y: auto; 108 | overflow-x: hidden; 109 | } 110 | 111 | /* Toolbar buttons */ 112 | 113 | .docs-panel-toolbar-toggle { 114 | position: absolute; 115 | right: 0; 116 | } 117 | 118 | /* Content formatting */ 119 | 120 | .capitalize { 121 | text-transform: capitalize; 122 | } 123 | 124 | .child-row { 125 | padding-left: 15px; 126 | padding-bottom: 10px; 127 | } 128 | 129 | .toolbar-content-row { 130 | padding-bottom: 10px; 131 | font-size: 14px; 132 | } 133 | 134 | code { 135 | background-color: transparent; 136 | } 137 | 138 | .docs-link code { 139 | color: #bf4d6a; 140 | cursor: pointer; 141 | text-decoration: underline; 142 | } 143 | 144 | .docs-link code:hover { 145 | text-shadow: 0 0 1px #c34646; 146 | } 147 | -------------------------------------------------------------------------------- /src/css/_editor-context-menu.css: -------------------------------------------------------------------------------- 1 | .editor-context-menu { 2 | position: fixed; 3 | display: none; 4 | padding: 0; 5 | min-width: 200px; 6 | font-size: 1rem; 7 | background-color: var(--ui-component-color); 8 | border-radius: var(--ui-border-radius); 9 | border: var(--ui-border); 10 | pointer-events: auto; 11 | overflow: hidden; 12 | z-index: var(--z09-context-menu); 13 | user-select: none; 14 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2); 15 | } 16 | 17 | .editor-context-menu ul { 18 | list-style-type: none; 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | .editor-context-menu li { 24 | padding: 0.25em 0.5em; 25 | border-top: 1px solid var(--ui-active-color); 26 | cursor: pointer; 27 | line-height: 1.4; 28 | color: var(--ui-component-text-color); 29 | font-weight: 300; 30 | font-size: 0.9em; 31 | } 32 | 33 | .editor-context-menu li:hover { 34 | background-color: var(--ui-active-color); 35 | } 36 | -------------------------------------------------------------------------------- /src/css/_editor-hidden-tooltip.css: -------------------------------------------------------------------------------- 1 | .editor-hidden-tooltip { 2 | position: fixed; 3 | min-width: 175px; 4 | top: 120px; 5 | right: 18px; 6 | padding: 0.8em; 7 | padding-right: 1em; 8 | border-radius: var(--ui-border-radius); 9 | background-color: var(--ui-tooltip-background-color); 10 | font-size: 1rem; 11 | color: var(--ui-component-text-color); 12 | z-index: var(--z09-tooltip); 13 | pointer-events: none; 14 | user-select: none; 15 | 16 | /* Pointer tip */ 17 | &::after { 18 | content: ''; 19 | position: absolute; 20 | width: 0; 21 | height: 0; 22 | border-top: 12px solid transparent; 23 | border-bottom: 12px solid transparent; 24 | border-left: 12px solid var(--ui-tooltip-background-color); 25 | top: 50%; 26 | margin-top: -12px; 27 | left: 100%; 28 | } 29 | } 30 | 31 | .editor-hidden-tooltip h5 { 32 | font-weight: bold; 33 | font-size: 1em; 34 | } 35 | 36 | .editor-hidden-tooltip p { 37 | font-weight: 200; 38 | margin-bottom: 0; 39 | } 40 | -------------------------------------------------------------------------------- /src/css/_embedded-play.css: -------------------------------------------------------------------------------- 1 | .embedded .workspace-container { 2 | top: 0; 3 | height: 100%; 4 | } 5 | 6 | .embedded .refresh-button { 7 | position: absolute; 8 | z-index: var(--z09); 9 | top: 4px; 10 | right: 4px; 11 | width: 30px; 12 | height: 30px; 13 | } 14 | -------------------------------------------------------------------------------- /src/css/_errors.css: -------------------------------------------------------------------------------- 1 | /* Overrides the indentation alignment (a hack for cm's on `renderLine` event) */ 2 | .CodeMirror-linewidget { 3 | left: 0 !important; 4 | padding-left: 0 !important; 5 | } 6 | 7 | .error, 8 | .warning { 9 | position: relative; 10 | font-family: var(--editor-font-family); 11 | padding-left: 3em; /* Theoretically, aligns error text to the first tab stop */ 12 | padding-right: 10px; 13 | } 14 | 15 | .error-icon, 16 | .warning-icon { 17 | position: absolute; 18 | left: 0; 19 | top: 3px; 20 | width: 3em; /* Centers the icon within the first tab stop */ 21 | text-align: center; 22 | } 23 | 24 | .error { 25 | background: var(--ui-error-color); 26 | color: white; 27 | } 28 | 29 | .warning { 30 | background: var(--ui-warning-color); 31 | color: black; 32 | } 33 | 34 | /* Experiment. */ 35 | .errors-panel { 36 | padding: 0; /* Overrides .modal */ 37 | text-align: left; 38 | position: absolute; 39 | left: 20px; 40 | bottom: 20px; 41 | width: 400px; 42 | overflow: hidden; 43 | pointer-events: auto; 44 | 45 | .btm { 46 | position: absolute; 47 | width: 18px; 48 | top: 1px; 49 | text-align: center; 50 | font-weight: 600; 51 | } 52 | 53 | .bt-exclamation-triangle { 54 | color: var(--ui-error-color); 55 | } 56 | 57 | .bt-exclamation-circle { 58 | color: var(--ui-warning-color); 59 | } 60 | } 61 | 62 | .errors-panel-viewport { 63 | position: relative; 64 | width: 100%; 65 | max-height: 400px; /* Prevent it from getting out of hand if something generated tons of errors */ 66 | overflow: auto; 67 | } 68 | 69 | .errors-panel-content { 70 | padding: 10px; 71 | user-select: auto; 72 | 73 | a { 74 | white-space: nowrap; 75 | color: lightblue; 76 | text-decoration: underline; 77 | } 78 | } 79 | 80 | .errors-panel-item { 81 | position: relative; 82 | 83 | &:not(:first-of-type) { 84 | margin-top: 0.85em; 85 | } 86 | } 87 | 88 | .errors-panel-text { 89 | margin-left: 22px; 90 | } 91 | -------------------------------------------------------------------------------- /src/css/_filedrop.css: -------------------------------------------------------------------------------- 1 | /* DRAG & DROP FILE LOADER */ 2 | 3 | .filedrop-container { 4 | display: none; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | z-index: var(--z01-filedrop); 11 | background-color: rgba(0, 0, 0, 0.8); 12 | cursor: copy; 13 | user-select: none; 14 | pointer-events: auto; 15 | } 16 | 17 | .filedrop-indicator { 18 | pointer-events: none; 19 | position: absolute; 20 | width: 100%; 21 | height: 600px; 22 | top: 50%; 23 | margin-top: -300px; 24 | color: white; 25 | text-align: center; 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | flex-direction: column; 30 | } 31 | 32 | .filedrop-icon { 33 | font-size: 14em; 34 | } 35 | 36 | .filedrop-label { 37 | margin-top: 1.5em; 38 | font-family: var(--font-family); 39 | font-weight: 200; 40 | font-size: 2.5em; 41 | } 42 | -------------------------------------------------------------------------------- /src/css/_floating-panel.css: -------------------------------------------------------------------------------- 1 | /* Draggable floating modal for pickers and other display panels */ 2 | 3 | .floating-panel-topbar { 4 | display: flex; 5 | background-color: var(--ui-component-color); 6 | height: 20px; 7 | justify-content: flex-end; 8 | user-select: none; 9 | } 10 | 11 | .floating-panel-drag { 12 | display: flex; 13 | flex-grow: 1; 14 | padding-left: 1em; 15 | font-size: 0.8em; 16 | text-align: left; 17 | align-items: center; 18 | cursor: move; 19 | } 20 | 21 | .floating-panel-close { 22 | height: calc(100% - 1px); 23 | width: 20px; 24 | flex-basis: 20px; 25 | flex-shrink: 0; 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | text-align: center; 30 | font-size: 13px; 31 | color: var(--ui-component-text-color); 32 | cursor: pointer; 33 | 34 | /* Override button style */ 35 | padding: 0; 36 | border: 0; 37 | border-radius: 0; 38 | background-color: transparent; 39 | box-shadow: none; 40 | 41 | &:hover { 42 | color: var(--ui-highlight-color); 43 | background-color: var(--ui-active-color); 44 | } 45 | } 46 | 47 | .modal-content { 48 | margin-top: 0; 49 | } 50 | -------------------------------------------------------------------------------- /src/css/_globey.css: -------------------------------------------------------------------------------- 1 | .globey { 2 | position: absolute; 3 | left: 30px; 4 | bottom: 30px; 5 | pointer-events: none; 6 | font-family: 'MS Sans Serif', system; 7 | font-size: 11px; 8 | /* stylelint-disable-next-line property-no-unknown */ 9 | font-smooth: never; 10 | -webkit-font-smoothing: none; 11 | z-index: -1; /* Exist underneath other overlays */ 12 | 13 | img { 14 | width: 128px; 15 | pointer-events: none; 16 | } 17 | } 18 | 19 | .globey-image { 20 | position: relative; 21 | left: 26px; 22 | width: 128px; 23 | height: 128px; 24 | pointer-events: auto; 25 | image-rendering: auto; 26 | image-rendering: crisp-edges; 27 | image-rendering: pixelated; 28 | } 29 | 30 | .globey-speech-bubble { 31 | background-image: url('../data/imgs/globey_speech_bubble.png'); 32 | background-repeat: no-repeat; 33 | height: 136px; 34 | width: 192px; 35 | padding: 10px 9px; 36 | position: relative; 37 | font-size: 11px; 38 | } 39 | 40 | .globey-nav { 41 | position: absolute; 42 | bottom: 35px; 43 | width: calc(100% - 20px); 44 | } 45 | 46 | .globey-button, 47 | .globey-button:focus, 48 | .globey-button:hover, 49 | .globey-button:active { 50 | background-color: #ffc; 51 | border: 1px solid #868a8e; 52 | box-shadow: none; 53 | border-radius: 5px; 54 | pointer-events: auto; 55 | font-family: 'MS Sans Serif', system; 56 | font-size: 11px; 57 | color: black; 58 | padding: 4px; 59 | min-width: 72px; 60 | } 61 | 62 | .globey-button:focus { 63 | outline: 1px dotted black; 64 | outline-offset: -4px; 65 | } 66 | 67 | .globey-button:active { 68 | padding-top: 5px; 69 | padding-left: 6px; 70 | padding-bottom: 3px; 71 | } 72 | 73 | .globey-button-right { 74 | float: right; 75 | } 76 | -------------------------------------------------------------------------------- /src/css/_icon.css: -------------------------------------------------------------------------------- 1 | /* Works with Black Tie icons */ 2 | .icon-active { 3 | color: yellow !important; 4 | font-weight: 700; 5 | } 6 | -------------------------------------------------------------------------------- /src/css/_inputs.css: -------------------------------------------------------------------------------- 1 | /* Inputs */ 2 | 3 | .map-toolbar { 4 | ::-webkit-input-placeholder { 5 | color: #909195; 6 | } 7 | 8 | :-moz-placeholder { 9 | color: #909195; 10 | } 11 | 12 | ::-moz-placeholder { 13 | color: #909195; 14 | } 15 | 16 | :-ms-input-placeholder { 17 | color: #909195; 18 | } 19 | 20 | /* Much dislike Black Tie's pick of font smoothing */ 21 | .btm { 22 | -webkit-font-smoothing: subpixel-antialiased; 23 | -moz-osx-font-smoothing: auto; 24 | } 25 | 26 | button { 27 | appearance: none; 28 | box-sizing: border-box; 29 | border: 0; 30 | padding: 0; 31 | height: 38px; 32 | width: 38px; 33 | font-size: 12px; 34 | background-color: var(--ui-element-color); 35 | color: var(--ui-component-text-color); 36 | cursor: pointer; 37 | outline: none; 38 | } 39 | 40 | button.active { 41 | background-color: var(--ui-active-color); 42 | color: var(--ui-highlight-color); 43 | } 44 | 45 | button:hover { 46 | background-color: var(--ui-active-color); 47 | color: var(--ui-highlight-color); 48 | } 49 | } 50 | 51 | .input-bar { 52 | display: flex; 53 | flex-direction: row; 54 | } 55 | 56 | input[type=text], 57 | input[type=url] { 58 | background-color: var(--ui-well-color); 59 | appearance: none; 60 | border: var(--ui-border); 61 | border-radius: var(--ui-border-radius); 62 | box-shadow: var(--ui-well-shadow); 63 | color: var(--ui-component-text-color); 64 | font-size: 12px; 65 | font-family: var(--font-family); 66 | font-weight: 200; 67 | line-height: 24px; 68 | padding: 4px 8px; 69 | width: 100%; 70 | box-sizing: border-box; 71 | } 72 | 73 | input { 74 | outline: none; 75 | } 76 | 77 | input:focus { 78 | border: 1px solid var(--ui-highlight-color); 79 | } 80 | 81 | input:invalid { 82 | border: 1px solid red; 83 | } 84 | 85 | input::selection { 86 | background-color: var(--ui-component-color); 87 | } 88 | 89 | label + input { 90 | margin-top: 6px; 91 | } 92 | 93 | label { 94 | font-size: 13px; 95 | font-weight: 400; 96 | color: var(--ui-subtext-color); 97 | } 98 | -------------------------------------------------------------------------------- /src/css/_leaflet-overrides.css: -------------------------------------------------------------------------------- 1 | .leaflet-container { 2 | background-color: #222; 3 | } 4 | -------------------------------------------------------------------------------- /src/css/_map-loading.css: -------------------------------------------------------------------------------- 1 | /* MAP LOADING INDICATOR */ 2 | 3 | .map-loading { 4 | position: absolute; 5 | height: 10px; 6 | width: 100%; 7 | left: 0; 8 | bottom: -10px; 9 | transition: 80ms bottom ease-out; 10 | animation: barberpole 1s linear infinite; 11 | background-size: 30px 30px; 12 | background-image: linear-gradient(135deg, #6b6b70 25%, var(--ui-active-color) 25%, var(--ui-active-color) 50%, #6b6b70 50%, #6b6b70 75%, var(--ui-active-color) 75%, var(--ui-active-color)); 13 | user-select: none; 14 | z-index: var(--z04-map-loading); 15 | 16 | &.map-loading-show { 17 | bottom: 0; 18 | } 19 | } 20 | 21 | @keyframes barberpole { 22 | from { 23 | background-position: 0 0; 24 | } 25 | 26 | to { 27 | background-position: 60px 30px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/css/_map-panel.css: -------------------------------------------------------------------------------- 1 | /* React MapPanel */ 2 | 3 | .map-panel { 4 | position: relative; 5 | z-index: var(--z04-map-toolbar); 6 | font-size: 1rem; 7 | } 8 | 9 | /* Style for map panel button to show it */ 10 | .map-panel-button-show { 11 | position: absolute; 12 | top: 6px; 13 | right: 4px; 14 | } 15 | 16 | /* Style for map panel */ 17 | .map-panel .panel { 18 | border-bottom: var(--ui-border); 19 | z-index: var(--z07-menu); 20 | } 21 | 22 | .map-panel-collapsible { 23 | color: var(--ui-component-text-color); 24 | background-color: var(--ui-editor-background-color); 25 | 26 | .panel-body { 27 | padding: 4px; 28 | padding-top: 6px; 29 | } 30 | 31 | .map-panel-toolbar { 32 | display: flex; 33 | align-items: center; 34 | } 35 | } 36 | 37 | /* Style for all map panel buttons */ 38 | .map-panel button { 39 | height: 32px; 40 | width: 32px; 41 | } 42 | 43 | .map-panel .btn-group { 44 | margin-left: 2px; 45 | } 46 | 47 | /* Style for button groups */ 48 | .map-panel-zoom-container { 49 | display: flex; 50 | } 51 | 52 | .map-panel-zoom { 53 | width: 36px; 54 | margin-right: 4px; 55 | font-size: 0.75em; 56 | text-align: center; 57 | line-height: 32px; 58 | flex: 0 0 auto; 59 | } 60 | 61 | .buttons-plusminus { 62 | display: flex; 63 | flex: 0 0 auto; 64 | 65 | .btm { 66 | font-size: 0.8em; /* slightly smaller */ 67 | } 68 | } 69 | 70 | .buttons-locate { 71 | flex: 0 0 auto; 72 | } 73 | 74 | .buttons-toggle { 75 | flex: 0 0 auto; 76 | } 77 | -------------------------------------------------------------------------------- /src/css/_map.css: -------------------------------------------------------------------------------- 1 | /* MAP VIEWPORT */ 2 | 3 | /* Container for map and map-related UI */ 4 | .map-container { 5 | position: absolute; 6 | left: 0; 7 | top: 0; 8 | height: 100%; 9 | width: 50%; /* Starting position; this is overridden by scripts */ 10 | overflow: hidden; 11 | } 12 | 13 | /* Container for Leaflet/Tangram */ 14 | .map-view, 15 | .map-view-not-loaded { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | width: 100%; 20 | height: 100%; 21 | } 22 | 23 | .map-view-not-loaded { 24 | background-image: url('../data/imgs/question.gif'); 25 | background-repeat: no-repeat; 26 | background-position: center center; 27 | background-color: var(--ui-base-color); 28 | z-index: var(--z05-map-not-loaded); 29 | } 30 | -------------------------------------------------------------------------------- /src/css/_modals.code-snippet.css: -------------------------------------------------------------------------------- 1 | 2 | .code-snippet-modal { 3 | width: 660px; 4 | } 5 | 6 | .code-snippet-textarea-container { 7 | width: 600px; 8 | height: 500px; 9 | margin-bottom: 1em; 10 | 11 | textarea { 12 | width: 100%; 13 | height: 100%; 14 | resize: none; 15 | font-family: var(--editor-font-family); 16 | font-size: 1em; 17 | padding: 1em; 18 | white-space: nowrap; 19 | } 20 | } 21 | 22 | /* TODO: Generalize tab stuff. these are for non-editor tabs */ 23 | .code-snippet-tabs { 24 | display: inline-block; 25 | } 26 | 27 | .code-snippet-tab { 28 | display: inline-block; 29 | padding: 0.65em 1em; 30 | border: 1px solid var(--ui-border-color); 31 | border-bottom: 0; 32 | border-top-left-radius: var(--ui-border-radius); 33 | border-top-right-radius: var(--ui-border-radius); 34 | margin-right: 3px; 35 | margin-bottom: -1px; 36 | cursor: pointer; 37 | background-color: var(--ui-base-color); 38 | box-shadow: inset 0 1px 2px 0 var(--ui-component-color); 39 | } 40 | 41 | .code-snippet-tab:hover { 42 | color: var(--ui-highlight-color); 43 | } 44 | 45 | .code-snippet-tab-selected { 46 | color: var(--ui-highlight-color); 47 | background-color: var(--ui-element-color); 48 | box-shadow: var(--ui-emboss-shadow); 49 | } 50 | 51 | .code-snippet-tabs-right { 52 | display: inline-block; 53 | float: right; 54 | line-height: 30px; 55 | } 56 | -------------------------------------------------------------------------------- /src/css/_modals.css: -------------------------------------------------------------------------------- 1 | /* MODALS */ 2 | 3 | .modal { 4 | display: block; /* Default display mode */ 5 | width: 400px; 6 | padding: 30px; 7 | font-family: var(--font-family); 8 | font-weight: 200; 9 | font-size: 1rem; 10 | text-align: center; 11 | color: var(--ui-component-text-color); 12 | background-color: var(--ui-base-color); 13 | box-shadow: var(--ui-modal-shadow), var(--ui-emboss-shadow); 14 | z-index: var(--z01-modal); 15 | pointer-events: auto; 16 | border: 1px solid var(--ui-border-color); 17 | border-radius: 2px; 18 | } 19 | 20 | /* 21 | For error modals we want the message to be user-selectable so it can be 22 | copy-pasted elsewhere. 23 | */ 24 | .modal:not(.error-modal) { 25 | user-select: none; 26 | cursor: default; 27 | } 28 | 29 | .modal-alt { 30 | text-align: left; 31 | 32 | .modal-buttons { 33 | justify-content: flex-end; 34 | } 35 | } 36 | 37 | .modal-text { 38 | display: inline-block; 39 | margin: 0; 40 | padding: 0; 41 | line-height: 1.4; 42 | 43 | a { 44 | text-decoration: underline; 45 | } 46 | } 47 | 48 | .modal-content { 49 | margin-top: 20px; 50 | } 51 | 52 | /* used in save-to modals */ 53 | .modal-content p { 54 | margin-top: 1em; 55 | } 56 | 57 | .modal-well { 58 | border: var(--ui-border); 59 | box-shadow: var(--ui-well-shadow); 60 | } 61 | 62 | .modal-buttons { 63 | width: 100%; 64 | margin-top: 24px; 65 | display: flex; 66 | flex-flow: row nowrap; 67 | justify-content: center; 68 | } 69 | 70 | .loading-spinner { 71 | margin-right: 30px; 72 | line-height: 40px; 73 | color: var(--ui-subtext-color); 74 | opacity: 0; 75 | visibility: hidden; 76 | 77 | &.loading-spinner-on { 78 | opacity: 1; 79 | visibility: visible; 80 | } 81 | 82 | .bt-spinner { 83 | margin-right: 0.25em; 84 | } 85 | } 86 | 87 | .open-url-modal { 88 | width: 600px; 89 | } 90 | 91 | .modal-about-text { 92 | margin-left: 40px; 93 | margin-right: 40px; 94 | 95 | p { 96 | font-size: 12px; 97 | margin-top: 16px; 98 | margin-bottom: 0; 99 | 100 | /* Reset text selection state so that text is copy-pastable */ 101 | user-select: auto; 102 | cursor: auto; 103 | } 104 | } 105 | 106 | .save-to-cloud-modal { 107 | width: 600px; 108 | } 109 | 110 | .save-to-cloud-success-modal { 111 | /* Overrides for this modal. 112 | TODO: Consider refactoring modal so we don't need any of this */ 113 | /* Inputs */ 114 | .saved-scene-copy-btn { 115 | margin-left: 2px; 116 | width: 40px; 117 | height: 34px; 118 | } 119 | } 120 | 121 | .save-existing-to-cloud-modal { 122 | width: 500px; 123 | 124 | /* re-using this CSS from OpenFromCloudModal */ 125 | .open-scene-option { 126 | padding: 0; 127 | cursor: auto; 128 | user-select: auto; 129 | } 130 | 131 | .open-scene-option:hover { 132 | background-color: transparent; 133 | } 134 | } 135 | 136 | .modal { 137 | position: relative; 138 | } 139 | -------------------------------------------------------------------------------- /src/css/_modals.open-scene.css: -------------------------------------------------------------------------------- 1 | /* MODALS · OPEN SCENES */ 2 | 3 | .open-scene-modal { 4 | width: 600px; 5 | /* Allow the height of this modal to expand on larger screens */ 6 | height: calc(100% - 400px); 7 | min-height: 600px; 8 | max-height: 1200px; 9 | } 10 | 11 | .open-scene-list { 12 | /* Ensure this fits within a variable-height modal. 13 | TODO: Don't use a magic number */ 14 | height: calc(100% - 94px); 15 | padding: 10px; 16 | overflow-x: hidden; 17 | overflow-y: auto; 18 | background-color: var(--ui-component-color); 19 | } 20 | 21 | .open-scene-option { 22 | display: flex; 23 | flex-direction: row; 24 | padding: 10px; 25 | cursor: pointer; 26 | 27 | &:hover { 28 | background-color: var(--ui-active-color); 29 | } 30 | } 31 | 32 | .open-scene-deleting { 33 | background-color: transparent; 34 | } 35 | 36 | /* We need a container element for the image so we can highlight, see below */ 37 | .open-scene-option-thumbnail { 38 | position: relative; 39 | margin-right: 10px; 40 | line-height: 0; /* Removes baseline from image */ 41 | 42 | img { 43 | display: block; /* Force display of img tag without a `src` attribute in Firefox */ 44 | width: 144px; 45 | height: 81px; 46 | } 47 | } 48 | 49 | /* Use .open-scene-selected for focus state */ 50 | .open-scene-selected { 51 | background-color: var(--ui-active-color); 52 | 53 | &:focus { 54 | outline: none; 55 | } 56 | } 57 | 58 | .open-scene-selected:not(.open-scene-deleting) { 59 | .open-scene-option-name { 60 | color: var(--ui-highlight-color); 61 | } 62 | } 63 | 64 | /* box-shadow is overlapped by the image, so a pseudo element puts it in the right place */ 65 | .open-scene-selected:not(.open-scene-deleting) .open-scene-option-thumbnail::after { 66 | position: absolute; 67 | display: block; 68 | content: ''; 69 | top: 0; 70 | left: 0; 71 | /* Ensure same dimensions as thumbnail image */ 72 | width: 144px; 73 | height: 81px; 74 | box-shadow: inset 0 0 0 1px var(--ui-highlight-color); 75 | } 76 | 77 | .open-scene-option-info { 78 | flex-grow: 1; 79 | margin-right: 10px; 80 | display: flex; 81 | flex-direction: column; 82 | } 83 | 84 | .open-scene-option-name { 85 | line-height: 24px; 86 | font-size: 1.1em; 87 | } 88 | 89 | .open-scene-option-description { 90 | line-height: 1.4; 91 | flex-grow: 1; 92 | color: var(--ui-subtext-color); 93 | } 94 | 95 | .open-scene-option-date { 96 | font-size: 0.8em; 97 | color: var(--ui-subtext-color); 98 | /* Breathing room in case description gets too close to where the date output is. */ 99 | margin-top: 0.5em; 100 | } 101 | 102 | .open-scene-deleting { 103 | cursor: default; 104 | 105 | .open-scene-option-name, 106 | .open-scene-option-description, 107 | .open-scene-option-date { 108 | color: gray; 109 | } 110 | 111 | .open-scene-option-thumbnail { 112 | opacity: 0.35; 113 | } 114 | } 115 | 116 | .open-scene-option-tasks { 117 | visibility: hidden; 118 | } 119 | 120 | .open-scene-option:hover .open-scene-option-tasks { 121 | visibility: visible; 122 | } 123 | -------------------------------------------------------------------------------- /src/css/_modals.welcome.css: -------------------------------------------------------------------------------- 1 | /* MODALS · WHAT'S NEW? */ 2 | 3 | .welcome-modal { 4 | width: 600px; 5 | } 6 | 7 | .modal-welcome-text p { 8 | text-align: left; 9 | padding: 1em 3.25em; 10 | } 11 | 12 | .welcome-modal .call-to-action { 13 | padding-top: 0; 14 | padding-bottom: 0; 15 | margin-top: -1em; 16 | overflow: hidden; 17 | text-align: left; 18 | display: flex; 19 | 20 | button { 21 | display: inline-block; 22 | width: 50%; 23 | height: 80px; 24 | margin-top: 0 !important; 25 | } 26 | 27 | button:first-of-type { 28 | margin-right: 1em; 29 | } 30 | } 31 | 32 | .whatsnew-modal { 33 | width: 600px; 34 | /* Allow the height of this modal to expand on larger screens */ 35 | height: calc(100% - 400px); 36 | min-height: 600px; 37 | max-height: 1200px; 38 | } 39 | 40 | .whatsnew-modal-changelog { 41 | /* Ensure this fits within a variable-height modal. 42 | TODO: Don't use a magic number */ 43 | height: calc(100% - 94px); 44 | box-sizing: border-box; 45 | } 46 | 47 | .changelog-frame { 48 | border: 0; 49 | width: 100%; 50 | height: 100%; 51 | } 52 | 53 | .changelog { 54 | overflow-x: hidden; 55 | overflow-y: auto; 56 | padding: 1em; 57 | background-color: var(--ui-component-color); 58 | color: var(--ui-component-text-color); 59 | font-weight: 300; 60 | line-height: 1.4; 61 | 62 | a { 63 | color: lightblue; 64 | text-decoration: underline; 65 | } 66 | 67 | h2 { 68 | margin-top: 0; 69 | font-size: 1.65em; 70 | padding-bottom: 0.25em; 71 | border-bottom: 2px solid var(--ui-element-color); 72 | } 73 | 74 | h3 { 75 | font-size: 1.25em; 76 | } 77 | 78 | article:not(:first-of-type) { 79 | border-top: 3px solid var(--ui-active-color); 80 | padding-top: 1em; 81 | } 82 | 83 | img { 84 | width: 100%; 85 | } 86 | 87 | .changelog-image { 88 | background-size: cover; 89 | background-position: center center; 90 | height: 200px; 91 | width: 100%; 92 | margin-top: 1em; 93 | margin-bottom: 1em; 94 | } 95 | 96 | ul { 97 | padding-left: 2em; 98 | } 99 | 100 | li { 101 | margin-top: 1em; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/css/_overlay.css: -------------------------------------------------------------------------------- 1 | /* OVERLAYS */ 2 | 3 | /* Container for all overlays */ 4 | .overlay-container, 5 | .modals-container, 6 | .modal-container { 7 | position: fixed; 8 | left: 0; 9 | top: 0; 10 | height: 100%; 11 | width: 100%; 12 | overflow: hidden; 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | z-index: var(--z08-overlay); 17 | } 18 | 19 | .overlay-container { 20 | pointer-events: none; 21 | } 22 | -------------------------------------------------------------------------------- /src/css/_sandbox.css: -------------------------------------------------------------------------------- 1 | #a-sandbox { 2 | margin-top: -33px; 3 | position: relative; 4 | width: 134px; 5 | height: 134px; 6 | padding: 0; 7 | background: #fc0; 8 | box-shadow: 5px 5px 10px #111; 9 | z-index: var(--z01-sandbox); 10 | } 11 | 12 | #a-sandbox::after { 13 | content: ''; 14 | position: absolute; 15 | border-style: solid; 16 | border-width: 9px 0 9px 10px; 17 | border-color: transparent #fc0; 18 | display: block; 19 | width: 0; 20 | z-index: var(--z01-sandbox); 21 | right: -10px; 22 | top: 14px; 23 | } 24 | 25 | #a-sandbox-canvas { 26 | margin: 2px; 27 | z-index: var(--z01-sandbox); 28 | overflow: hidden; 29 | mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC'); 30 | } 31 | 32 | #a-sandbox-colorpicker { 33 | position: relative; 34 | top: -132px; 35 | left: 112px; 36 | border: 1px solid #aaa; 37 | border-radius: 50%; 38 | width: 17px; 39 | height: 17px; 40 | box-sizing: border-box; 41 | box-shadow: inset 0 0 0 2px white; 42 | cursor: pointer; 43 | background-color: white; 44 | } 45 | -------------------------------------------------------------------------------- /src/css/_shield.css: -------------------------------------------------------------------------------- 1 | /* SHIELD */ 2 | 3 | .shield { 4 | display: none; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | background-color: rgba(0, 0, 0, 0.5); 11 | user-select: none; 12 | pointer-events: auto; 13 | } 14 | -------------------------------------------------------------------------------- /src/css/_sign-in-overlay.css: -------------------------------------------------------------------------------- 1 | .sign-in-overlay { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | color: white; 7 | } 8 | 9 | .sign-in-overlay-title { 10 | margin-bottom: 1em; 11 | } 12 | -------------------------------------------------------------------------------- /src/css/_textmarkers.css: -------------------------------------------------------------------------------- 1 | /* CodeMirror text markers */ 2 | 3 | .CodeMirror-widget { 4 | /* Resets this property for wrapped lines. The hanging indent adds 5 | its own text-indent and we do not want widgets to inherit it. */ 6 | text-indent: initial; 7 | } 8 | 9 | /* CodeMirror text markers and popups */ 10 | /* Root element inserted by CodeMirror */ 11 | .textmarker-root { 12 | display: inline-block; 13 | position: relative; 14 | box-sizing: border-box; 15 | background-color: none; 16 | user-select: none; 17 | } 18 | 19 | /* Generic top-level namespace for textmarker elements, which will be a direct 20 | child element inserted into the root element. */ 21 | .textmarker { 22 | display: inline-block; 23 | position: relative; 24 | margin: 0 0.5em; 25 | user-select: none; 26 | } 27 | 28 | /* TODO: Rename, merge with floating-panel ? */ 29 | .widget-modal { 30 | display: inline-flex !important; 31 | width: auto; 32 | padding: 0; 33 | outline: none; 34 | box-shadow: var(--ui-modal-shadow); 35 | background-color: var(--ui-component-color); 36 | z-index: var(--z09-pickers); 37 | } 38 | 39 | /* INDIVIDUAL TEXT MARKERS AND PICKERS */ 40 | /* *********************************** */ 41 | 42 | /* GLSL pickers */ 43 | 44 | .glsl-picker-canvas { 45 | background-color: var(--ui-element-color); 46 | } 47 | 48 | /* Boolean (true/false toggle) */ 49 | 50 | .textmarker-boolean { 51 | box-sizing: border-box; 52 | border: 1px solid rgba(255, 255, 255, 0.15); 53 | border-radius: var(--ui-border-radius); 54 | width: 1em; 55 | height: 1em; 56 | cursor: pointer; 57 | margin-top: -0.15em; 58 | display: flex; 59 | overflow: hidden; 60 | } 61 | 62 | .textmarker-boolean label { 63 | display: flex; 64 | } 65 | 66 | .textmarker-boolean input[type=checkbox] { 67 | position: relative; 68 | outline: none; 69 | margin-top: -0.15em; 70 | cursor: pointer; 71 | } 72 | 73 | .textmarker-boolean input[type=checkbox]::after { 74 | content: '\2717'; 75 | color: white; 76 | visibility: visible; 77 | display: flex; 78 | text-align: center; 79 | justify-content: center; 80 | background-color: var(--ui-element-color); 81 | } 82 | 83 | .textmarker-boolean input[type=checkbox]:checked::after { 84 | content: '\2713'; 85 | color: white; 86 | } 87 | 88 | /* Dropdown selector */ 89 | 90 | .textmarker-dropdown { 91 | display: inline-flex; 92 | position: relative; 93 | font-weight: 400; 94 | margin-left: 10px; 95 | margin-right: 10px; 96 | outline: none; 97 | border-radius: 3px; 98 | } 99 | 100 | .textmarker-dropdown select { 101 | font-family: var(--font-family); 102 | font-size: 100%; 103 | font-weight: 400; 104 | outline: none; 105 | background-color: #5e6064; 106 | color: white; 107 | border: 0; 108 | height: 1.2em; 109 | width: 1em; /* Remove width to get full dropdown to appear */ 110 | border-radius: 4px; 111 | padding-left: 16px; 112 | } 113 | 114 | .textmarker-dropdown option { 115 | outline: none; 116 | margin-left: 20px; 117 | } 118 | 119 | select { 120 | appearance: none; 121 | outline: none; 122 | border: 0; 123 | width: 100%; 124 | background: #fff url('../data/imgs/arrow_down.png') no-repeat; 125 | background-size: 16px; 126 | background-position: left center; 127 | } 128 | 129 | /* Vector picker */ 130 | 131 | /* Should probably simplify this for color picker too. 132 | just refers to button being clicked to open modal */ 133 | .textmarker-vectorpicker { 134 | box-sizing: border-box; 135 | border: 1px solid rgba(255, 255, 255, 0.15); 136 | max-width: 14px; 137 | max-height: 14px; 138 | width: 1em; 139 | height: 1em; 140 | cursor: pointer; 141 | vertical-align: middle; 142 | margin-top: -0.15em; 143 | } 144 | -------------------------------------------------------------------------------- /src/css/_tooltip.css: -------------------------------------------------------------------------------- 1 | /* TOOLTIP REACT */ 2 | 3 | .tooltip-arrow { 4 | border-width: 0 8px 8px; 5 | transform: translateX(-3px); 6 | } 7 | 8 | .tooltip-inner { 9 | /* Margins create room between edge of a tooltip and edge of the screen. 10 | Note that this also creates a gap between side arrow so do not use them. */ 11 | margin-left: 0.5em; 12 | margin-right: 0.5em; 13 | padding: 0.5em 0.75em; 14 | border-radius: var(--ui-border-radius); 15 | font-size: 0.75rem; 16 | font-weight: 200; 17 | color: var(--ui-highlight-color); 18 | } 19 | -------------------------------------------------------------------------------- /src/css/_typography.css: -------------------------------------------------------------------------------- 1 | /* TYPOGRAPHY */ 2 | @import url('https://fonts.googleapis.com/css?family=Roboto:400,700,300|Source+Code+Pro:400,200,600'); 3 | 4 | @font-face { 5 | font-family: 'MS Sans Serif'; 6 | src: url('../data/fonts/ms-sans-serif/MS Sans Serif.ttf') format('truetype'); 7 | font-weight: 400; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: 'MS Sans Serif'; 13 | src: url('../data/fonts/ms-sans-serif-bold/MS Sans Serif Bold.ttf') format('truetype'); 14 | font-weight: 600; 15 | font-style: normal; 16 | } 17 | -------------------------------------------------------------------------------- /src/css/_ui.css: -------------------------------------------------------------------------------- 1 | /* GENERAL UI HELPERS */ 2 | 3 | /* Generic active class */ 4 | .active { 5 | color: var(--ui-highlight-color); 6 | } 7 | 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6 { 14 | margin-bottom: 1em; 15 | font-weight: 500; 16 | 17 | &:first-child { 18 | margin-top: 0; 19 | } 20 | } 21 | 22 | h2 { 23 | font-size: 1.8em; 24 | font-weight: 200; 25 | } 26 | 27 | h4 { 28 | font-size: 1.25em; 29 | } 30 | 31 | p { 32 | margin: 0 0 1em; 33 | } 34 | 35 | /* Links ? */ 36 | a, 37 | a:visited, 38 | a:hover, 39 | a:active { 40 | color: var(--ui-link-text-color); 41 | } 42 | 43 | hr { 44 | margin-top: 20px; 45 | margin-bottom: 20px; 46 | border: 1px solid var(--ui-element-color); 47 | } 48 | -------------------------------------------------------------------------------- /src/css/_workspace.css: -------------------------------------------------------------------------------- 1 | /* Editor workspace */ 2 | 3 | .workspace-container { 4 | position: fixed; 5 | top: var(--menu-bar-height); 6 | left: 0; 7 | /* Setting absolute right position instead of width: 100% improves resizing 8 | performance when dragging the left edge of window */ 9 | right: 0; 10 | bottom: 0; 11 | background-color: var(--ui-editor-background-color); /* Background color during loading */ 12 | } 13 | 14 | #draggable-container { 15 | position: fixed; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | -------------------------------------------------------------------------------- /src/js/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import Map from './Map'; 4 | import EditorPane from './EditorPane'; 5 | import MenuBar from './MenuBar'; 6 | import FileDrop from '../file/FileDrop'; 7 | import SignInOverlay from './SignInOverlay'; 8 | // import ColorPalette from './ColorPalette'; 9 | import ErrorsPanel from './ErrorsPanel'; 10 | // todo: combine 11 | import ModalShield from '../modals/ModalShield'; 12 | import ModalRoot from '../modals/ModalRoot'; 13 | import Globey from './Globey'; 14 | 15 | import { initTangramPlay } from '../tangram-play'; 16 | import { showWelcomeScreen } from '../ui/welcome'; 17 | import store from '../store'; 18 | 19 | export default class App extends React.Component { 20 | componentDidMount() { 21 | initTangramPlay(); 22 | showWelcomeScreen(); 23 | } 24 | 25 | shouldComponentUpdate() { 26 | return false; 27 | } 28 | 29 | render() { 30 | return ( 31 | 32 |
33 | 34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 | {/* */} 47 |
48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 |
60 | 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/js/components/AppEmbedded.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import Map from './Map'; 4 | import EditorPane from './EditorPane'; 5 | import RefreshButton from './RefreshButton'; 6 | 7 | import store from '../store'; 8 | import { initTangramPlay } from '../tangram-play'; 9 | 10 | /** 11 | * This class is identical to normal Tangram Play but represents an embedded version of the app 12 | */ 13 | export default class AppEmbedded extends React.Component { 14 | componentDidMount() { 15 | initTangramPlay(); 16 | } 17 | 18 | shouldComponentUpdate() { 19 | return false; 20 | } 21 | 22 | render() { 23 | return ( 24 | 25 |
26 |
27 |
28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 | 36 |
37 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/js/components/DraggableModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Draggable from 'react-draggable'; 4 | import ModalDialog from 'react-bootstrap/lib/ModalDialog'; 5 | 6 | /** 7 | * Represents a draggable container for a modal 8 | * Important: attaches to a div with a class 'drag' 9 | * 10 | * This is a workaround for Draggable not working directly with react-bootstrap 11 | * Modal components. See here. https://github.com/mzabriskie/react-draggable/issues/56 12 | */ 13 | export default function DraggableModal(props) { 14 | return ( 15 | 21 | 22 | 23 | ); 24 | } 25 | 26 | DraggableModal.propTypes = { 27 | x: PropTypes.number.isRequired, 28 | y: PropTypes.number.isRequired, 29 | }; 30 | -------------------------------------------------------------------------------- /src/js/components/EditorCallToAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | // Redux 6 | import store from '../store'; 7 | import { SHOW_MODAL } from '../store/actions'; 8 | 9 | class EditorCallToAction extends React.PureComponent { 10 | // eslint-disable-next-line class-methods-use-this 11 | onClickExample() { 12 | store.dispatch({ 13 | type: SHOW_MODAL, 14 | modalType: 'OPEN_EXAMPLE', 15 | }); 16 | } 17 | 18 | render() { 19 | // Don't flash this when Tangram Play is initializing; 20 | // files are still zero, but we won't prompt until after 21 | if (this.props.appInitialized === false) return null; 22 | 23 | // Return nothing if editor contains files 24 | if (this.props.files.length > 0) return null; 25 | 26 | return ( 27 |
28 |
29 |

Nothing is loaded in Tangram Play.

30 | 31 | 32 | 33 | {/* Other ideas // 34 |

Some of your recent scenes

35 | 36 |
    37 |
  • Like this one
  • 38 |
  • Or this one
  • 39 |
40 | 41 |

42 | Or drag and drop a scene file from your computer onto this window! 43 |

44 | */} 45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | EditorCallToAction.propTypes = { 52 | appInitialized: PropTypes.bool, 53 | files: PropTypes.arrayOf(PropTypes.object), 54 | }; 55 | 56 | EditorCallToAction.defaultProps = { 57 | appInitialized: false, 58 | files: [], 59 | }; 60 | 61 | function mapStateToProps(state) { 62 | return { 63 | appInitialized: state.app.initialized, 64 | files: state.scene.files, 65 | }; 66 | } 67 | 68 | export default connect(mapStateToProps)(EditorCallToAction); 69 | -------------------------------------------------------------------------------- /src/js/components/EditorContextMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class EditorContextMenu extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.state = { 8 | display: true, 9 | }; 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 |
16 |
    17 |
  • Open in new tab (read-only)
  • 18 |
19 |
20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/js/components/EditorHiddenTooltip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | // Redux 6 | import { SET_APP_STATE } from '../store/actions'; 7 | 8 | class EditorHiddenTooltip extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | display: false, 14 | dismissed: false, 15 | }; 16 | 17 | this.onMouseDownToDismiss = this.onMouseDownToDismiss.bind(this); 18 | } 19 | 20 | componentWillMount() { 21 | let display = false; 22 | // TODO: don't hardcode hidden position check 23 | if (this.props.dividerPositionX >= window.innerWidth - 10) { 24 | display = true; 25 | } else if (this.props.showEditorHiddenTooltip === true) { 26 | display = true; 27 | } 28 | 29 | this.setState({ display }); 30 | } 31 | 32 | componentWillReceiveProps(nextProps) { 33 | // TODO: don't hardcode hidden position check 34 | const editorIsVisuallyCollapsed = nextProps.dividerPositionX >= window.innerWidth - 10; 35 | const collapsedStateIsNew = this.props.dividerPositionX !== nextProps.dividerPositionX; 36 | 37 | // Show when specifically requested, or if the editor is collapsed by hand 38 | if ((nextProps.showEditorHiddenTooltip === true && editorIsVisuallyCollapsed) || 39 | (editorIsVisuallyCollapsed && collapsedStateIsNew)) { 40 | this.setState({ 41 | display: true, 42 | dismissed: false, 43 | }); 44 | } 45 | } 46 | 47 | // Activate on mousedown rather than on click, because drag interactions do 48 | // not transform into click events sometimes, and interferes with display state. 49 | onMouseDownToDismiss(event) { 50 | // TODO: Animate out 51 | this.props.dispatch({ 52 | type: SET_APP_STATE, 53 | showEditorHiddenTooltip: false, 54 | }); 55 | this.setState({ dismissed: true }); 56 | 57 | window.removeEventListener('mousedown', this.onMouseDownToDismiss); 58 | } 59 | 60 | render() { 61 | if (this.state.display === true && this.state.dismissed === false) { 62 | // Clicks dismiss the tooltip after some delay 63 | window.setTimeout(() => { 64 | window.addEventListener('mousedown', this.onMouseDownToDismiss); 65 | }, 0); 66 | 67 | return ( 68 |
69 |
The editor pane is hidden
70 |

Drag this divider handle to bring it back!

71 |
72 | ); 73 | } 74 | 75 | return null; 76 | } 77 | } 78 | 79 | EditorHiddenTooltip.propTypes = { 80 | dispatch: PropTypes.func.isRequired, 81 | dividerPositionX: PropTypes.number, 82 | showEditorHiddenTooltip: PropTypes.bool, 83 | }; 84 | 85 | EditorHiddenTooltip.defaultProps = { 86 | dividerPositionX: 0, 87 | showEditorHiddenTooltip: false, 88 | }; 89 | 90 | function mapStateToProps(state) { 91 | return { 92 | dividerPositionX: state.persistence.dividerPositionX, 93 | showEditorHiddenTooltip: state.app.showEditorHiddenTooltip, 94 | }; 95 | } 96 | 97 | export default connect(mapStateToProps)(EditorHiddenTooltip); 98 | -------------------------------------------------------------------------------- /src/js/components/EditorPane.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * EditorPane 3 | * 4 | * This is a parent container component for the editor. It handles position and size. 5 | * The component is included here as a draggable handle to resize 6 | * the pane. Logic that affects editor content itself (e.g. files) reside in 7 | * the child component. 8 | */ 9 | import React from 'react'; 10 | import PropTypes from 'prop-types'; 11 | import { connect } from 'react-redux'; 12 | import EventEmitter from './event-emitter'; 13 | import Editor from './Editor'; 14 | import Divider from './Divider'; 15 | import EditorHiddenTooltip from './EditorHiddenTooltip'; 16 | 17 | class EditorPane extends React.PureComponent { 18 | constructor(props) { 19 | super(props); 20 | 21 | this.animating = false; 22 | 23 | this.updateEditorWidth = this.updateEditorWidth.bind(this); 24 | } 25 | 26 | componentDidMount() { 27 | // Initially set the editor width based on Redux state. This is because 28 | // this component mounts before Divider is ready to send events. 29 | this.updateEditorWidth({ posX: this.props.dividerPositionX }); 30 | EventEmitter.subscribe('divider:reposition', this.updateEditorWidth); 31 | } 32 | 33 | componentDidUpdate(prevProps, prevState) { 34 | // Handle divider position changes 35 | if (this.props.dividerPositionX !== prevProps.dividerPositionX) { 36 | this.updateEditorWidth({ posX: this.props.dividerPositionX }); 37 | } 38 | } 39 | 40 | /** 41 | * Sets editor pane width. 42 | * This is called in response to the `divider:reposition` event which 43 | * passes an event object containing the left edge of the divider element. 44 | * It can also be called manually (see `componentDidMount()`) as long as 45 | * the `event` object matches the signature. 46 | */ 47 | updateEditorWidth(event) { 48 | // Early return if `this.el` is `null`, which happens when this function 49 | // is called from listening for `divider:reposition` while the component 50 | // is still updating, so the DOM node has not appeared as a ref. 51 | if (!this.el) return; 52 | 53 | this.el.style.width = `${window.innerWidth - event.posX}px`; 54 | } 55 | 56 | render() { 57 | return ( 58 |
{ this.el = ref; }}> 59 | 60 | 61 | 62 |
63 | ); 64 | } 65 | } 66 | 67 | EditorPane.propTypes = { 68 | dividerPositionX: PropTypes.number.isRequired, 69 | }; 70 | 71 | function mapStateToProps(state) { 72 | return { 73 | dividerPositionX: state.persistence.dividerPositionX, 74 | }; 75 | } 76 | 77 | export default connect(mapStateToProps)(EditorPane); 78 | -------------------------------------------------------------------------------- /src/js/components/EditorTabBar.jsx: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { connect } from 'react-redux'; 5 | import EditorTabs from './EditorTabs'; 6 | import IconButton from './IconButton'; 7 | import { setDividerPosition } from './Divider'; 8 | import { SET_APP_STATE } from '../store/actions'; 9 | 10 | class EditorTabBar extends React.PureComponent { 11 | constructor(props) { 12 | super(props); 13 | this.onClickHideEditor = this.onClickHideEditor.bind(this); 14 | } 15 | 16 | /** 17 | * Hides the editor pane. 18 | * There is no special "flag" for hidden; it requests the Divider component 19 | * to update its position to the full window width (as far right as possible). 20 | * The Divider component will take care of the rest. 21 | */ 22 | onClickHideEditor(event) { 23 | setDividerPosition(window.innerWidth); 24 | this.props.showEditorHiddenTooltip(); 25 | } 26 | 27 | render() { 28 | // Disable tabs in embedded mode. 29 | // See request https://github.com/tangrams/tangram-play/issues/620 30 | // Rather than expose Yet Another Embed Option, there's a product answer 31 | // to this: there's no real need for tabs in embedded mode (at least not 32 | // yet) so let's remove this functionality from embedded. 33 | if (this.props.disabled) { 34 | return null; 35 | } 36 | 37 | return ( 38 |
39 | 40 | 46 |
47 | ); 48 | } 49 | } 50 | 51 | EditorTabBar.propTypes = { 52 | // Injected by `mapStateToProps` 53 | disabled: PropTypes.bool, 54 | 55 | // Injected by `mapDispatchToProps` 56 | showEditorHiddenTooltip: PropTypes.func.isRequired, 57 | }; 58 | 59 | EditorTabBar.defaultProps = { 60 | disabled: false, 61 | showEditorHiddenTooltip: noop, 62 | }; 63 | 64 | function mapStateToProps(state) { 65 | return { 66 | disabled: !state.app.showEditorTabBar, 67 | }; 68 | } 69 | 70 | function mapDispatchToProps(dispatch) { 71 | return { 72 | showEditorHiddenTooltip: () => { 73 | dispatch({ 74 | type: SET_APP_STATE, 75 | showEditorHiddenTooltip: true, 76 | }); 77 | }, 78 | }; 79 | } 80 | 81 | export default connect(mapStateToProps, mapDispatchToProps)(EditorTabBar); 82 | -------------------------------------------------------------------------------- /src/js/components/ErrorsPanel.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Make a floating panel for Tangram errors. Not all errors have line numbers. 3 | * This just creates a panel to display all of them. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import { connect } from 'react-redux'; 8 | import Draggable from 'react-draggable'; 9 | 10 | class ErrorsPanel extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | show: false, 16 | }; 17 | 18 | this.onClickClose = this.onClickClose.bind(this); 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | if (nextProps.errors.length > 0) { 23 | this.setState({ show: true }); 24 | } else { 25 | this.setState({ show: false }); 26 | } 27 | } 28 | 29 | onClickClose() { 30 | this.setState({ show: !this.state.show }); 31 | } 32 | 33 | render() { 34 | const displayStyle = { display: 'none' }; 35 | 36 | if (this.state.show) { 37 | displayStyle.display = 'block'; 38 | } 39 | 40 | return ( 41 | 45 |
46 |
47 |
Scene errors
48 | 49 |
50 |
51 |
52 | {this.props.errors.map((error, index) => { 53 | let iconTypeClass; 54 | if (error.type === 'error') { 55 | iconTypeClass = 'btm bt-exclamation-triangle'; 56 | } else if (error.type === 'warning') { 57 | iconTypeClass = 'btm bt-exclamation-circle'; 58 | } 59 | 60 | let displayText = error.message; 61 | if (!displayText || displayText.length === 0) { 62 | displayText = `Unspecified ${error.type}.`; 63 | } 64 | 65 | let moreLink; 66 | if (error.link) { 67 | moreLink = ( 68 | 69 | Learn more. 70 | 71 | ); 72 | } 73 | 74 | return ( 75 |
76 |
77 |
78 | {displayText} 79 | {' '}{moreLink} 80 |
81 |
82 | ); 83 | })} 84 |
85 |
86 |
87 | 88 | ); 89 | } 90 | } 91 | 92 | ErrorsPanel.propTypes = { 93 | errors: PropTypes.arrayOf(PropTypes.object), 94 | }; 95 | 96 | ErrorsPanel.defaultProps = { 97 | errors: [], 98 | }; 99 | 100 | function mapStateToProps(state) { 101 | return { 102 | errors: state.errors.errors || [], 103 | }; 104 | } 105 | 106 | export default connect(mapStateToProps)(ErrorsPanel); 107 | -------------------------------------------------------------------------------- /src/js/components/Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Represents an icon that receives a 'type' prop indicating how it should look 6 | * as well as an optional 'active' prop indicating whether icon should be active 7 | */ 8 | export default function Icon(props) { 9 | let className = `btm ${props.type}`; 10 | 11 | if (props.active) { 12 | className += ' icon-active'; 13 | } 14 | 15 | // Additional classes 16 | if (props.className) { 17 | className += ` ${props.className}`; 18 | } 19 | 20 | return ( 21 | 22 | ); 23 | } 24 | 25 | Icon.propTypes = { 26 | type: PropTypes.string.isRequired, 27 | className: PropTypes.string, 28 | active: PropTypes.bool, 29 | }; 30 | 31 | Icon.defaultProps = { 32 | className: '', 33 | active: false, 34 | }; 35 | -------------------------------------------------------------------------------- /src/js/components/IconButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; 4 | import Tooltip from 'react-bootstrap/lib/Tooltip'; 5 | // Test: not using the React-Bootstrap 43 | 44 | ); 45 | } 46 | 47 | IconButton.propTypes = { 48 | className: PropTypes.string, 49 | icon: PropTypes.string.isRequired, 50 | active: PropTypes.bool, 51 | tooltip: PropTypes.string, 52 | tooltipPlacement: PropTypes.string, 53 | buttonRef: PropTypes.func, 54 | }; 55 | 56 | IconButton.defaultProps = { 57 | className: '', 58 | tooltipPlacement: 'bottom', 59 | buttonRef: function noop() {}, 60 | active: false, 61 | tooltip: '', 62 | }; 63 | -------------------------------------------------------------------------------- /src/js/components/Map.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import EventEmitter from './event-emitter'; 5 | import MapPanel from './MapPanel'; 6 | import Camera from './Camera'; 7 | import SceneLoading from '../map/SceneLoading'; 8 | import { initMap, loadScene, destroyScene, refreshMap } from '../map/map'; 9 | 10 | class Map extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.updateMapWidth = this.updateMapWidth.bind(this); 15 | } 16 | 17 | componentDidMount() { 18 | EventEmitter.subscribe('divider:reposition', this.updateMapWidth); 19 | 20 | // We have to run initMap here because this instantiates Leaflet 21 | // into a map container, which expects the DOM element for it to 22 | // exist already. So we can only call it during this lifecycle method. 23 | initMap(); 24 | } 25 | 26 | componentWillUpdate(nextProps) { 27 | // If we don't have any scene files, kill the map so it doesn't take memory 28 | if (this.props.app.initialized && 29 | (nextProps.scene.files.length === 0 || this.props.app.mapNotLoaded === true)) { 30 | destroyScene(); 31 | 32 | // Bail from `componentWillUpdate`, we're done here 33 | return; 34 | } 35 | 36 | // If the scene has changed, load the scene file from the root scene 37 | // contents, which should already be fetched by now. Don't load the 38 | // originalUrl, because contents may differ from the originalUrl due 39 | // to user edits. 40 | if (nextProps.scene.counter > this.props.scene.counter) { 41 | const { scene } = nextProps; 42 | const rootFile = scene.rootFileIndex; 43 | const url = URL.createObjectURL(new Blob([scene.files[rootFile].contents])); 44 | 45 | loadScene(url, { 46 | reset: true, 47 | basePath: scene.originalBasePath, 48 | }); 49 | } 50 | } 51 | 52 | // Updating map element width can happen many times a second while it's 53 | // being resized. Directly adjusting DOM in this way is much faster than 54 | // re-rendering on a state change. 55 | updateMapWidth(event) { 56 | this.mapEl.style.width = `${event.posX}px`; 57 | 58 | // Invalidates and refreshes Leaflet's map size. 59 | refreshMap(); 60 | } 61 | 62 | render() { 63 | return ( 64 |
{ this.mapEl = ref; }}> 65 | {(() => { 66 | // Don't flash this when Tangram Play is initializing; 67 | // files are still zero, but we won't prompt until after 68 | if (!this.props.app.initialized) return null; 69 | 70 | if (this.props.scene.files.length === 0 || this.props.app.mapNotLoaded === true) { 71 | return ( 72 |
73 | ); 74 | } 75 | return null; 76 | })()} 77 |
78 | 79 | 80 | 81 |
82 |
83 | ); 84 | } 85 | } 86 | 87 | Map.propTypes = { 88 | app: PropTypes.shape({ 89 | initialized: PropTypes.bool, 90 | mapNotLoaded: PropTypes.bool, 91 | }).isRequired, 92 | scene: PropTypes.shape({ 93 | counter: PropTypes.number, 94 | files: PropTypes.array, 95 | }).isRequired, 96 | }; 97 | 98 | function mapStateToProps(state) { 99 | return { 100 | app: state.app, 101 | scene: state.scene, 102 | }; 103 | } 104 | 105 | export default connect(mapStateToProps)(Map); 106 | -------------------------------------------------------------------------------- /src/js/components/MapPanelZoom.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ButtonGroup from 'react-bootstrap/lib/ButtonGroup'; 3 | import IconButton from './IconButton'; 4 | import { map } from '../map/map'; 5 | import EventEmitter from './event-emitter'; 6 | 7 | function formatZoom(zoom) { 8 | const fractionalNumber = Math.floor(zoom * 10) / 10; 9 | return Number.parseFloat(fractionalNumber).toFixed(1); 10 | } 11 | 12 | export default class MapPanelZoom extends React.Component { 13 | /** 14 | * Used to setup the state of the component. Regular ES6 classes do not 15 | * automatically bind 'this' to the instance, therefore this is the best 16 | * place to bind event handlers 17 | * 18 | * @param props - parameters passed from the parent 19 | */ 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | zoom: 0, // Current map zoom position to display 25 | }; 26 | 27 | this.onClickZoomIn = this.onClickZoomIn.bind(this); 28 | this.onClickZoomOut = this.onClickZoomOut.bind(this); 29 | } 30 | 31 | componentDidMount() { 32 | EventEmitter.subscribe('map:init', () => { 33 | this.setState({ zoom: map.getZoom() }); 34 | }); 35 | 36 | // Need to subscribe to map zooming events so that our React component 37 | // plays nice with the non-React map 38 | EventEmitter.subscribe('leaflet:zoomend', (data) => { 39 | this.setState({ zoom: map.getZoom() }); 40 | }); 41 | } 42 | 43 | /** Zoom functionality **/ 44 | 45 | /** 46 | * Zoom into the map when user clicks ZoomIn button 47 | */ 48 | onClickZoomIn(event) { 49 | // Not a documented feature, but shift-clicking will zoom in and 50 | // round that zoom to the nearest integer. 51 | if (event.shiftKey) { 52 | const currentZoom = map.getZoom(); 53 | map.setZoom(Math.floor(currentZoom + 1), { animate: true }); 54 | } else { 55 | map.zoomIn(1, { animate: true }); 56 | } 57 | 58 | this.setState({ zoom: map.getZoom() }); 59 | } 60 | 61 | /** 62 | * Zoom into the map when user clicks ZoomOut button 63 | */ 64 | onClickZoomOut(event) { 65 | // Not a documented feature, but shift-clicking will zoom out and 66 | // round that zoom to the nearest integer. 67 | if (event.shiftKey) { 68 | const currentZoom = map.getZoom(); 69 | map.setZoom(Math.ceil(currentZoom - 1), { animate: true }); 70 | } else { 71 | map.zoomOut(1, { animate: true }); 72 | } 73 | 74 | this.setState({ zoom: map.getZoom() }); 75 | } 76 | 77 | render() { 78 | return ( 79 |
80 |
81 | z{formatZoom(this.state.zoom)} 82 |
83 | 84 | {/* Zoom buttons */} 85 | 86 | 92 | 98 | 99 |
100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/js/components/MenuFullscreen.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavItem from 'react-bootstrap/lib/NavItem'; 3 | import Tooltip from 'react-bootstrap/lib/Tooltip'; 4 | import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; 5 | import Icon from './Icon'; 6 | 7 | import { 8 | isFullscreenEnabled, 9 | getFullscreenElement, 10 | toggleFullscreen, 11 | } from '../ui/fullscreen'; 12 | 13 | export default class MenuFullscreen extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | fullscreenActive: false, 19 | }; 20 | 21 | this.checkFullscreenState = this.checkFullscreenState.bind(this); 22 | } 23 | 24 | componentDidMount() { 25 | // Other actions (like pressing escape) can take users out of fullscreen 26 | // mode. When this event is fired, we check to see whether we are in 27 | // fullscreen mode and updates the visual state of the menu item. 28 | // We listen for all prefixed events because no browser currently 29 | // implements Fullscreen API without vendor prefixes. 30 | document.addEventListener('fullscreenchange', this.checkFullscreenState, false); 31 | document.addEventListener('mozfullscreenchange', this.checkFullscreenState, false); 32 | document.addEventListener('webkitfullscreenchange', this.checkFullscreenState, false); 33 | document.addEventListener('MSFullscreenChange', this.checkFullscreenState, false); 34 | } 35 | 36 | onClickFullscreen(event) { 37 | toggleFullscreen(); 38 | } 39 | 40 | checkFullscreenState() { 41 | const fullscreenElement = getFullscreenElement(); 42 | if (fullscreenElement) { 43 | this.setState({ fullscreenActive: true }); 44 | } else { 45 | this.setState({ fullscreenActive: false }); 46 | } 47 | } 48 | 49 | render() { 50 | // This does not render if fullscreen is not enabled on browser. 51 | if (!isFullscreenEnabled()) return null; 52 | 53 | return ( 54 | View fullscreen} 58 | delayShow={200} 59 | > 60 | 65 | Fullscreen 66 | 67 | 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/js/components/RefreshButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from './IconButton'; 3 | 4 | import { reloadOriginalScene } from '../tangram-play'; 5 | 6 | /** 7 | * Button with which user can click to refresh original scene file in the editor. 8 | * For use within embedded Tangram Play 9 | */ 10 | export default class RefreshButton extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | button: 'bt-sync', 16 | }; 17 | 18 | this.onClick = this.onClick.bind(this); 19 | this.resetButton = this.resetButton.bind(this); 20 | } 21 | 22 | /** 23 | * On click, the button spins and calls function to refresh original scene 24 | */ 25 | onClick() { 26 | this.setState({ button: 'bt-sync bt-spin active' }); 27 | reloadOriginalScene(); 28 | window.setTimeout(this.resetButton, 1000); 29 | } 30 | 31 | /** 32 | * Reset the button so it stops spinning 33 | */ 34 | resetButton() { 35 | this.setState({ button: 'bt-sync' }); 36 | } 37 | 38 | render() { 39 | return ( 40 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/js/components/SignInOverlay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Button from 'react-bootstrap/lib/Button'; 5 | 6 | import store from '../store'; 7 | import { SHOW_SIGN_IN_OVERLAY, HIDE_SIGN_IN_OVERLAY } from '../store/actions'; 8 | import { closeSignInWindow } from '../user/sign-in-window'; 9 | 10 | // Externally called to turn this on. 11 | export function showSignInOverlay() { 12 | store.dispatch({ type: SHOW_SIGN_IN_OVERLAY }); 13 | } 14 | 15 | export function hideSignInOverlay() { 16 | store.dispatch({ type: HIDE_SIGN_IN_OVERLAY }); 17 | } 18 | 19 | class SignInOverlay extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | 23 | this.onClickReturn = this.onClickReturn.bind(this); 24 | } 25 | 26 | onClickReturn(event) { 27 | hideSignInOverlay(); 28 | closeSignInWindow(); 29 | this.props.dispatch({ 30 | type: 'SET_SIGN_IN_CALLBACK_METHOD', 31 | method: null, 32 | }); 33 | } 34 | 35 | render() { 36 | if (this.props.visible) { 37 | return ( 38 |
39 |
Waiting for you to sign in...
40 | 43 |
44 | ); 45 | } 46 | 47 | return null; 48 | } 49 | } 50 | 51 | SignInOverlay.propTypes = { 52 | dispatch: PropTypes.func.isRequired, 53 | visible: PropTypes.bool, 54 | }; 55 | 56 | SignInOverlay.defaultProps = { 57 | visible: false, 58 | }; 59 | 60 | function mapStateToProps(state) { 61 | return { 62 | visible: state.app.signInOverlay, 63 | }; 64 | } 65 | 66 | export default connect(mapStateToProps)(SignInOverlay); 67 | -------------------------------------------------------------------------------- /src/js/components/UserAvatar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * If username is not provided, infer one from e-mail address. If no e-mail 6 | * address is provided, return placeholder text. 7 | * 8 | * @param {string} nickname - Username (if available) 9 | * @param {string} email - User e-mail address (if available) 10 | * @return {string} 11 | */ 12 | function getDisplayName(nickname, email) { 13 | if (nickname) return nickname; 14 | if (email) return email.split('@')[0]; 15 | return 'Anonymous'; 16 | } 17 | 18 | /** 19 | * Returns an component if an image url is provided, or an empty
20 | * if there is no image url. 21 | * 22 | * @param {string} url - absolute URL to image 23 | * @return {React.Component} 24 | */ 25 | function getImageElement(url) { 26 | if (url) { 27 | // Alt text is blank because the display name will also be displayed by the image. 28 | return (); 33 | } 34 | 35 | return
; 36 | } 37 | 38 | export default function UserAvatar(props) { 39 | const user = props.user; 40 | const displayName = getDisplayName(user.nickname, user.email); 41 | const imageElement = getImageElement(user.avatar); 42 | const adminStarElement = (user.admin === true) ? 43 | : 44 | null; 45 | 46 | return ( 47 | 48 | {imageElement} 49 | {displayName} 50 | {adminStarElement} 51 | 52 | ); 53 | } 54 | 55 | UserAvatar.propTypes = { 56 | user: PropTypes.shape({ 57 | nickname: PropTypes.string, 58 | email: PropTypes.string, 59 | avatar: PropTypes.string, 60 | admin: PropTypes.bool, 61 | }).isRequired, 62 | }; 63 | 64 | UserAvatar.defaultProps = { 65 | user: { 66 | nickname: '', 67 | email: '', 68 | avatar: '', 69 | admin: false, 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /src/js/components/event-emitter.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = { 2 | // Never access the `events` property directly. 3 | events: {}, 4 | dispatch(event, data) { 5 | if (!this.events[event]) { 6 | return; // no one is listening to this event 7 | } 8 | for (let i = 0; i < this.events[event].length; i++) { 9 | this.events[event][i](data); 10 | } 11 | }, 12 | subscribe(event, callback) { 13 | if (!this.events[event]) { 14 | this.events[event] = []; // new event 15 | } 16 | this.events[event].push(callback); 17 | }, 18 | unsubscribe(event, callback) { 19 | if (!this.events[event]) { 20 | return; // unsubscribing from an event that doesn't exist 21 | } 22 | for (let i = 0; i < this.events[event].length; i++) { 23 | if (this.events[event][i] === callback) { 24 | this.events[event].splice(i, 1); // removes it from event stack 25 | } 26 | } 27 | }, 28 | }; 29 | 30 | export default EventEmitter; 31 | -------------------------------------------------------------------------------- /src/js/components/textmarkers/BooleanMarker.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Checkbox from 'react-bootstrap/lib/Checkbox'; 4 | import { setCodeMirrorValue } from '../../editor/editor'; 5 | 6 | export default class BooleanMarker extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | // Incoming prop is a string, cast it to boolean 12 | isChecked: (this.props.value === 'true'), 13 | 14 | // TODO: disable the checkbox if the value is not a true/false, 15 | // to prevent this from overwriting other values 16 | }; 17 | 18 | this.onChange = this.onChange.bind(this); 19 | } 20 | 21 | onChange(event) { 22 | const value = event.target.checked; 23 | this.setState({ isChecked: value }); 24 | this.setEditorValue(value.toString()); 25 | } 26 | 27 | /** 28 | * Communicates a value back to CodeMirror 29 | */ 30 | setEditorValue(string) { 31 | setCodeMirrorValue(this.props.marker, string); 32 | } 33 | 34 | render() { 35 | return ( 36 | 41 | ); 42 | } 43 | } 44 | 45 | BooleanMarker.propTypes = { 46 | marker: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 47 | value: PropTypes.string.isRequired, 48 | }; 49 | 50 | BooleanMarker.defaultProps = { 51 | value: false, 52 | }; 53 | -------------------------------------------------------------------------------- /src/js/components/textmarkers/color/ColorPicker.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | // Class essentially taken from 'react-color': https://github.com/casesandberg/react-color/blob/master/src/components/sketched/Sketch.js 3 | import React from 'react'; 4 | import { Hue, Alpha, Checkboard } from 'react-color/lib/components/common'; 5 | import ColorPickerSaturation from './ColorPickerSaturation'; 6 | import ColorPickerInputFields from './ColorPickerInputFields'; 7 | import Color from './color'; 8 | 9 | export default class ColorPicker extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | // tinycolor2 resets hue to zero when either saturation (s) or 14 | // brightness (v) is zero, so we maintain our own internal cache 15 | // of this. We also remember the alpha because sometimes it is 16 | // also reset. (TODO) 17 | const color = this.props.color.getHsv(); 18 | this.state = { 19 | hue: color.h, 20 | alpha: color.a, 21 | }; 22 | 23 | this.onChangeSaturation = this.onChangeSaturation.bind(this); 24 | this.onChangeHueAlpha = this.onChangeHueAlpha.bind(this); 25 | this.onChangeInputs = this.onChangeInputs.bind(this); 26 | } 27 | 28 | onChangeSaturation(data) { 29 | // Use cached hue and alpha value 30 | const color = new Color({ 31 | h: this.state.hue, 32 | s: data.s, 33 | v: data.v, 34 | a: this.state.alpha, 35 | }); 36 | this.props.onChange(color); 37 | } 38 | 39 | onChangeHueAlpha(data) { 40 | const color = new Color({ h: data.h, s: data.s, l: data.l, a: data.a }); 41 | this.props.onChange(color); 42 | this.setState({ hue: data.h, alpha: data.a }); 43 | } 44 | 45 | onChangeInputs(data) { 46 | let color; 47 | 48 | // If data comes as RGBA object 49 | if ({}.hasOwnProperty.call(data, 'r')) { 50 | color = new Color({ r: data.r, g: data.g, b: data.b, a: data.a }); 51 | } else { 52 | // Else if its a hex string 53 | color = new Color(data); 54 | } 55 | 56 | this.props.onChange(color); 57 | this.setState({ hue: color.getHsv().h, alpha: data.a }); 58 | } 59 | 60 | render() { 61 | const hsl = this.props.color.getHsl(); 62 | const hueHslProp = { 63 | h: this.state.hue, 64 | s: hsl.s, 65 | l: hsl.l, 66 | a: hsl.a, 67 | }; 68 | 69 | return ( 70 |
71 | 76 | 77 |
78 |
79 |
80 | 84 |
85 |
86 | 91 |
92 |
93 |
94 | 95 |
99 |
100 |
101 | 102 | 106 |
107 | ); 108 | } 109 | } 110 | 111 | ColorPicker.propTypes = { 112 | color: PropTypes.objectOf(PropTypes.any).isRequired, 113 | onChange: PropTypes.func, 114 | }; 115 | 116 | ColorPicker.defaultProps = { 117 | onChange: function noop() {}, 118 | }; 119 | -------------------------------------------------------------------------------- /src/js/components/textmarkers/color/ColorPickerInputFields.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | // Class essentially taken from 'react-color' https://github.com/casesandberg/react-color/blob/master/src/components/sketched/SketchFields.js 3 | 4 | import React from 'react'; 5 | import { EditableInput } from 'react-color/lib/components/common'; 6 | 7 | export default class ColorPickerInputFields extends React.PureComponent { 8 | constructor(props) { 9 | super(props); 10 | this.onChange = this.onChange.bind(this); 11 | } 12 | 13 | /** 14 | * Handle changes in input fields. 15 | * Signature of the `data` object passed in uses EditableInput's `label` 16 | * prop as the property name. e.g. `label='r'` becomes `data.r`. Do not 17 | * change these labels! (Use CSS to style these elements if necessary.) 18 | */ 19 | onChange(data) { 20 | const color = this.props.color.getRgba(); 21 | 22 | if (data.hex) { 23 | this.props.onChange(data.hex); 24 | } else if (data.r || data.g || data.b || data.a) { 25 | let a = parseFloat(data.a); 26 | 27 | // Clamp a between 0-1 range 28 | a = Math.max(Math.min(a, 1), 0); 29 | 30 | if (a === 0) { 31 | this.props.onChange({ 32 | r: data.r || color.r, 33 | g: data.g || color.g, 34 | b: data.b || color.b, 35 | a: 0.0, 36 | }); 37 | } else { 38 | this.props.onChange({ 39 | r: data.r || color.r, 40 | g: data.g || color.g, 41 | b: data.b || color.b, 42 | a: a || color.a, 43 | }); 44 | } 45 | } 46 | } 47 | 48 | render() { 49 | const color = this.props.color.getRgba(); 50 | const hex = this.props.color.getHexString(); 51 | 52 | return ( 53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 | 65 |
66 |
67 | 68 |
69 |
70 | ); 71 | } 72 | } 73 | 74 | ColorPickerInputFields.propTypes = { 75 | color: PropTypes.objectOf(PropTypes.any).isRequired, 76 | onChange: PropTypes.func, 77 | }; 78 | 79 | ColorPickerInputFields.defaultProps = { 80 | onChange: function noop() {}, 81 | }; 82 | -------------------------------------------------------------------------------- /src/js/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | // Nextzen key used exclusively by Tangram Play 3 | NEXTZEN_API_KEY: 'mHPUxCSaRgiJERj1lLDLew', 4 | MAPZEN_API: { 5 | ORIGIN: { 6 | DEVELOPMENT: 'http://localhost', 7 | STAGING: 'https://dev.mapzen.com', 8 | PRODUCTION: 'https://mapzen.com', 9 | }, 10 | SCENE_API_PATH: '/api/scenes/', 11 | USER_API_PATH: '/api/developer.json', 12 | }, 13 | TILES: { 14 | API_KEYS: { 15 | SUPPRESSED: [ 16 | 'mHPUxCSaRgiJERj1lLDLew', // Matches NEXTZEN_API_KEY 17 | ], 18 | }, 19 | }, 20 | SEARCH: { 21 | HOST: 'api.geocode.earth', 22 | API_KEY: 'ge-3d066b6b1c398181', 23 | }, 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /src/js/editor/codemirror/glsl-tangram.js: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | import 'codemirror/mode/clike/clike'; 3 | 4 | function wordsToObj(str) { 5 | const obj = {}; 6 | const keys = str.split(' '); 7 | for (let i = 0; i < keys.length; ++i) { 8 | obj[keys[i]] = true; 9 | } 10 | return obj; 11 | } 12 | 13 | function cppHook(stream, state) { 14 | if (!state.startOfLine) { 15 | return false; 16 | } 17 | for (;;) { 18 | if (stream.skipTo('\\')) { 19 | stream.next(); 20 | if (stream.eol()) { 21 | state.tokenize = cppHook; 22 | break; 23 | } 24 | } else { 25 | stream.skipToEnd(); 26 | state.tokenize = null; 27 | break; 28 | } 29 | } 30 | return 'meta'; 31 | } 32 | 33 | function def(mimes, mode) { 34 | if (typeof mimes === 'string') { 35 | mimes = [mimes]; 36 | } 37 | const words = []; 38 | 39 | function add(obj) { 40 | if (obj) { 41 | Object.keys(obj).forEach((key) => { 42 | if ({}.hasOwnProperty.call(obj, key)) { 43 | words.push(key); 44 | } 45 | }); 46 | } 47 | } 48 | add(mode.keywords); 49 | add(mode.builtin); 50 | add(mode.atoms); 51 | if (words.length) { 52 | mode.helperType = mimes[0]; 53 | CodeMirror.registerHelper('hintWords', mimes[0], words); 54 | } 55 | 56 | for (let i = 0; i < mimes.length; ++i) { 57 | CodeMirror.defineMIME(mimes[i], mode); 58 | } 59 | } 60 | 61 | def(['glsl', 'x-shader/x-vertex', 'x-shader/x-fragment'], { 62 | name: 'clike', 63 | keywords: wordsToObj('float int bool void ' + 64 | 'vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 ' + 65 | 'mat2 mat3 mat4 ' + 66 | 'sampler2D samplerCube ' + 67 | 'const attribute uniform varying ' + 68 | 'break continue discard return ' + 69 | 'for while do if else struct ' + 70 | 'in out inout'), 71 | blockKeywords: wordsToObj('for while do if else struct'), 72 | builtin: wordsToObj('radians degrees sin cos tan asin acos atan ' + 73 | 'pow exp log exp2 sqrt inversesqrt ' + 74 | 'abs sign floor ceil fract mod min max clamp mix step smoothstep ' + 75 | 'length distance dot cross normalize faceforward ' + 76 | 'reflect refract matrixCompMult ' + 77 | 'lessThan lessThanEqual greaterThan greaterThanEqual ' + 78 | 'equal notEqual any all not ' + 79 | 'texture2D textureCube'), 80 | atoms: wordsToObj('true false ' + 81 | 'u_time u_meters_per_pixel u_device_pixel_ratio u_map_position ' + 82 | 'u_tile_origin u_resolution ' + 83 | 'v_world_position v_texcoord ' + 84 | 'v_position position width v_color color v_normal normal material ' + 85 | 'light_accumulator_ambient light_accumulator_diffuse light_accumulator_specular ' + 86 | 'gl_FragColor gl_Position gl_PointSize gl_FragCoord '), 87 | hooks: { '#': cppHook }, 88 | modeProps: { 89 | fold: ['brace', 'include'], 90 | }, 91 | }); 92 | -------------------------------------------------------------------------------- /src/js/editor/codemirror/hint-tangram.js: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | import { hint } from '../suggest'; 3 | 4 | CodeMirror.registerHelper('hint', 'yaml', hint); 5 | -------------------------------------------------------------------------------- /src/js/editor/codemirror/yaml-parser.js: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | import YAML from 'yaml-ast-parser'; 3 | 4 | function parseDoc(cm) { 5 | const doc = cm.getDoc(); 6 | const content = doc.getValue(); 7 | doc.yamlNodes = YAML.safeLoad(content); 8 | } 9 | 10 | function initYAMLParser(cm) { 11 | // Parse YAML abstract syntax tree when it's edited, or swapped in from elsewhere 12 | cm.on('changes', parseDoc); 13 | cm.on('swapDoc', parseDoc); 14 | } 15 | 16 | CodeMirror.defineInitHook(initYAMLParser); 17 | -------------------------------------------------------------------------------- /src/js/editor/keymap.js: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | import { unfoldAll, foldByLevel } from './codemirror/tools'; 3 | import { takeScreenshot } from '../map/screenshot'; 4 | import { increaseEditorFontSize, decreaseEditorFontSize } from '../store/actions/settings'; 5 | 6 | /** 7 | * Creates and exports additional keybinding functionality to CodeMirror. 8 | * 9 | * @returns {Object} extraKeysSettings - an object of extra key mappings to 10 | * provide to the settings object of CodeMirror. 11 | */ 12 | export function getExtraKeyMap() { 13 | // Test for whether to use a Ctrl key (Windows) or Cmd key (Mac) 14 | const isMac = CodeMirror.keyMap.default === CodeMirror.keyMap.macDefault; 15 | const ctrlKey = isMac ? 'Cmd-' : 'Ctrl-'; 16 | 17 | const extraKeysSettings = { 18 | 'Ctrl-Space': 'autocomplete', 19 | Tab(cm) { 20 | // If something is selected, particularly for selection of multiple 21 | // lines, tab inserts additional indentation, rather than replace 22 | // the selection with a tab character. 23 | if (cm.somethingSelected()) { 24 | cm.indentSelection('add'); 25 | } else { 26 | // Maps the tab key to insert spaces instead of a tab character. 27 | // https://codemirror.net/doc/manual.html#keymaps 28 | cm.replaceSelection(Array(cm.getOption('indentUnit') + 1).join(' ')); 29 | } 30 | }, 31 | 'Alt-F': (cm) => { 32 | cm.foldCode(cm.getCursor(), cm.getOption('foldGutter').rangeFinder); 33 | }, 34 | 'Alt-P': (cm) => { 35 | takeScreenshot(); 36 | }, 37 | 'Ctrl-0': (cm) => { 38 | unfoldAll(cm); 39 | }, 40 | 'Ctrl-1': (cm) => { 41 | foldByLevel(cm, 0); 42 | }, 43 | 'Ctrl-2': (cm) => { 44 | foldByLevel(cm, 1); 45 | }, 46 | 'Ctrl-3': (cm) => { 47 | foldByLevel(cm, 2); 48 | }, 49 | 'Ctrl-4': (cm) => { 50 | foldByLevel(cm, 3); 51 | }, 52 | 'Ctrl-5': (cm) => { 53 | foldByLevel(cm, 4); 54 | }, 55 | 'Ctrl-6': (cm) => { 56 | foldByLevel(cm, 5); 57 | }, 58 | 'Ctrl-7': (cm) => { 59 | foldByLevel(cm, 6); 60 | }, 61 | 'Ctrl-8': (cm) => { 62 | foldByLevel(cm, 7); 63 | }, 64 | }; 65 | 66 | // TODO: We might need to get around some commenting bugs by hijacking 67 | // the comment key and directing it to use different characters using 68 | // brute-force analysis of the line itself. 69 | extraKeysSettings[`${ctrlKey}/`] = (cm) => { 70 | cm.toggleComment({ indent: true }); 71 | }; 72 | 73 | // Set Ctrl- or Cmd- buttons depending on Mac or Windows devices. 74 | extraKeysSettings[`${ctrlKey}-`] = (cm) => { 75 | decreaseEditorFontSize(); 76 | cm.refresh(); 77 | }; 78 | // Equal (=) maps to the Plus (+) 79 | extraKeysSettings[`${ctrlKey}=`] = (cm) => { 80 | increaseEditorFontSize(); 81 | cm.refresh(); 82 | }; 83 | 84 | return extraKeysSettings; 85 | } 86 | -------------------------------------------------------------------------------- /src/js/embedded.js: -------------------------------------------------------------------------------- 1 | // Polyfills 2 | import 'babel-polyfill'; 3 | import 'whatwg-fetch'; 4 | import 'url-search-params-polyfill'; 5 | 6 | // React 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | 10 | // Libraries 11 | import Raven from 'raven-js'; 12 | 13 | // Components 14 | import AppEmbedded from './components/AppEmbedded'; 15 | 16 | // Redux 17 | import store from './store'; 18 | import { SET_APP_STATE } from './store/actions'; 19 | 20 | // Miscellaneous 21 | import { getURLSearchParam } from './tools/url-state'; 22 | 23 | // Error tracking 24 | // Load this before all other modules. Only load when run in production. 25 | // Requires `loose-envify` package in build process to set the correct `NODE_ENV`. 26 | if (process.env.NODE_ENV === 'production') { 27 | Raven.config('https://728949999d2a438ab006fed5829fb9c5@app.getsentry.com/78467', { 28 | whitelistUrls: [/mapzen\.com/, /www\.mapzen\.com/], 29 | environment: process.env.NODE_ENV, 30 | }).install(); 31 | } 32 | 33 | // When hosted on production Mapzen, set document.domain to allow cross-origin 34 | // access across subdomains. 35 | if (document.domain.indexOf('mapzen.com') === 0) { 36 | document.domain = document.domain; 37 | } 38 | 39 | // Set some application state variables that control the view in embedded mode 40 | store.dispatch({ 41 | type: SET_APP_STATE, 42 | debug: (getURLSearchParam('debug') === 'true'), 43 | isEmbedded: true, 44 | disableMapToolbar: true, 45 | disableMultiFile: true, 46 | showEditorTabBar: false, 47 | }); 48 | 49 | // Mount React components 50 | ReactDOM.render(React.createElement(AppEmbedded), document.getElementById('root')); 51 | -------------------------------------------------------------------------------- /src/js/map/SceneLoading.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Shows a loading indicator when Tangram is loading and building a new 3 | * scene. It is hidden when Tangram is done (after Tangram's `view_complete` 4 | * event callback, or on Tangram error. 5 | */ 6 | import React from 'react'; 7 | import PropTypes from 'prop-types'; 8 | import { connect } from 'react-redux'; 9 | 10 | function SceneLoading(props) { 11 | let classNames = 'map-loading'; 12 | if (props.loading) { 13 | classNames += ' map-loading-show'; 14 | } 15 | 16 | return
; 17 | } 18 | 19 | SceneLoading.propTypes = { 20 | loading: PropTypes.bool, 21 | }; 22 | 23 | SceneLoading.defaultProps = { 24 | loading: false, 25 | }; 26 | 27 | function mapStateToProps(state) { 28 | return { 29 | loading: state.app.tangramSceneLoading, 30 | }; 31 | } 32 | 33 | export default connect(mapStateToProps)(SceneLoading); 34 | -------------------------------------------------------------------------------- /src/js/map/actions.js: -------------------------------------------------------------------------------- 1 | // Redux 2 | import store from '../store'; 3 | import { SET_APP_STATE } from '../store/actions'; 4 | 5 | /** 6 | * Shows the scene loading indicator. 7 | */ 8 | export function showSceneLoadingIndicator() { 9 | store.dispatch({ 10 | type: SET_APP_STATE, 11 | tangramSceneLoading: true, 12 | }); 13 | } 14 | 15 | /** 16 | * Hide the scene loading indicator. 17 | */ 18 | export function hideSceneLoadingIndicator() { 19 | store.dispatch({ 20 | type: SET_APP_STATE, 21 | tangramSceneLoading: false, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/js/map/bookmarks.js: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage'; 2 | import { uniqWith, isEqualWith, reject } from 'lodash'; 3 | 4 | const STORAGE_BOOKMARKS_KEY = 'bookmarks'; 5 | 6 | /** 7 | * Gets location bookmarks. 8 | * This is asynchronous and returns a Promise. 9 | * 10 | * @returns {Promise} - resolved value is current bookmarks content 11 | */ 12 | export function getLocationBookmarks() { 13 | return localforage.getItem(STORAGE_BOOKMARKS_KEY) 14 | // If not set previously, then this value is null, so return an 15 | // empty array. 16 | .then(bookmarks => bookmarks || []); 17 | } 18 | 19 | /** 20 | * Saves a new location bookmark to the list of bookmarks 21 | * This is asynchronous and returns a Promise. 22 | * 23 | * @param {Object} newBookmark - an object having the following signature: 24 | * { 25 | label, // String - name of the location. 26 | lat, // Number - latitude 27 | lng, // Number - longitude 28 | zoom, // Number - zoom level 29 | timestamp, // String - date string 30 | } 31 | * @returns {Promise} - resolved value is current bookmarks content after save 32 | */ 33 | export function saveLocationBookmark(newBookmark) { 34 | return getLocationBookmarks() 35 | .then((bookmarks) => { 36 | bookmarks.push(newBookmark); 37 | 38 | // In case we try to add a bookmark that already exists, we 39 | // dedupe this array by performing a equality comparison between 40 | // the properities `label`, `lat`, `lng` and `zoom` of each object. 41 | // (Don't compare by unique id like `timestamp` which defeats the purpose) 42 | function eqWithCustomizer(objectValue, otherValue) { 43 | return (objectValue.label === otherValue.label && 44 | objectValue.lat === otherValue.lat && 45 | objectValue.lng === otherValue.lng && 46 | objectValue.zoom === otherValue.zoom); 47 | } 48 | 49 | function uniqWithComparator(objectValue, otherValue) { 50 | return isEqualWith(objectValue, otherValue, eqWithCustomizer); 51 | } 52 | 53 | const uniqueBookmarks = uniqWith(bookmarks, uniqWithComparator); 54 | 55 | return localforage.setItem(STORAGE_BOOKMARKS_KEY, uniqueBookmarks); 56 | }); 57 | } 58 | 59 | /** 60 | * Delete only one bookmark, given a unique identifier. We will use the `_date` 61 | * property as this unique id. Do not delete by array index, since there is no 62 | * guarantee that a React view's index value will match what's in the storage. 63 | * 64 | * @param {string} uid - unique identifier for bookmark. 65 | * @returns {Promise} - resolved value is current bookmarks content after delete 66 | */ 67 | export function deleteLocationBookmark(uid) { 68 | return getLocationBookmarks() 69 | .then((bookmarks) => { 70 | // Reject the bookmark with this given uid 71 | const updatedBookmarks = reject(bookmarks, { timestamp: uid }); 72 | 73 | return localforage.setItem(STORAGE_BOOKMARKS_KEY, updatedBookmarks); 74 | }); 75 | } 76 | 77 | /** 78 | * Clear all current bookmarks by replacing it with an empty array. 79 | * 80 | * @returns {Promise} - resolved value is empty array 81 | */ 82 | export function clearLocationBookmarks() { 83 | return localforage.setItem(STORAGE_BOOKMARKS_KEY, []); 84 | } 85 | -------------------------------------------------------------------------------- /src/js/map/screenshot.js: -------------------------------------------------------------------------------- 1 | import { saveAs } from 'file-saver'; 2 | import { map, tangramScene } from './map'; 3 | 4 | /** 5 | * Utility function to generate a filename slug from current date and 6 | * current map view properties. 7 | * 8 | * @returns {string} - see format in comments, inline 9 | */ 10 | function createFilenameSlug() { 11 | const date = new Date(); 12 | 13 | // Get string values for each portion of the date, left-padding the 14 | // trailing 0 if necessary. 15 | /* eslint-disable prefer-template */ 16 | const year = date.getFullYear().toString(); 17 | const month = ('0' + (date.getMonth() + 1)).slice(-2); // 0-indexed, add 1 18 | const day = ('0' + date.getDate()).slice(-2); 19 | const hour = ('0' + date.getHours()).slice(-2); // 24-hour clock. 20 | const minute = ('0' + date.getMinutes()).slice(-2); 21 | const second = ('0' + date.getSeconds()).slice(-2); 22 | /* eslint-enable prefer-template */ 23 | 24 | // Get map view 25 | const center = map.getCenter(); 26 | const zoom = map.getZoom().toFixed(4).toString(); 27 | const lat = center.lat.toFixed(4).toString(); 28 | const lng = center.lng.toFixed(4).toString(); 29 | 30 | // String format, eg. 31 | // @8.7067&76.2119&-60.8138_2016-07-29_12.59.16 32 | // @z&x&y is so that it can be pasted back into a hash in the URL. 33 | // (Unfortunately forward slashes are not file system friendly.) 34 | return `@${zoom}&${lat}&${lng}_${year}-${month}-${day}_${hour}.${minute}.${second}`; 35 | } 36 | 37 | /** 38 | * Uses Tangram's native screenshot functionality to download an image. 39 | * 40 | * @public 41 | * @requires FileSaver 42 | */ 43 | export function takeScreenshot() { 44 | tangramScene.screenshot().then((result) => { 45 | const slug = createFilenameSlug(); 46 | 47 | // uses FileSaver.js: https://github.com/eligrey/FileSaver.js/ 48 | saveAs(result.blob, `tangram-${slug}.png`); 49 | }); 50 | } 51 | 52 | /** 53 | * Uses Tangram's native screenshot functionality to return a Promise 54 | * whose resolve function passes in an object containing two properities: 55 | * blob - a Blob object representing the image binary 56 | * url - a string containing a base64 data-URI 57 | * 58 | * @public 59 | * @returns Promise 60 | */ 61 | export function getScreenshotData() { 62 | return tangramScene.screenshot(); 63 | } 64 | -------------------------------------------------------------------------------- /src/js/map/video.js: -------------------------------------------------------------------------------- 1 | // Take a video capture and save to file 2 | import { saveAs } from 'file-saver'; 3 | import { tangramLayer } from './map'; 4 | 5 | let isCapturing = false; 6 | 7 | export function startVideoCapture() { 8 | if (!isCapturing) { 9 | if (tangramLayer.scene.startVideoCapture()) { 10 | isCapturing = true; 11 | } 12 | } else { 13 | tangramLayer.scene.stopVideoCapture().then((video) => { 14 | isCapturing = false; 15 | saveAs(video.blob, `tangram-video-${+new Date()}.webm`); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/js/modals/AboutModal.jsx: -------------------------------------------------------------------------------- 1 | import L from 'leaflet'; 2 | import CodeMirror from 'codemirror'; 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import Button from 'react-bootstrap/lib/Button'; 7 | import VERSION from '../version.json'; 8 | 9 | import Modal from './Modal'; 10 | 11 | class AboutModal extends React.PureComponent { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.onClickClose = this.onClickClose.bind(this); 16 | } 17 | 18 | onClickClose() { 19 | this.props.dispatch({ 20 | type: 'HIDE_MODAL', 21 | id: this.props.modalId, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 | 31 |
32 |

About Tangram Play

33 | 34 |

35 | Tangram Play (beta) v{VERSION.v} 36 |

37 | 38 |

39 | {/* Get and display version numbers. 40 | Tangram version comes with its own "v" */} 41 | Tangram {window.Tangram.version} 42 | {/* Add "v" for Leaflet and CodeMirror */} 43 |
Leaflet {`v${L.version}`} 44 |
CodeMirror {`v${CodeMirror.version}`} 45 |

46 |

47 | Made with 💖 by Mapzen. 48 |

49 |

50 | View source on GitHub 51 |

52 |
53 | 54 |
55 | 56 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | AboutModal.propTypes = { 63 | dispatch: PropTypes.func.isRequired, 64 | modalId: PropTypes.number.isRequired, 65 | }; 66 | 67 | export default connect()(AboutModal); 68 | -------------------------------------------------------------------------------- /src/js/modals/ErrorModal.jsx: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import Button from 'react-bootstrap/lib/Button'; 7 | import Modal from './Modal'; 8 | import Icon from '../components/Icon'; 9 | 10 | import store from '../store'; 11 | import { SHOW_MODAL } from '../store/actions'; 12 | 13 | class ErrorModal extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.onClickClose = this.onClickClose.bind(this); 18 | } 19 | 20 | componentDidMount() { 21 | // Focus on the continue button when it is shown. 22 | // This is not currently the default action for modals, but may be in 23 | // the future. 24 | ReactDOM.findDOMNode(this.continueButton).focus(); // eslint-disable-line react/no-find-dom-node 25 | } 26 | 27 | // Always execute the confirm function after unmounting (clicking the 28 | // "Confirm" button unmounts) 29 | componentWillUnmount() { 30 | this.props.confirmFunction(); 31 | } 32 | 33 | onClickClose() { 34 | this.props.dispatch({ 35 | type: 'HIDE_MODAL', 36 | id: this.props.modalId, 37 | }); 38 | // After unmounting, `componentWillUnmount()` is called and the 39 | // `confirmFunction()` will be executed. 40 | } 41 | 42 | render() { 43 | return ( 44 | 48 |

49 | {this.props.error.message || this.props.error} 50 |

51 | 52 |
53 | 61 |
62 |
63 | ); 64 | } 65 | } 66 | 67 | ErrorModal.propTypes = { 68 | dispatch: PropTypes.func.isRequired, 69 | modalId: PropTypes.number.isRequired, 70 | 71 | // Error message might be an Error object or a string 72 | error: PropTypes.oneOfType([ 73 | PropTypes.string, 74 | PropTypes.instanceOf(Error), 75 | ]).isRequired, 76 | confirmFunction: PropTypes.func, 77 | }; 78 | 79 | ErrorModal.defaultProps = { 80 | confirmFunction: noop, 81 | }; 82 | 83 | export default connect()(ErrorModal); 84 | 85 | /** 86 | * A convenience function for displaying the ErrorModal. 87 | * 88 | * @param {string} message - the message to display in the modal 89 | * @param {Function} callback - callback function to execute when the error 90 | * modal is unmounted. This is passed in to ErrorModal's props as 91 | * `confirmFunction` 92 | */ 93 | export function showErrorModal(message, callback = noop) { 94 | store.dispatch({ 95 | type: SHOW_MODAL, 96 | modalType: 'ERROR', 97 | modalProps: { 98 | error: message, 99 | confirmFunction: callback, 100 | }, 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /src/js/modals/ExamplesModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SceneSelectModal from './SceneSelectModal'; 4 | 5 | import { load } from '../tangram-play'; 6 | import EXAMPLES_DATA from './examples.json'; 7 | 8 | function confirmHandler(selected) { 9 | if (!selected) return; 10 | 11 | load({ 12 | url: selected.url, 13 | data: selected, 14 | source: 'EXAMPLES', 15 | }); 16 | } 17 | 18 | export default function ExamplesModal(props) { 19 | return ( 20 | 26 | ); 27 | } 28 | 29 | ExamplesModal.propTypes = { 30 | modalId: PropTypes.number.isRequired, 31 | }; 32 | -------------------------------------------------------------------------------- /src/js/modals/LoadingSpinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Icon from '../components/Icon'; 4 | 5 | export default function LoadingSpinner(props) { 6 | let className = 'loading-spinner'; 7 | if (props.on) { 8 | className += ' loading-spinner-on'; 9 | } 10 | 11 | return ( 12 |
13 | 14 | {props.msg} 15 |
16 | ); 17 | } 18 | 19 | LoadingSpinner.propTypes = { 20 | on: PropTypes.bool, 21 | msg: PropTypes.string, 22 | }; 23 | 24 | LoadingSpinner.defaultProps = { 25 | on: false, 26 | msg: 'Working...', 27 | }; 28 | -------------------------------------------------------------------------------- /src/js/modals/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { noop } from 'lodash'; 4 | 5 | export default class Modal extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.storeRefs = this.storeRefs.bind(this); 10 | this.onKeyDown = this.onKeyDown.bind(this); 11 | this.handleEscKey = this.handleEscKey.bind(this); 12 | this.handleEnterKey = this.handleEnterKey.bind(this); 13 | } 14 | 15 | componentDidMount() { 16 | // Add listeners for keys 17 | window.addEventListener('keydown', this.onKeyDown, false); 18 | } 19 | 20 | componentWillUnmount() { 21 | window.removeEventListener('keydown', this.onKeyDown, false); 22 | } 23 | 24 | onKeyDown(event) { 25 | const key = event.keyCode || event.which; 26 | 27 | switch (key) { 28 | case 13: 29 | this.handleEnterKey(event); 30 | break; 31 | case 27: 32 | this.handleEscKey(event); 33 | break; 34 | default: 35 | break; 36 | } 37 | } 38 | 39 | /** 40 | * Stores reference to this modal's DOM node locally, and sends to parent 41 | * component if a callback function is provided. 42 | */ 43 | storeRefs(ref) { 44 | this.el = ref; 45 | this.props.setRef(ref); 46 | } 47 | 48 | /** 49 | * Handles when the Enter key is pressed. 50 | * Should be the same function as if you pressed the Confirm button. 51 | * Events are passed to the function as the first parameter. 52 | */ 53 | handleEnterKey(event) { 54 | // By default, the confirmFunction prop is a no-op 55 | this.props.confirmFunction(event); 56 | } 57 | 58 | /** 59 | * Handles when the Escape key is pressed. 60 | * Should be the same function as if you pressed the Cancel button. 61 | * Events are passed to the function as the first parameter. 62 | */ 63 | handleEscKey(event) { 64 | // Bail if escape disabled via props 65 | if (this.props.disableEsc === true) return; 66 | 67 | if (this.props.cancelFunction !== noop) { 68 | this.props.cancelFunction(event); 69 | } 70 | } 71 | 72 | render() { 73 | let classNames = 'modal'; 74 | 75 | if (this.props.className) { 76 | classNames = `${classNames} ${this.props.className}`; 77 | } 78 | 79 | return ( 80 |
81 |
82 | {this.props.children} 83 |
84 |
85 | ); 86 | } 87 | } 88 | 89 | Modal.propTypes = { 90 | children: PropTypes.node.isRequired, 91 | className: PropTypes.string, 92 | disableEsc: PropTypes.bool, 93 | cancelFunction: PropTypes.func, 94 | confirmFunction: PropTypes.func, 95 | setRef: PropTypes.func, 96 | }; 97 | 98 | Modal.defaultProps = { 99 | className: '', 100 | disableEsc: false, 101 | cancelFunction: noop, 102 | confirmFunction: noop, 103 | setRef: noop, 104 | }; 105 | -------------------------------------------------------------------------------- /src/js/modals/ModalRoot.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | // Import all modals here 6 | import AboutModal from './AboutModal'; 7 | import WelcomeModal from './WelcomeModal'; 8 | import WhatsNewModal from './WhatsNewModal'; 9 | import SupportModal from './SupportModal'; 10 | import ConfirmDialogModal from './ConfirmDialogModal'; 11 | import ErrorModal from './ErrorModal'; 12 | import ExamplesModal from './ExamplesModal'; 13 | import OpenFromCloudModal from './OpenFromCloudModal'; 14 | import OpenGistModal from './OpenGistModal'; // LEGACY. 15 | import OpenUrlModal from './OpenUrlModal'; 16 | import SaveToCloudModal from './SaveToCloudModal'; 17 | import SaveExistingToCloudModal from './SaveExistingToCloudModal'; 18 | import SaveGistModal from './SaveGistModal'; // LEGACY. 19 | import SaveGistSuccessModal from './SaveGistSuccessModal'; // LEGACY. 20 | import ShareHostedMapModal from './ShareHostedMapModal'; 21 | import CodeSnippetModal from './CodeSnippetModal'; 22 | 23 | const MODAL_COMPONENTS = { 24 | ABOUT: AboutModal, 25 | WELCOME: WelcomeModal, 26 | WHATS_NEW: WhatsNewModal, 27 | SUPPORT: SupportModal, 28 | CONFIRM_DIALOG: ConfirmDialogModal, 29 | ERROR: ErrorModal, 30 | OPEN_EXAMPLE: ExamplesModal, 31 | OPEN_FROM_CLOUD: OpenFromCloudModal, 32 | OPEN_GIST: OpenGistModal, // LEGACY. 33 | OPEN_URL: OpenUrlModal, 34 | SAVE_TO_CLOUD: SaveToCloudModal, 35 | SAVE_EXISTING_TO_CLOUD: SaveExistingToCloudModal, 36 | SAVE_GIST: SaveGistModal, // LEGACY. 37 | SAVE_GIST_SUCCESS: SaveGistSuccessModal, // LEGACY 38 | SHARE_HOSTED_MAP: ShareHostedMapModal, 39 | SHOW_CODE_SNIPPET: CodeSnippetModal, 40 | }; 41 | 42 | const ModalRoot = ({ stack }) => { 43 | // Sort modals by priority value -- highest is displayed on top. 44 | const modalComponents = stack.sort((a, b) => a.priority - b.priority) 45 | .map(({ modalType, modalProps, id }) => { 46 | if (!modalType) return null; 47 | 48 | const SpecificModal = MODAL_COMPONENTS[modalType]; 49 | return ; 50 | }); 51 | 52 | return
{modalComponents}
; 53 | }; 54 | 55 | ModalRoot.propTypes = { 56 | stack: PropTypes.arrayOf(PropTypes.object), 57 | }; 58 | 59 | ModalRoot.defaultProps = { 60 | stack: [], 61 | }; 62 | 63 | export default connect( 64 | state => state.modals 65 | )(ModalRoot); 66 | -------------------------------------------------------------------------------- /src/js/modals/ModalShield.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | function ModalShield(props) { 6 | const displayStyle = props.stack.length > 0 ? 7 | { display: 'block' } : 8 | { display: 'none' }; 9 | 10 | return
; 11 | } 12 | 13 | ModalShield.propTypes = { 14 | stack: PropTypes.arrayOf(PropTypes.any), 15 | }; 16 | 17 | ModalShield.defaultProps = { 18 | stack: [], 19 | }; 20 | 21 | function mapStateToProps(state) { 22 | return { 23 | stack: state.modals.stack, 24 | }; 25 | } 26 | 27 | export default connect(mapStateToProps)(ModalShield); 28 | -------------------------------------------------------------------------------- /src/js/modals/OpenFromCloudModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SceneSelectModal from './SceneSelectModal'; 4 | 5 | import { load } from '../tangram-play'; 6 | import { fetchSceneList, deleteScene } from '../storage/mapzen'; 7 | 8 | function confirmHandler(selected) { 9 | if (!selected) return; 10 | 11 | load({ 12 | url: selected.entrypoint_url, 13 | data: selected, 14 | source: 'MAPZEN', 15 | }); 16 | } 17 | 18 | export default function OpenFromCloudModal(props) { 19 | return ( 20 | 29 | ); 30 | } 31 | 32 | OpenFromCloudModal.propTypes = { 33 | modalId: PropTypes.number.isRequired, 34 | }; 35 | -------------------------------------------------------------------------------- /src/js/modals/OpenGistModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SceneSelectModal from './SceneSelectModal'; 4 | 5 | import { showErrorModal } from './ErrorModal'; 6 | import { load } from '../tangram-play'; 7 | import { getSceneURLFromGistAPI } from '../tools/gist-url'; 8 | import { getGists, removeNonexistentGistFromLocalStorage } from '../storage/gist'; 9 | 10 | /** 11 | * If opening a URL is not successful 12 | * 13 | * @param {Error} error - thrown by something else 14 | * if error.message is a number, then it is a status code from Fetch 15 | * but status code numbers are converted to strings 16 | * there must be a better way of doing this 17 | * @param {string} url - the Gist URL that was attempted 18 | */ 19 | function handleError(error, value) { 20 | let message = ''; 21 | 22 | if (error.message === '404') { 23 | message = 'This Gist could not be found.'; 24 | } else if (error.message === '403') { 25 | message = 'We exceeded the rate limit for GitHub’s non-authenticated request API.'; 26 | } else if (Number.isInteger(window.parseInt(error.message, 10))) { 27 | message = `The Gist server gave us an error code of ${error.message}`; 28 | } 29 | 30 | showErrorModal(`Could not load the Gist! ${message}`); 31 | 32 | if (error.message === '404') { 33 | removeNonexistentGistFromLocalStorage(value); 34 | } 35 | } 36 | 37 | function confirmHandler(selected) { 38 | if (!selected) return; 39 | 40 | getSceneURLFromGistAPI(selected.url) 41 | .then((url) => { 42 | load({ 43 | url, 44 | data: selected, 45 | source: 'GIST', 46 | }); 47 | }) 48 | .catch((error) => { 49 | handleError(error, selected.url); 50 | }); 51 | } 52 | 53 | export default function OpenGistModal(props) { 54 | return ( 55 | 62 | ); 63 | } 64 | 65 | OpenGistModal.propTypes = { 66 | modalId: PropTypes.number.isRequired, 67 | }; 68 | -------------------------------------------------------------------------------- /src/js/modals/OpenUrlModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Button from 'react-bootstrap/lib/Button'; 5 | import Modal from './Modal'; 6 | import Icon from '../components/Icon'; 7 | import LoadingSpinner from './LoadingSpinner'; 8 | 9 | import { load } from '../tangram-play'; 10 | 11 | let lastAttemptedUrlInput; 12 | 13 | class OpenUrlModal extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | thinking: false, 19 | input: lastAttemptedUrlInput || '', 20 | }; 21 | 22 | this.onClickConfirm = this.onClickConfirm.bind(this); 23 | this.onClickCancel = this.onClickCancel.bind(this); 24 | this.onChangeInput = this.onChangeInput.bind(this); 25 | this.unmountSelf = this.unmountSelf.bind(this); 26 | } 27 | 28 | componentDidMount() { 29 | this.input.select(); 30 | this.input.focus(); 31 | } 32 | 33 | onClickConfirm() { 34 | // Bail if no input 35 | if (!this.state.input) return; 36 | 37 | // Waiting state 38 | this.setState({ 39 | thinking: true, 40 | }); 41 | 42 | // Cache this 43 | lastAttemptedUrlInput = this.state.input; 44 | 45 | // We no longer check for valid URL signatures. 46 | // It is easier to attempt to fetch an input URL and see what happens. 47 | const url = this.state.input.trim(); 48 | load({ url }) 49 | .then(this.unmountSelf); 50 | } 51 | 52 | onClickCancel(event) { 53 | this.unmountSelf(); 54 | } 55 | 56 | onChangeInput(event) { 57 | this.setState({ input: event.target.value }); 58 | } 59 | 60 | unmountSelf() { 61 | this.props.dispatch({ 62 | type: 'HIDE_MODAL', 63 | id: this.props.modalId, 64 | }); 65 | } 66 | 67 | render() { 68 | return ( 69 | 75 |

Open a scene file from URL

76 | 77 |
78 | { this.input = ref; }} 85 | onChange={this.onChangeInput} 86 | /> 87 |
88 | 89 |
90 | 91 | 98 | 105 |
106 |
107 | ); 108 | } 109 | } 110 | 111 | OpenUrlModal.propTypes = { 112 | dispatch: PropTypes.func.isRequired, 113 | modalId: PropTypes.number.isRequired, 114 | }; 115 | 116 | export default connect()(OpenUrlModal); 117 | -------------------------------------------------------------------------------- /src/js/modals/SaveGistSuccessModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import ReactDOM from 'react-dom'; 5 | import Button from 'react-bootstrap/lib/Button'; 6 | import Clipboard from 'clipboard'; 7 | 8 | import IconButton from '../components/IconButton'; 9 | import Modal from './Modal'; 10 | 11 | class SaveGistSuccessModal extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.onClickConfirm = this.onClickConfirm.bind(this); 16 | } 17 | 18 | componentDidMount() { 19 | this.setupClipboard(); 20 | this.urlInput.select(); 21 | } 22 | 23 | componentWillUnmount() { 24 | // Clean up clipboard object 25 | this.clipboard.destroy(); 26 | } 27 | 28 | onClickConfirm(event) { 29 | this.props.dispatch({ 30 | type: 'HIDE_MODAL', 31 | id: this.props.modalId, 32 | }); 33 | } 34 | 35 | // Sets up clipboard.js functionality. Not a React component. 36 | setupClipboard() { 37 | // eslint-disable-next-line react/no-find-dom-node 38 | const clipboardButtonEl = ReactDOM.findDOMNode(this.clipboardButton); 39 | 40 | // Initiate clipboard button 41 | this.clipboard = new Clipboard(clipboardButtonEl); 42 | 43 | this.clipboard.on('success', (e) => { 44 | console.info('Action:', e.action); 45 | console.info('Text:', e.text); 46 | console.info('Trigger:', e.trigger); 47 | 48 | e.clearSelection(); 49 | }); 50 | 51 | this.clipboard.on('error', (e) => { 52 | console.error('Action:', e.action); 53 | console.error('Trigger:', e.trigger); 54 | }); 55 | 56 | clipboardButtonEl.focus(); 57 | } 58 | 59 | render() { 60 | return ( 61 | 65 |
66 |

67 | Your gist has been saved. 68 |

69 |

70 | Remember this URL! 71 |

72 |
73 | { this.urlInput = ref; }} 78 | defaultValue={this.props.urlValue} 79 | /> 80 | { this.clipboardButton = ref; }} 86 | /> 87 |
88 |
89 |
90 | 93 |
94 |
95 | ); 96 | } 97 | } 98 | 99 | SaveGistSuccessModal.propTypes = { 100 | dispatch: PropTypes.func.isRequired, 101 | urlValue: PropTypes.string.isRequired, 102 | modalId: PropTypes.number.isRequired, 103 | }; 104 | 105 | export default connect()(SaveGistSuccessModal); 106 | -------------------------------------------------------------------------------- /src/js/modals/SceneItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default function SceneItem(props) { 5 | const date = (props.date) ? ( 6 |
7 | {/* Show the date this was saved. 8 | TODO: better formatting; maybe use moment.js */} 9 | Saved on {new Date(props.date).toLocaleString()} 10 |
11 | ) : null; 12 | 13 | const children = (props.children) ? ( 14 |
15 | {props.children} 16 |
17 | ) : null; 18 | 19 | return ( 20 |
21 |
22 | 23 |
24 |
25 |
26 | {props.name} 27 |
28 |
29 | {props.description} 30 |
31 | {date} 32 |
33 | {children} 34 |
35 | ); 36 | } 37 | 38 | SceneItem.propTypes = { 39 | thumbnail: PropTypes.string.isRequired, 40 | name: PropTypes.string.isRequired, 41 | description: PropTypes.string, 42 | date: PropTypes.string, 43 | children: PropTypes.node, 44 | }; 45 | 46 | SceneItem.defaultProps = { 47 | description: 'No description provided.', 48 | date: '', 49 | children: null, 50 | }; 51 | -------------------------------------------------------------------------------- /src/js/modals/SupportModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Button from 'react-bootstrap/lib/Button'; 5 | import Modal from './Modal'; 6 | 7 | class SupportModal extends React.PureComponent { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.onClickClose = this.onClickClose.bind(this); 12 | } 13 | 14 | onClickClose() { 15 | this.props.dispatch({ 16 | type: 'HIDE_MODAL', 17 | id: this.props.modalId, 18 | }); 19 | } 20 | 21 | render() { 22 | return ( 23 | 27 |
28 |

Tangram Play Support

29 | 30 |

31 | Learn more about using Tangram and Mapzen vector tiles on their documentation pages. 32 |

33 | 34 |

35 | Having a problem with Tangram Play? Do you have feedback to share? Contact Mapzen Support by emailing tangram@mapzen.com. 36 |

37 | 38 |

39 | Tangram Play is still in active development and you can have a role in it! Add bugs or feature requests as an issue on the project’s GitHub repository. 40 |

41 |
42 | 43 |
44 | 45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | SupportModal.propTypes = { 52 | dispatch: PropTypes.func.isRequired, 53 | modalId: PropTypes.number.isRequired, 54 | }; 55 | 56 | export default connect()(SupportModal); 57 | -------------------------------------------------------------------------------- /src/js/modals/WelcomeModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Button from 'react-bootstrap/lib/Button'; 5 | import Modal from './Modal'; 6 | import Icon from '../components/Icon'; 7 | import { HIDE_MODAL, DISMISS_WELCOME_SCREEN } from '../store/actions'; 8 | 9 | class WelcomeModal extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.onClickClose = this.onClickClose.bind(this); 14 | } 15 | 16 | onClickClose() { 17 | this.props.dispatch({ 18 | type: HIDE_MODAL, 19 | id: this.props.modalId, 20 | }); 21 | 22 | this.props.dispatch({ type: DISMISS_WELCOME_SCREEN }); 23 | } 24 | 25 | onClickTangram() { 26 | window.open('https://mapzen.com/products/tangram/', '_blank'); 27 | } 28 | 29 | onClickGetStarted() { 30 | window.open('./docs/', '_blank'); 31 | } 32 | 33 | render() { 34 | return ( 35 | 39 |
40 |

Welcome to Tangram Play!

41 | 42 |

43 | We’re excited to release the public beta of Tangram Play, a text 44 | editor for working with Tangram, our web map rendering library. 45 | Your use of Play during this phase will help us improve our tools 46 | for web map design. 47 |

48 | 49 |
50 | 51 | 52 |
53 | 54 |
55 | 58 |
59 |
60 |
61 | ); 62 | } 63 | } 64 | 65 | WelcomeModal.propTypes = { 66 | dispatch: PropTypes.func.isRequired, 67 | modalId: PropTypes.number.isRequired, 68 | }; 69 | 70 | export default connect()(WelcomeModal); 71 | -------------------------------------------------------------------------------- /src/js/modals/WhatsNewModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Button from 'react-bootstrap/lib/Button'; 5 | import Modal from './Modal'; 6 | import Icon from '../components/Icon'; 7 | 8 | class WhatsNewModal extends React.PureComponent { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.onClickClose = this.onClickClose.bind(this); 13 | } 14 | 15 | onClickClose() { 16 | this.props.dispatch({ 17 | type: 'HIDE_MODAL', 18 | id: this.props.modalId, 19 | }); 20 | } 21 | 22 | render() { 23 | return ( 24 | 28 |
29 |

What’s new!

30 |
31 | 32 |
33 |