├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── build ├── glslEditor.css ├── glslEditor.css.map ├── glslEditor.js └── glslEditor.min.js ├── favicon.gif ├── gulpfile.js ├── index.html ├── package-lock.json ├── package.json ├── src ├── css │ ├── ElectronApp.css │ ├── _debugger.css │ ├── _editor.css │ ├── _errors.css │ ├── _icons.css │ ├── _menu.css │ ├── _modals.css │ ├── _panel.css │ ├── _pickers.css │ ├── _shader.css │ └── glslEditor.css ├── index.html ├── js │ ├── GlslEditor.js │ ├── core │ │ ├── Editor.js │ │ └── Shader.js │ ├── io │ │ ├── BufferManager.js │ │ ├── FileDrop.js │ │ ├── HashWatch.js │ │ ├── LocalStorage.js │ │ └── Share.js │ ├── tools │ │ ├── common.js │ │ ├── debugging.js │ │ ├── download.js │ │ ├── interactiveDom.js │ │ ├── mediaCapture.js │ │ ├── mixin.js │ │ └── urls.js │ └── ui │ │ ├── ErrorsDisplay.js │ │ ├── ExportIcon.js │ │ ├── Helpers.js │ │ ├── Menu.js │ │ ├── MenuItem.js │ │ ├── VisualDebugger.js │ │ ├── modals │ │ ├── ExportModal.js │ │ └── Modal.js │ │ └── pickers │ │ ├── ColorPicker.js │ │ ├── FloatPicker.js │ │ ├── Picker.js │ │ ├── Vec2Picker.js │ │ ├── Vec3Picker.js │ │ └── types │ │ ├── Color.js │ │ ├── ColorConverter.js │ │ ├── Float.js │ │ ├── Matrix.js │ │ └── Vector.js └── main.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | src/**/*.min.js 2 | src/js/vendor/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "parserOptions": { 6 | "sourceType": "module" 7 | }, 8 | "rules": { 9 | "no-multi-spaces": 2, 10 | "no-empty": [ 11 | 2, 12 | { 13 | "allowEmptyCatch": true 14 | } 15 | ], 16 | "no-implicit-coercion": [ 17 | 2, 18 | { 19 | "boolean": true, 20 | "string": true, 21 | "number": true 22 | } 23 | ], 24 | "no-mixed-spaces-and-tabs": 2, 25 | "no-multiple-empty-lines": 2, 26 | "padded-blocks": [ 27 | 2, 28 | "never" 29 | ], 30 | "quote-props": [ 31 | 2, 32 | "as-needed" 33 | ], 34 | "key-spacing": 0, 35 | "space-unary-ops": [ 36 | 2, 37 | { 38 | "words": false, 39 | "nonwords": false 40 | } 41 | ], 42 | "no-spaced-func": 2, 43 | "space-in-parens": [ 44 | 2, 45 | "never" 46 | ], 47 | "no-trailing-spaces": 2, 48 | "yoda": [ 49 | 2, 50 | "never" 51 | ], 52 | "camelcase": [ 53 | 2, 54 | { 55 | "properties": "always" 56 | } 57 | ], 58 | "comma-style": [ 59 | 2, 60 | "last" 61 | ], 62 | "curly": [ 63 | 2, 64 | "all" 65 | ], 66 | "brace-style": [ 67 | 2, 68 | "stroustrup", 69 | { 70 | "allowSingleLine": true 71 | } 72 | ], 73 | "eol-last": 2, 74 | "wrap-iife": 2, 75 | "semi": [ 76 | 2, 77 | "always" 78 | ], 79 | "space-infix-ops": 2, 80 | "space-before-blocks": [ 81 | 2, 82 | "always" 83 | ], 84 | "array-bracket-spacing": [ 85 | 2, 86 | "never", 87 | { 88 | "objectsInArrays": false 89 | } 90 | ], 91 | "indent": [ 92 | 2, 93 | 4, 94 | { 95 | "SwitchCase": 1 96 | } 97 | ], 98 | "linebreak-style": [ 99 | 2, 100 | "unix" 101 | ], 102 | "quotes": [ 103 | 2, 104 | "single", 105 | { 106 | "avoidEscape": true 107 | } 108 | ] 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ._.DS_Store 3 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Patricio Gonzalez Vivo ( http://www.patriciogonzalezvivo.com ) 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 | # [GlslEditor](https://github.com/patriciogonzalezvivo/glslEditor) 2 | 3 | ![](http://patriciogonzalezvivo.com/images/glslEditor/00.gif) 4 | 5 | [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4BQMKQJDQ9XH6) 6 | 7 | Friendly GLSL Shader editor based on [Codemirror](http://codemirror.net/) compatible with [glslViewer](https://github.com/patriciogonzalezvivo/glslViewer) (C++/OpenGL ES) and [glslCanvas](https://github.com/patriciogonzalezvivo/glslCanvas) (JS/WebGL). 8 | 9 | Was originally develop to work as a embedded editor for [The Book of Shaders](https://thebookofshaders.com). But now have grown as a stand alone Web app. Thanks to their compatibility with other apps of this ecosystems like [glslViewer](https://github.com/patriciogonzalezvivo/glslViewer) that runs in the RaspberryPi directly from console, [GlslEditor](https://github.com/patriciogonzalezvivo/glslEditor) interact with other projects like [OpenFrame.io](http://openframe.io) allowing the user to export the shaders to frames with only one button. 10 | 11 | ![](http://patriciogonzalezvivo.com/images/glslEditor/01.gif) 12 | 13 | You can use it directly from [editor.thebookofshaders.com](http://editor.thebookofshaders.com/) or host one on your own website by including the two ```build``` files: ```glslEditor.css``` and ```glslEditor.js```: 14 | 15 | ```html 16 | 17 | 18 | ``` 19 | 20 | You can also install it through npm: 21 | 22 | ```bash 23 | npm install glslEditor --save 24 | ``` 25 | 26 | And then you are ready to use it by passing an **DOM element** or **query selector string**, and a set of options; 27 | 28 | ```html 29 | 30 |
31 | 32 | 43 | ``` 44 | 45 | This is a list of all the **options** you can set up: 46 | 47 | | Property | type | description | default | 48 | |----------------------|------|---|-----| 49 | | ```canvas_size``` |number| Initial square size of the shader canvas |```250```| 50 | | ```canvas_width``` |number| Initial width of the shader canvas |```250```| 51 | | ```canvas_height``` |number| Initial height of the shader canvas |```250```| 52 | | ```canvas_draggable```| bool | Enables dragging, resizing and snaping capabilities to the shader canvas |```false```| 53 | | ```canvas_follow``` | bool | Enables the shader canvas to follow the curser |```false```| 54 | | ```theme``` | string | Codemirror style to use on the editor |```"default"```| 55 | | ```menu``` | bool | Adds a menu that contain: 'new', 'open', 'save' and 'share' options | ```false```| 56 | | ```multipleBuffers``` | bool | Allows the creation of new tabs |```false```| 57 | | ```fileDrops``` | bool | Listen to Drag&Drop events |```false```| 58 | | ```watchHash```| bool | Listen to changes on the wash path to load files |```false```| 59 | | ```frag_header``` | string| Adds a hidden header to every shader before compiling |```""```| 60 | | ```frag_footer``` | string| Adds a hidden footer to every shader before compiling |```""```| 61 | | ```indentUnit``` | number | How many spaces a block should be indented | ```4``` | 62 | | ```tabSize``` | number | The width of a tab character | ```4``` | 63 | | ```indentWithTabs``` | bool | Whether, when indenting, the spaces should be replaced by tabs | ```false``` | 64 | | ```lineWrapping``` | bool | Whether CodeMirror should wrap for long lines | ```true``` | 65 | | ```autofocus``` | bool | Can be used to make CodeMirror focus itself on initialization | ```true``` | 66 | 67 | ## Some of the features features 68 | 69 | - Inline Color picker and 3D vector picker for '''vec3'' 70 | 71 | ![](http://patriciogonzalezvivo.com/images/glslEditor/pickers1.gif) 72 | 73 | - Inline Trackpad for '''vec2''' 74 | 75 | ![](http://patriciogonzalezvivo.com/images/glslEditor/picker2.gif) 76 | 77 | - Slider for floats 78 | 79 | - Inline error display 80 | 81 | ![](http://patriciogonzalezvivo.com/images/glslEditor/error.gif) 82 | 83 | - Breakpoints for variables 84 | 85 | ![](http://patriciogonzalezvivo.com/images/glslEditor/debugger.gif) 86 | 87 | ## Electron Version 88 | 89 | When developing use this to automatically reload Electron on every change 90 | 91 | ```bash 92 | npm run dev 93 | ``` 94 | 95 | For use just do: 96 | 97 | ```bash 98 | npm run start 99 | ``` 100 | 101 | 102 | ## TODOs 103 | 104 | - [ ] Twitter sharing options 105 | - [ ] Facebook sharing options 106 | 107 | - [ ] Open modal from url, log or file 108 | 109 | - [ ] Uniform widgets 110 | - [ ] Time widget 111 | - [ ] Texture inspector 112 | 113 | ## Author 114 | 115 | [Patricio Gonzalez Vivo](https://twitter.com/patriciogv): [github](https://github.com/patriciogonzalezvivo) | [twitter](https://twitter.com/patriciogv) | [website](http://patriciogonzalezvivo.com) 116 | 117 | ## Acknowledgments 118 | 119 | Special thanks to: 120 | 121 | - [Lou Huang](@saikofish): glslEditor born from learned leassons on [TangramPlay](http://tangrams.github.io/tangram-play/). His code and wizdom is all arround this project. 122 | - [Brett Camper](@professorlemeza): media capture and texture class (on GlslCanvas) are totally his credit. 123 | - [Jaume Sanchez Elias](@thespite): thanks for the big help with the profiler tester. 124 | - [Kenichi Yoneda (Kynd)](@kynd.info): helped with the UI and presentation mode 125 | - [Thomas Hooper](@tdhooper): performance improvements 126 | 127 | 128 | -------------------------------------------------------------------------------- /favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patriciogonzalezvivo/glslEditor/4b834d308d19d322fcce6b0bbb700046140ae1eb/favicon.gif -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | // var gutil = require('gulp-util'); 5 | var derequire = require('gulp-derequire'); 6 | var livereload = require('gulp-livereload'); 7 | var plumber = require('gulp-plumber'); 8 | var sourcemaps = require('gulp-sourcemaps'); 9 | var electron = require('electron-connect').server.create(); 10 | 11 | var paths = { 12 | styles: 'src/css/**/*.css', 13 | scripts: 'src/js/**/*.js' 14 | }; 15 | 16 | // Build stylesheets 17 | gulp.task('css', function () { 18 | var postcss = require('gulp-postcss'); 19 | var autoprefixer = require('autoprefixer'); 20 | var cssimport = require('postcss-import'); 21 | var nested = require('postcss-nested'); 22 | var customProperties = require('postcss-custom-properties'); 23 | var colorHexAlpha = require('postcss-color-hex-alpha'); 24 | var csswring = require('csswring'); 25 | var reporter = require('postcss-reporter'); 26 | 27 | var plugins = [ 28 | cssimport, 29 | nested, 30 | customProperties(), 31 | colorHexAlpha(), 32 | autoprefixer({ browsers: ['last 2 versions', 'IE >= 11'] }), 33 | // preserveHacks is true because NOT preserving them doesn't mean 34 | // delete the hack, it means turn it into real CSS. Which is not 35 | // what we want! 36 | csswring({ removeAllComments: true, preserveHacks: true }), 37 | reporter() 38 | ]; 39 | 40 | return gulp.src('./src/css/glslEditor.css') 41 | .pipe(plumber()) 42 | .pipe(sourcemaps.init()) 43 | .pipe(postcss(plugins)) 44 | .pipe(sourcemaps.write('.')) 45 | .pipe(gulp.dest('./build')) 46 | .pipe(livereload()); 47 | }); 48 | 49 | // Build Javascripts 50 | gulp.task('js', function () { 51 | var browserify = require('browserify'); 52 | var shim = require('browserify-shim'); 53 | var babelify = require('babelify'); 54 | var source = require('vinyl-source-stream'); 55 | var buffer = require('vinyl-buffer'); 56 | var uglify = require('gulp-uglify'); 57 | var rename = require('gulp-rename'); 58 | 59 | var bundle = browserify({ 60 | entries: 'src/js/GlslEditor.js', 61 | standalone: 'GlslEditor', 62 | debug: true, 63 | transform: [ 64 | babelify.configure({ optional: ['runtime'] }), 65 | shim 66 | ] 67 | }); 68 | 69 | return bundle.bundle() 70 | .pipe(plumber()) 71 | .pipe(source('glslEditor.js')) 72 | .pipe(derequire()) 73 | .pipe(buffer()) 74 | // .pipe(sourcemaps.init({ loadMaps: true })) 75 | // Add transformation tasks to the pipeline here. 76 | // .on('error', gutil.log) 77 | // .pipe(sourcemaps.write('.')) 78 | .pipe(gulp.dest('./build')) 79 | .pipe(uglify()) 80 | .pipe(rename({ extname: '.min.js' })) 81 | .pipe(gulp.dest('./build')); 82 | }); 83 | 84 | // Rerun the task when a file changes 85 | gulp.task('watch', function () { 86 | livereload.listen(); 87 | gulp.watch(paths.styles, ['css']); 88 | gulp.watch(paths.scripts, ['js']); 89 | }); 90 | 91 | gulp.task('run', function () { 92 | // Start browser process 93 | electron.start(); 94 | // Reload browser process 95 | gulp.watch(['build/glslEditor.css', 'build/glslEditor.js','src/index.html'], electron.reload); 96 | gulp.watch(['src/main.js'], electron.restart); 97 | }); 98 | 99 | // Build files, do not watch 100 | gulp.task('build', ['css', 'js']); 101 | 102 | gulp.task('electron', ['css', 'js', 'watch', 'run']); 103 | 104 | // The default task (called when you run `gulp` from cli) 105 | gulp.task('default', ['css', 'js', 'watch']); 106 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GLSL Editor 7 | 8 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 39 | 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glslEditor", 3 | "version": "0.0.23", 4 | "description": "Simple GLSL Fragment Shader Editor", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "dev:vite": "vite src/", 8 | "build": "gulp", 9 | "dev": "gulp electron", 10 | "start": "electron .", 11 | "package-local": "electron-packager . --out ./build --overwrite", 12 | "package-all": "electron-packager . --out ./build --overwrite --all", 13 | "postinstall": "ln -nsf ../src/js node_modules/app", 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "lint": "eslint *.js src/" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/patriciogonzalezvivo/glslEditor.git" 20 | }, 21 | "keywords": [ 22 | "glsl", 23 | "shader", 24 | "editor" 25 | ], 26 | "author": "Patricio Gonzalez Vivo", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/patriciogonzalezvivo/glslEditor/issues" 30 | }, 31 | "homepage": "https://github.com/patriciogonzalezvivo/glslEditor#readme", 32 | "devDependencies": { 33 | "autoprefixer": "^6.0.3", 34 | "babelify": "^6.4.0", 35 | "browserify": "^11.2.0", 36 | "browserify-shim": "^3.8.11", 37 | "csswring": "^4.0.0", 38 | "electron": "^1.6.5", 39 | "electron-connect": "^0.6.1", 40 | "electron-packager": "^8.6.0", 41 | "gulp": "^3.9.0", 42 | "gulp-derequire": "^2.1.0", 43 | "gulp-livereload": "^3.8.1", 44 | "gulp-plumber": "^1.0.1", 45 | "gulp-postcss": "^6.0.1", 46 | "gulp-rename": "^1.3.0", 47 | "gulp-sourcemaps": "^1.6.0", 48 | "gulp-uglify": "^1.4.2", 49 | "gulp-util": "^3.0.7", 50 | "jscs": "^2.4.0", 51 | "modernizr": "^3.1.0", 52 | "postcss-color-hex-alpha": "^2.0.0", 53 | "postcss-custom-properties": "^5.0.0", 54 | "postcss-import": "^7.1.0", 55 | "postcss-nested": "^1.0.0", 56 | "postcss-reporter": "^1.3.0", 57 | "vinyl-buffer": "^1.0.0", 58 | "vinyl-source-stream": "^1.1.0" 59 | }, 60 | "dependencies": { 61 | "babel": "^5.8.29", 62 | "babel-runtime": "5.8.29", 63 | "codemirror": "5.8.0", 64 | "cross-storage": "^1.0.0", 65 | "document-register-element": "^1.9.1", 66 | "eslint": "^4.11.0", 67 | "glslCanvas": "^0.2.6", 68 | "vite": "^3.2.1", 69 | "xhr": "^2.2.0" 70 | }, 71 | "overrides": { 72 | "graceful-fs": "^4.2.10" 73 | }, 74 | "browserify-shim": {} 75 | } 76 | -------------------------------------------------------------------------------- /src/css/ElectronApp.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color: rgb(39, 40, 34); 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | } 8 | 9 | /* Fixes drag issue when attempting to drag a file into non-editor portion of the page. */ 10 | #glsl_editor { 11 | display: block; 12 | height: 100%; 13 | } -------------------------------------------------------------------------------- /src/css/_debugger.css: -------------------------------------------------------------------------------- 1 | .ge_assing_marker { 2 | margin-left: -15px; 3 | color: var(--ui-highlight-color); 4 | font-size: 10px; 5 | /*transform: translate(0,50%);*/ 6 | } 7 | 8 | .ge_assing_marker_faster { 9 | color: green; 10 | background-color: green; 11 | } 12 | 13 | .ge_assing_marker_pct { 14 | position: absolute; 15 | top: 0px; 16 | left: -17px; 17 | height: 100%; 18 | z-index: -1; 19 | background-color: #555; 20 | } 21 | 22 | .ge_assing_marker_slower { 23 | background-color: red; 24 | } 25 | 26 | .ge_assing_marker:hover { 27 | color: white; 28 | } 29 | 30 | .ge_assing_marker_on { 31 | margin-left: -15px; 32 | color: white; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/css/_editor.css: -------------------------------------------------------------------------------- 1 | /* EDITOR */ 2 | 3 | .CodeMirror { 4 | font-family: var(--editor-font-family); 5 | font-size: 14px; 6 | line-height: 1.4; 7 | height: auto; 8 | z-index: var(--z-editor); 9 | } 10 | 11 | .CodeMirror-linenumbers { 12 | padding: 0 8px; 13 | font-family: Helvetica, Geneva, sans-serif; 14 | } 15 | 16 | .CodeMirror-linenumber.CodeMirror-gutter-elt { 17 | color: #546E7A; 18 | } 19 | 20 | /* Slightly more padding around the text content */ 21 | .CodeMirror pre { 22 | padding: 0 8px; 23 | } 24 | 25 | .CodeMirror-vscrollbar { 26 | visibility: hidden; 27 | } 28 | 29 | .ge_editor { 30 | background-color: #272823; 31 | } 32 | 33 | .ge_editor-unfocus { 34 | opacity: .5; 35 | filter: blur(1px); 36 | background-color: #272823; 37 | } 38 | 39 | .CodeMirror { 40 | margin-top: 12px; 41 | } 42 | -------------------------------------------------------------------------------- /src/css/_errors.css: -------------------------------------------------------------------------------- 1 | 2 | .ge-error, 3 | .ge-warning { 4 | position: relative; 5 | font-family: var(--editor-font-family); 6 | padding-left: 3em; /* Theoretically, aligns error text to the first tab stop */ 7 | padding-right: 10px; 8 | } 9 | 10 | .ge-error-icon, 11 | .ge-warning-icon { 12 | position: absolute; 13 | left: 0; 14 | /*top: 3px;*/ 15 | width: 3em; /* Centers the icon within the first tab stop */ 16 | text-align: center; 17 | } 18 | 19 | .ge-error { 20 | background: #be1a20; 21 | color: white; 22 | } 23 | 24 | .ge-warning { 25 | background: #ffcc00; 26 | color: black; 27 | } -------------------------------------------------------------------------------- /src/css/_icons.css: -------------------------------------------------------------------------------- 1 | .ge_export_icon { 2 | position: relative; 3 | float: right; 4 | right: 8px; 5 | bottom: 40px; 6 | width: 32px; 7 | height: 28px; 8 | border-radius:16px; 9 | padding-top:4px; 10 | /*box-shadow: var(--modal-shadow);*/ 11 | user-select: none; 12 | z-index: var(--z-menu); 13 | text-align:center; 14 | vertical-align: bottom; 15 | background-color: rgba(0,0,0,.5); 16 | font-size: 18px !important; 17 | color: var(--ui-component-text-color); 18 | cursor: pointer; 19 | } 20 | 21 | .ge_export_icon:hover { 22 | color: var(--ui-highlight-color); 23 | } 24 | -------------------------------------------------------------------------------- /src/css/_menu.css: -------------------------------------------------------------------------------- 1 | /* MENU */ 2 | 3 | .ge_menu_bar { 4 | position: fixed; 5 | width: 100%; 6 | margin: 0px; 7 | z-index: var(--z-menu); 8 | color: var(--ui-component-text-color); 9 | background-color: var(--ui-base-color); 10 | font-family: Helvetica, Geneva, sans-serif; 11 | font-weight: 400; 12 | } 13 | 14 | .ge_menu { 15 | display: inline-block; 16 | margin: 16px 32px 16px 16px; 17 | letter-spacing: 0.2em; 18 | font-size: 14px; 19 | } 20 | 21 | .ge_menu .material-icons { 22 | font-size: 18px; 23 | vertical-align: bottom; 24 | display: inline-block; 25 | margin-right: 4px; 26 | } 27 | 28 | .ge_menu--hidden { 29 | display: none; 30 | } 31 | 32 | .ge_menu_button { 33 | cursor: pointer; 34 | background: none; 35 | display: block; 36 | border: none; 37 | border-radius: 4px; 38 | padding: 0; 39 | font: inherit; 40 | color: inherit; 41 | outline: none; 42 | text-align: left; 43 | letter-spacing: 0.05em !important; 44 | } 45 | 46 | .ge_menu_highlight { 47 | width: 24px; 48 | color: #FFFFFF; 49 | } 50 | 51 | .ge_menu_disabled { 52 | width: 24px; 53 | color: #777777; 54 | } 55 | 56 | .ge_menu_button:hover { 57 | color: var(--ui-highlight-color); 58 | } 59 | 60 | .ge_sub_menu .ge_menu_button { 61 | width: 100%; 62 | } 63 | 64 | .ge_sub_menu_button { 65 | height: 35px; 66 | cursor: pointer; 67 | background: none; 68 | border: none; 69 | padding: 0; 70 | font: inherit; 71 | color: inherit; 72 | text-align: left; 73 | 74 | -moz-user-select: -moz-none; 75 | -khtml-user-select: none; 76 | -webkit-user-select: none; 77 | -ms-user-select: none; 78 | user-select: none; 79 | } 80 | 81 | .ge_sub_menu_button::selection { background: transparent;color:inherit; } 82 | .ge_sub_menu_button::-moz-selection { background: transparent;color:inherit; } 83 | .ge_sub_menu_button:hover { color: var(--ui-highlight-color); } 84 | 85 | .ge_sub_menu { 86 | padding: 4px 12px 4px 12px; 87 | font-family: Helvetica, Geneva, sans-serif; 88 | font-weight: 300; 89 | font-size: 14px; 90 | } 91 | 92 | .ge_menu_icon { 93 | display: inline-block; 94 | width: 48px; 95 | } 96 | -------------------------------------------------------------------------------- /src/css/_modals.css: -------------------------------------------------------------------------------- 1 | .ge_modal { 2 | box-sizing: border-box; 3 | position: absolute; 4 | z-index: var(--z-helpers); /* z-index will be modified by Greensock Draggable when dragging */ 5 | user-select: none; 6 | box-shadow: var(--modal-shadow); 7 | font-family: Helvetica, Geneva, sans-serif; 8 | font-weight: 100; 9 | } 10 | 11 | .ge_export_modal { 12 | overflow: hidden; 13 | color: var(--ui-component-text-color); 14 | background-color: var(--ui-base-color); 15 | /*box-shadow: var(--modal-shadow);*/ 16 | box-shadow: 0 5px 5px 5px rgba(0,0,0,.2),0 5px 0px 0px rgba(0,0,0,.25); 17 | user-select: none; 18 | cursor: pointer; 19 | margin: 0px; 20 | padding-right: 10px; 21 | padding-left: 10px; 22 | padding-top: 5px; 23 | padding-bottom: 5px; 24 | list-style-type: none; 25 | } 26 | 27 | .ge_tooltip_modal { 28 | padding: 10px; 29 | padding-top: 0px; 30 | padding-bottom: 0px; 31 | background-color: var(--ui-component-color); 32 | font-size: 14px; 33 | } 34 | 35 | .ge_tooltip_modal a { 36 | text-decoration: none; 37 | color: var(--ui-highlight-color); 38 | } 39 | 40 | .ge_tooltip_modal p { 41 | color: var(--ui-subtext-color); 42 | margin: 6px; 43 | } -------------------------------------------------------------------------------- /src/css/_panel.css: -------------------------------------------------------------------------------- 1 | .ge_panel { 2 | cursor: pointer; 3 | color: var(--ui-component-text-color); 4 | background-color: var(--ui-base-color); 5 | font-family: Helvetica, Geneva, sans-serif; 6 | font-weight: 100; 7 | margin: 0px; 8 | padding-top: 5px; 9 | z-index: var(--z-menu); 10 | border-bottom: solid 1px #444444; 11 | } 12 | 13 | li.ge_panel_tab { 14 | cursor: pointer; 15 | list-style-type: none; 16 | background-color: var(--ui-base-color); 17 | color: #777777; 18 | display: inline-block; 19 | text-align: center; 20 | padding: 8px 16px 8px 16px; 21 | margin-bottom: 8px; 22 | letter-spacing: 0.05em; 23 | font-face: monospace; 24 | font-size: 14px; 25 | } 26 | 27 | li.ge_panel_tab_active { 28 | cursor: pointer; 29 | //background-color: var(--ui-active-color); 30 | border-radius: 4px; 31 | list-style-type: none; 32 | display: inline-block; 33 | text-align: center; 34 | padding: 8px 16px 8px 16px; 35 | margin-bottom: 8px; 36 | letter-spacing: 0.05em; 37 | font-size: 14px; 38 | } 39 | 40 | li.ge_panel_tab:hover { 41 | color: var(--ui-highlight-color); 42 | } 43 | 44 | a.ge_panel_tab_close { 45 | cursor: pointer; 46 | font-size: 12px; 47 | margin-left: 10px; 48 | font-weight: 100; 49 | } 50 | -------------------------------------------------------------------------------- /src/css/_pickers.css: -------------------------------------------------------------------------------- 1 | .ge_picker_modal { 2 | box-sizing: border-box; 3 | position: absolute; 4 | padding: 0px; 5 | /*background-color: var(--ui-component-color);*/ 6 | overflow: hidden; 7 | box-shadow: var(--modal-shadow); 8 | user-select: none; 9 | z-index: var(--z-helpers); /* z-index will be modified by Greensock Draggable when dragging */ 10 | } 11 | 12 | .ge_picker_modal * { 13 | box-sizing: border-box; 14 | } 15 | 16 | .ge_picker_canvas { 17 | width: 100%; 18 | height: 100%; 19 | cursor: crosshair; 20 | } 21 | 22 | .ge_colorpicker_patch { 23 | position: absolute; 24 | left: 0; 25 | top: 0; 26 | width: 100%; 27 | height: 50px; 28 | } 29 | 30 | .ge_colorpicker_hsv-map { 31 | position: relative; 32 | height: 200px; 33 | width: 100%; 34 | top: 50px; 35 | } 36 | 37 | .ge_colorpicker_disc-cover { 38 | opacity: 0; 39 | background-color: #000; 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | width: 200px; 44 | height: 200px; 45 | border-radius: 50%; 46 | cursor: crosshair; 47 | } 48 | 49 | .ge_colorpicker_disc-cover::after { 50 | content: ''; 51 | background-color: transparent; 52 | position: absolute; 53 | top: -1px; 54 | left: -1px; 55 | width: 202px; 56 | height: 202px; 57 | border: 3px solid var(--ui-component-color); 58 | border-radius: 50%; 59 | opacity: 1; 60 | pointer-events: none; 61 | box-sizing: border-box; 62 | } 63 | 64 | .ge_colorpicker_bar-bg, 65 | .ge_colorpicker_bar-white { 66 | position: absolute; 67 | right: 0; 68 | top: 0; 69 | width: 25px; 70 | height: 200px; 71 | } 72 | 73 | .ge_colorpicker_bar-white { 74 | background-color: #fff; 75 | } 76 | 77 | .ge_colorpicker_disc-cursor { 78 | position: absolute; 79 | border: 1px solid #eee; 80 | border-radius: 50%; 81 | width: 9px; 82 | height: 9px; 83 | margin: -5px; 84 | cursor: crosshair; 85 | } 86 | 87 | .ge_colorpicker_dark .ge_colorpicker_disc-cursor { 88 | border-color: #333; 89 | } 90 | 91 | /*.ge_colorpicker_no-cursor, 92 | .ge_colorpicker_no-cursor .ge_colorpicker_patch, 93 | .ge_colorpicker_no-cursor .ge_colorpicker_disc-cursor, 94 | .ge_colorpicker_no-cursor .ge_colorpicker_disc-cover { 95 | cursor: none; 96 | }*/ 97 | 98 | .ge_colorpicker_bar-luminance { 99 | position: absolute; 100 | right: 0; 101 | top: 0; 102 | } 103 | 104 | .ge_colorpicker_bar-cursors { 105 | position: absolute; 106 | right: 0; 107 | width: 25px; 108 | top: 0; 109 | height: 200px; 110 | overflow: hidden; 111 | } 112 | .ge_colorpicker_bar-cursor-left, 113 | .ge_colorpicker_bar-cursor-right { 114 | position: absolute; 115 | width: 0; 116 | height: 0; 117 | border: 4px solid transparent; 118 | margin-top: -4px; 119 | } 120 | .ge_colorpicker_bar-cursor-left { 121 | left: 0; 122 | border-left: 4px solid #eee; 123 | } 124 | .ge_colorpicker_bar-cursor-right { 125 | right: 0; 126 | border-right: 4px solid #eee; 127 | } 128 | 129 | .ge_colorpicker_dark .ge_colorpicker_bar-cursor-left { 130 | border-left-color: #333; 131 | } 132 | .ge_colorpicker_dark .ge_colorpicker_bar-cursor-right { 133 | border-right-color: #333; 134 | } 135 | 136 | .ge_colorpicker_link-button { 137 | position: absolute; 138 | bottom: 5px; 139 | left: 5px; 140 | color: #85CCC4; /*white;*/ 141 | font-family: Helvetica, Arial, sans-serif;; 142 | font-size: 20px; 143 | font-weight: 100; 144 | cursor: pointer; 145 | } 146 | 147 | -------------------------------------------------------------------------------- /src/css/_shader.css: -------------------------------------------------------------------------------- 1 | /* CANVAS */ 2 | 3 | .ge_canvas_container { 4 | position: fixed; 5 | right: 0px; 6 | margin: 0px; 7 | padding: 0px; 8 | transition: top 0.05s ease-out; 9 | z-index: var(--z-shader); 10 | } 11 | 12 | .ge_canvas { 13 | margin: 0px; 14 | padding: 0px; 15 | z-index: var(--z-shader); 16 | } 17 | 18 | .ge_control { 19 | margin: 0px; 20 | padding: 0px; 21 | position: absolute; 22 | width: 100%; 23 | bottom: 16px; 24 | 25 | opacity: 1; 26 | -webkit-transition: opacity .3s; 27 | -moz-transition: opacity .3s; 28 | transition: opacity .3s; 29 | z-index: var(--z-shader); 30 | } 31 | 32 | .ge_control_panel { 33 | padding: 8px; 34 | border-radius: 4px; 35 | text-align: center; 36 | position: absolute; 37 | right: 0; 38 | left: 0; 39 | bottom: 0; 40 | margin: auto; 41 | width: 120px; 42 | background-color: rgba(0,0,0,.5); 43 | font-size: 18px !important; 44 | opacity: 1; 45 | -webkit-transition: opacity .3s; 46 | -moz-transition: opacity .3s; 47 | transition: opacity .3s; 48 | z-index: var(--z-shader); 49 | } 50 | 51 | 52 | 53 | .ge_contol li { 54 | margin: 0; 55 | padding: 0; 56 | list-style-type: none; 57 | } 58 | 59 | .ge_control_hidden { 60 | /*display: none;*/ 61 | opacity: 0; 62 | } 63 | 64 | .ge_control_element { 65 | background-color: rgba(0,0,0,0); 66 | color: var(--ui-component-text-color); 67 | font-family: Helvetica, Geneva, sans-serif; 68 | font-weight: 300; 69 | display: inline; 70 | -moz-user-select: -moz-none; 71 | -khtml-user-select: none; 72 | -webkit-user-select: none; 73 | -ms-user-select: none; 74 | user-select: none; 75 | } 76 | 77 | .ge_control_element_button { 78 | display: inline-block; 79 | margin: 0px 8px 0px 8px; 80 | width: 24px; 81 | height: 24px; 82 | cursor: pointer; 83 | background: none; 84 | border: none; 85 | padding: 0; 86 | font: inherit; 87 | color: inherit; 88 | text-align: center; 89 | vertical-align: middle; 90 | font-size: 14px; 91 | overflow: hidden; 92 | outline: none; 93 | //border: solid 1px rgba(255, 255, 255, 0.8); 94 | //border-radius: 4px; 95 | -moz-user-select: -moz-none; 96 | -khtml-user-select: none; 97 | -webkit-user-select: none; 98 | -ms-user-select: none; 99 | user-select: none; 100 | } 101 | 102 | .ge_control_element_button::selection { background: transparent;color:inherit; } 103 | .ge_control_element_button::-moz-selection { background: transparent;color:inherit; } 104 | 105 | .ge_control_element_button:hover { 106 | color: var(--ui-highlight-color); 107 | } 108 | 109 | .ghostdom { 110 | background: #999; 111 | opacity: 0.2; 112 | 113 | position: absolute; 114 | right: 0px; 115 | margin: 0; 116 | padding: 0; 117 | 118 | -webkit-transition: all 0.25s ease-in-out; 119 | -moz-transition: all 0.25s ease-in-out; 120 | -ms-transition: all 0.25s ease-in-out; 121 | -o-transition: all 0.25s ease-in-out; 122 | transition: all 0.25s ease-in-out; 123 | 124 | z-index: var(--z-shader-ghost); 125 | } 126 | -------------------------------------------------------------------------------- /src/css/glslEditor.css: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | 3 | /* Native Codemirror */ 4 | @import 'codemirror/lib/codemirror.css'; 5 | @import 'codemirror/addon/dialog/dialog.css'; 6 | @import 'codemirror/addon/fold/foldgutter.css'; 7 | @import 'codemirror/addon/hint/show-hint.css'; 8 | 9 | /* Themes */ 10 | @import 'codemirror/theme/ambiance.css'; 11 | @import 'codemirror/theme/base16-dark.css'; 12 | @import 'codemirror/theme/base16-light.css'; 13 | @import 'codemirror/theme/dracula.css'; 14 | @import 'codemirror/theme/eclipse.css'; 15 | @import 'codemirror/theme/elegant.css'; 16 | @import 'codemirror/theme/icecoder.css'; 17 | @import 'codemirror/theme/lesser-dark.css'; 18 | @import 'codemirror/theme/material.css'; 19 | @import 'codemirror/theme/mdn-like.css'; 20 | @import 'codemirror/theme/midnight.css'; 21 | @import 'codemirror/theme/monokai.css'; 22 | @import 'codemirror/theme/neat.css'; 23 | @import 'codemirror/theme/neo.css'; 24 | @import 'codemirror/theme/pastel-on-dark.css'; 25 | @import 'codemirror/theme/railscasts.css'; 26 | @import 'codemirror/theme/seti.css'; 27 | @import 'codemirror/theme/solarized.css'; 28 | @import 'codemirror/theme/tomorrow-night-bright.css'; 29 | @import 'codemirror/theme/tomorrow-night-eighties.css'; 30 | @import 'codemirror/theme/ttcn.css'; 31 | @import 'codemirror/theme/yeti.css'; 32 | 33 | /* Elements */ 34 | @import '_menu'; 35 | @import '_panel'; 36 | @import '_editor'; 37 | @import '_shader'; 38 | 39 | @import '_pickers'; 40 | @import '_modals'; 41 | @import '_errors'; 42 | @import '_debugger'; 43 | @import '_icons'; 44 | 45 | /* VARIABLES */ 46 | 47 | :root { 48 | --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 49 | /* -- font-family: 'Montserrat'; */ 50 | --editor-font-family: 'Source Code Pro', monospace; 51 | --ui-base-color: #2f302b; 52 | --ui-component-color: #2e3033; 53 | --ui-element-color: #3a3c40; 54 | --ui-active-color: #171e22; 55 | --ui-highlight-color: #B3E5FC; 56 | --ui-component-text-color: #eefafa; 57 | --ui-link-text-color: #e1eeee; 58 | --ui-subtext-color: #c0c6c6; 59 | --ui-border-color: #999ca0; 60 | --modal-shadow: 0 0 0 5px rgba(0,0,0,0.2), 0 4px 8px 0 rgba(0,0,0,0.25); 61 | 62 | /* Z-INDEX SCALE */ 63 | --z-editor: 100; 64 | --z-shader-ghost: 199; 65 | --z-shader: 200; 66 | --z-divider: 250; 67 | --z-menu: 300; 68 | --z-helpers: 1000; 69 | } 70 | 71 | .CodeMirror-hints { 72 | position: absolute; 73 | z-index: 10; 74 | overflow: hidden; 75 | list-style: none; 76 | 77 | margin: 0; 78 | padding: 2px; 79 | 80 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 81 | -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 82 | box-shadow: 2px 3px 5px rgba(0,0,0,.2); 83 | border-radius: 3px; 84 | border: 1px solid silver; 85 | 86 | background: white; 87 | font-size: 90%; 88 | font-family: monospace; 89 | 90 | max-height: 20em; 91 | overflow-y: auto; 92 | 93 | z-index: 100; 94 | } 95 | 96 | .CodeMirror-hint { 97 | margin: 0; 98 | padding: 0 4px; 99 | border-radius: 2px; 100 | max-width: 300em; 101 | overflow: hidden; 102 | white-space: pre; 103 | color: black; 104 | cursor: pointer; 105 | z-index: 100; 106 | } 107 | 108 | li.CodeMirror-hint-active { 109 | background: #08f; 110 | color: white; 111 | z-index: 100; 112 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GLSL Editor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /src/js/GlslEditor.js: -------------------------------------------------------------------------------- 1 | import "document-register-element"; 2 | import Shader from "./core/Shader"; 3 | import { initEditor, focusAll } from "./core/Editor"; 4 | 5 | import Menu from "./ui/Menu"; 6 | import Helpers from "./ui/Helpers"; 7 | import ErrorsDisplay from "./ui/ErrorsDisplay"; 8 | import VisualDebugger from "./ui/VisualDebugger"; 9 | import ExportIcon from "./ui/ExportIcon"; 10 | 11 | import FileDrop from "./io/FileDrop"; 12 | import HashWatch from "./io/HashWatch"; 13 | import BufferManager from "./io/BufferManager"; 14 | import LocalStorage from "./io/LocalStorage"; 15 | const STORAGE_LAST_EDITOR_CONTENT = "last-content"; 16 | 17 | // Import Utils 18 | import xhr from "xhr"; 19 | import { subscribeMixin } from "./tools/mixin"; 20 | import { saveAs } from "./tools/download"; 21 | import { getJSON } from "./tools/common"; 22 | import CodeMirror from "codemirror"; 23 | 24 | // // Cross storage for Openframe -- allows restricted access to certain localStorage operations 25 | // // on the openframe domain 26 | // import { CrossStorageClient } from "cross-storage"; 27 | // import { getMode } from "codemirror"; 28 | 29 | const EMPTY_FRAG_SHADER = `// Author: 30 | // Title: 31 | 32 | #ifdef GL_ES 33 | precision mediump float; 34 | #endif 35 | 36 | uniform vec2 u_resolution; 37 | uniform vec2 u_mouse; 38 | uniform float u_time; 39 | 40 | void main() { 41 | vec2 st = gl_FragCoord.xy/u_resolution.xy; 42 | st.x *= u_resolution.x/u_resolution.y; 43 | 44 | vec3 color = vec3(0.); 45 | color = vec3(st.x,st.y,abs(sin(u_time))); 46 | 47 | gl_FragColor = vec4(color,1.0); 48 | }`; 49 | 50 | export default class GlslEditor { 51 | constructor(selector, options) { 52 | this.createFontLink(); 53 | subscribeMixin(this); 54 | 55 | if ( 56 | typeof selector === "object" && 57 | selector.nodeType && 58 | selector.nodeType === 1 59 | ) { 60 | this.container = selector; 61 | } 62 | else if (typeof selector === "string") { 63 | this.container = document.querySelector(selector); 64 | if (!this.container) { 65 | throw new Error(`element ${selector} not present`); 66 | } 67 | } 68 | else { 69 | console.log( 70 | "Error, type " + typeof selector + " of " + selector + " is unknown" 71 | ); 72 | return; 73 | } 74 | 75 | this.options = {}; 76 | this.change = false; 77 | this.autoupdate = true; 78 | this.lygia_glob = null; 79 | 80 | if (options) 81 | this.options = options; 82 | 83 | if (this.options.imgs === undefined) 84 | this.options.imgs = []; 85 | 86 | if (this.options.display_menu === undefined) 87 | this.options.display_menu = true; 88 | 89 | if (this.container.hasAttribute("data-textures")) { 90 | let imgList = this.container.getAttribute("data-textures").split(","); 91 | for (let i in imgList) 92 | this.options.imgs.push(imgList[i]); 93 | 94 | } 95 | 96 | // Default Theme 97 | if (!this.options.theme) 98 | this.options.theme = "default"; 99 | 100 | // Default Context 101 | if (!this.options.frag) { 102 | var innerHTML = this.container.innerHTML.replace(/<br>/g, ""); 103 | innerHTML = innerHTML.replace(/
/g, ""); 104 | innerHTML = innerHTML.replace(/ /g, ""); 105 | innerHTML = innerHTML.replace(/</g, "<"); 106 | innerHTML = innerHTML.replace(/>/g, ">"); 107 | innerHTML = innerHTML.replace(/&/g, "&"); 108 | this.options.frag = innerHTML || EMPTY_FRAG_SHADER; 109 | 110 | if (innerHTML) 111 | this.container.innerHTML = ""; 112 | } 113 | 114 | // Default invisible Fragment header 115 | if (!this.options.frag_header) 116 | this.options.frag_header = ""; 117 | 118 | // Default invisible Fragment footer 119 | if (!this.options.frag_footer) 120 | this.options.frag_footer = ""; 121 | 122 | // Listen to hash changes 123 | if (this.options.watchHash) 124 | new HashWatch(this); 125 | 126 | // Load UI 127 | if (this.options.menu) 128 | this.menu = new Menu(this); 129 | 130 | // Support for multiple buffers 131 | if (this.options.multipleBuffers) 132 | this.bufferManager = new BufferManager(this); 133 | 134 | // Listen to file drops 135 | if (this.options.fileDrops) 136 | new FileDrop(this); 137 | 138 | if (this.options.indentUnit === undefined) 139 | this.options.indentUnit = 4; 140 | 141 | if (this.options.tabSize === undefined) 142 | this.options.tabSize = 4; 143 | 144 | if (this.options.indentWithTabs === undefined) 145 | this.options.indentWithTabs = false; 146 | 147 | if (this.options.lineWrapping === undefined) 148 | this.options.lineWrapping = true; 149 | 150 | if (this.options.autofocus === undefined) 151 | this.options.autofocus = true; 152 | 153 | // CORE elements 154 | this.shader = new Shader(this); 155 | this.editor = initEditor(this); 156 | 157 | this.helpers = new Helpers(this); 158 | this.errorsDisplay = new ErrorsDisplay(this); 159 | this.visualDebugger = new VisualDebugger(this); 160 | 161 | if (this.options.exportIcon) 162 | this.export = new ExportIcon(this); 163 | 164 | // EVENTS 165 | this.editor.on("change", () => { 166 | if (this.autoupdate) { 167 | this.update(); 168 | } 169 | }); 170 | 171 | if (this.options.canvas_follow) { 172 | this.shader.el.style.position = "relative"; 173 | 174 | if (this.options.canvas_float) 175 | this.shader.el.style.float = this.options.canvas_float; 176 | 177 | this.editor.on("cursorActivity", (cm) => { 178 | let height = 179 | cm.heightAtLine(cm.getCursor().line + 1, "local") - 180 | this.shader.el.clientHeight; 181 | if (height < 0) { 182 | height = 0.0; 183 | } 184 | this.shader.el.style.top = height.toString() + "px"; 185 | }); 186 | } 187 | 188 | this.editor.on('inputRead', (cm, change) => { 189 | let cur = cm.getCursor(), 190 | token = cm.getTokenAt(cur); 191 | let line = token.string.trim(); 192 | 193 | if (line.startsWith('#include')) { 194 | let path = line.substring(10); 195 | if (this.lygia_glob === null) { 196 | getJSON('https://lygia.xyz/glsl.json', (err, data) => { 197 | if (err === null) { 198 | this.lygia_glob = data; 199 | } 200 | }); 201 | } 202 | console.log('autocomplete for', path); 203 | 204 | let start = token.start; 205 | let end = cur.ch; 206 | let lineN = cur.line; 207 | 208 | let result = [] 209 | 210 | if (this.lygia_glob !== null) { 211 | this.lygia_glob.forEach( (w) => {if (w.startsWith(path)) result.push('#include \"' + w + '\"');} ); 212 | result.sort(); 213 | } 214 | 215 | if (result.length > 0) { 216 | CodeMirror.showHint(cm, () => { 217 | let rta = { 218 | list: result, 219 | from: CodeMirror.Pos(lineN, start), 220 | to: CodeMirror.Pos(lineN, end) 221 | }; 222 | 223 | console.log(rta); 224 | return rta; 225 | }, {completeSingle: true, alignWithWord: true}); 226 | } 227 | } 228 | }); 229 | 230 | // If the user bails for whatever reason, hastily shove the contents of 231 | // the editor into some kind of storage. This overwrites whatever was 232 | // there before. Note that there is not really a way of handling unload 233 | // with our own UI and logic, since this allows for widespread abuse 234 | // of normal browser functionality. 235 | window.addEventListener("beforeunload", (event) => { 236 | let content = {}; 237 | if ( 238 | this.bufferManager && 239 | Object.keys(this.bufferManager.buffers).length !== 0 240 | ) { 241 | for (var key in this.bufferManager.buffers) { 242 | content[key] = this.bufferManager.buffers[key].getValue(); 243 | } 244 | } else { 245 | content[new Date().getTime().toString()] = this.editor.getValue(); 246 | } 247 | 248 | if (this.options.menu) { 249 | LocalStorage.setItem( 250 | STORAGE_LAST_EDITOR_CONTENT, 251 | JSON.stringify(content) 252 | ); 253 | } 254 | }); 255 | 256 | if (this.options.menu) { 257 | // If there is previus content load it. 258 | let oldContent = JSON.parse( 259 | LocalStorage.getItem(STORAGE_LAST_EDITOR_CONTENT) 260 | ); 261 | if (oldContent) { 262 | for (var key in oldContent) { 263 | this.open(oldContent[key], key); 264 | } 265 | } else { 266 | this.new(); 267 | } 268 | } else { 269 | this.new(); 270 | } 271 | 272 | // if (this.options.menu || this.options.exportIcon) { 273 | // // setup CrossStorage client 274 | // this.storage = new CrossStorageClient("https://openframe.io/hub.html"); 275 | // this.storage.onConnect().then(() => { 276 | // console.log("Connected to OpenFrame [o]"); 277 | // }); 278 | // // }).bind(this); 279 | // } 280 | 281 | return this; 282 | } 283 | 284 | new() { 285 | this.setContent( 286 | this.options.frag || EMPTY_FRAG_SHADER, 287 | new Date().getTime().toString() 288 | ); 289 | this.trigger("new_content", {}); 290 | this.options.frag = null; 291 | } 292 | 293 | setContent(shader, tabName) { 294 | // If the string is CODE 295 | if (this.shader && this.shader.canvas) { 296 | if (this.debugging) { 297 | this.debugging = false; 298 | focusAll(this.editor); 299 | } 300 | this.shader.canvas.load( 301 | this.options.frag_header + shader + this.options.frag_footer 302 | ); 303 | } 304 | 305 | if (this.editor) { 306 | if (tabName !== undefined && this.bufferManager !== undefined) { 307 | this.bufferManager.open(tabName, shader); 308 | this.bufferManager.select(tabName); 309 | } 310 | else { 311 | this.editor.setValue(shader); 312 | this.editor.setSize(null, this.editor.getDoc().height + "px"); 313 | this.editor.setSize(null, "auto"); 314 | this.filename = tabName; 315 | } 316 | } 317 | this.change = true; 318 | } 319 | 320 | open(shader, tabName) { 321 | if (typeof shader === "object") { 322 | const reader = new FileReader(); 323 | let ge = this; 324 | reader.onload = (e) => { 325 | ge.setContent(e.target.result, shader.name); 326 | }; 327 | reader.readAsText(shader); 328 | } else if (typeof shader === "string") { 329 | if (/\.frag$/.test(shader) || /\.fs$/.test(shader)) { 330 | // If the string is an URL 331 | xhr.get(shader, (error, response, body) => { 332 | if (error) { 333 | console.log("Error downloading ", shader, error); 334 | return; 335 | } 336 | this.setContent(body, tabName); 337 | }); 338 | } else { 339 | this.setContent(shader, tabName); 340 | } 341 | } 342 | } 343 | 344 | getContent() { 345 | return this.editor.getValue(); 346 | } 347 | 348 | getAuthor() { 349 | let content = this.getContent(); 350 | let result = content.match( 351 | /\/\/\s*[A|a]uthor\s*[\:]?\s*([\w|\s|\@|\(|\)|\-|\_]*)/i 352 | ); 353 | if (result && !(result[1] === " " || result[1] === "")) { 354 | let author = result[1].replace(/(\r\n|\n|\r)/gm, ""); 355 | return author; 356 | } else { 357 | return "unknown"; 358 | } 359 | } 360 | 361 | getTitle() { 362 | let content = this.getContent(); 363 | let result = content.match( 364 | /\/\/\s*[T|t]itle\s*:\s*([\w|\s|\@|\(|\)|\-|\_]*)/i 365 | ); 366 | if (result && !(result[1] === " " || result[1] === "")) { 367 | let title = result[1].replace(/(\r\n|\n|\r)/gm, ""); 368 | return title; 369 | } else if (this.bufferManager !== undefined) { 370 | return this.bufferManager.current; 371 | } else { 372 | return "unknown"; 373 | } 374 | } 375 | 376 | // Returns Promise 377 | getOfToken() { 378 | return this.storage.get("accessToken"); 379 | } 380 | 381 | download() { 382 | let content = this.getContent(); 383 | let name = this.getTitle(); 384 | if (name !== "") { 385 | name += "-"; 386 | } 387 | name += new Date().getTime(); 388 | 389 | // Download code 390 | const blob = new Blob([content], { type: "text/plain" }); 391 | saveAs(blob, name + ".frag"); 392 | this.editor.doc.markClean(); 393 | this.change = false; 394 | } 395 | 396 | update() { 397 | if (this.debugging) { 398 | this.debugging = false; 399 | focusAll(this.editor); 400 | } 401 | 402 | if (this.visualDebugger.testingResults.length) 403 | this.visualDebugger.clean(); 404 | 405 | this.shader.canvas.load( 406 | this.options.frag_header + 407 | this.editor.getValue() + 408 | this.options.frag_footer 409 | ); 410 | } 411 | 412 | createFontLink() { 413 | var head = document.getElementsByTagName("head")[0]; 414 | var link = document.createElement("link"); 415 | link.href = "https://fonts.googleapis.com/icon?family=Material+Icons"; 416 | link.type = "text/css"; 417 | link.rel = "stylesheet"; 418 | link.media = "screen,print"; 419 | head.appendChild(link); 420 | document.getElementsByTagName("head")[0].appendChild(link); 421 | } 422 | 423 | togglePresentationWindow(flag) { 424 | this.pWindowOpen = flag; 425 | if (flag) 426 | this.shader.openWindow(); 427 | else 428 | this.shader.closeWindow(); 429 | } 430 | 431 | onClosePresentationWindow() { 432 | this.pWindowOpen = false; 433 | } 434 | } 435 | 436 | window.GlslEditor = GlslEditor; 437 | 438 | var GlslWebComponent = function () {}; 439 | GlslWebComponent.prototype = Object.create(HTMLElement.prototype); 440 | GlslWebComponent.prototype.createdCallback = function createdCallback() { 441 | var options = { 442 | canvas_size: 150, 443 | canvas_follow: true, 444 | tooltips: true, 445 | }; 446 | 447 | for (var i = 0; i < this.attributes.length; i++) { 448 | var attribute = this.attributes[i]; 449 | if (attribute.specified) { 450 | var value = attribute.value; 451 | 452 | if (value === "true") 453 | value = true; 454 | else if (value === "false") 455 | value = false; 456 | else if (parseInt(value)) 457 | value = parseInt(value); 458 | 459 | options[attribute.name] = value; 460 | } 461 | } 462 | 463 | this.glslEditor = new GlslEditor(this, options); 464 | }; 465 | 466 | document.registerElement("glsl-editor", GlslWebComponent); 467 | -------------------------------------------------------------------------------- /src/js/core/Editor.js: -------------------------------------------------------------------------------- 1 | // Import CodeMirror 2 | import CodeMirror from 'codemirror'; 3 | 4 | // Import CodeMirror addons and modules 5 | import 'codemirror/addon/search/search'; 6 | import 'codemirror/addon/search/searchcursor'; 7 | import 'codemirror/addon/comment/comment'; 8 | import 'codemirror/addon/dialog/dialog'; 9 | import 'codemirror/addon/edit/matchbrackets'; 10 | import 'codemirror/addon/edit/closebrackets'; 11 | import 'codemirror/addon/wrap/hardwrap'; 12 | import 'codemirror/addon/fold/foldcode'; 13 | import 'codemirror/addon/fold/foldgutter'; 14 | import 'codemirror/addon/fold/indent-fold'; 15 | import 'codemirror/addon/hint/show-hint'; 16 | import 'codemirror/addon/hint/javascript-hint'; 17 | import 'codemirror/addon/display/rulers'; 18 | import 'codemirror/addon/display/panel'; 19 | import 'codemirror/addon/hint/show-hint'; 20 | import 'codemirror/mode/clike/clike.js'; 21 | 22 | // Keymap 23 | import 'codemirror/keymap/sublime'; 24 | 25 | const UNFOCUS_CLASS = 'ge_editor-unfocus'; 26 | 27 | export function initEditor (main) { 28 | if (main.options.lineNumbers === undefined) 29 | main.options.lineNumbers = true; 30 | 31 | // CREATE AND START CODEMIRROR 32 | let el = document.createElement('div'); 33 | el.setAttribute('class', 'ge_editor'); 34 | 35 | // If there is a menu offset the editor to come after it 36 | if (main.menu) 37 | el.style.paddingTop = (main.menu.el.clientHeight || main.menu.el.offsetHeight || main.menu.el.scrollHeight) + 'px'; 38 | 39 | main.container.appendChild(el); 40 | 41 | function fillInclude(cm, option) { 42 | return new Promise( (accept) => { 43 | setTimeout( () => { 44 | let cur = cm.getCursor(). 45 | line = cm.getLine(cur.line).trim(); 46 | var start = cur.ch, end = cur.ch; 47 | 48 | if (line.startsWith('#include \"lygia')) { 49 | let path = line.substring(10); 50 | if (this.lygia_glob === null) { 51 | getJSON('https://lygia.xyz/glsl.json', (err, data) => { 52 | if (err === null) { 53 | this.lygia_glob = data; 54 | } 55 | }); 56 | } 57 | console.log('autocomplete for', path); 58 | 59 | let start = token.start; 60 | let end = cur.ch; 61 | let lineN = cur.line; 62 | 63 | let result = [] 64 | 65 | if (this.lygia_glob !== null) { 66 | this.lygia_glob.forEach( (w) => {if (w.startsWith(path)) result.push('#include \"' + w + '\"');} ); 67 | result.sort(); 68 | } 69 | 70 | if (result.length > 0) { 71 | console.log(lineN, result); 72 | return accept({list: result, 73 | from: CodeMirror.Pos(cur.line, start), 74 | to: CodeMirror.Pos(cur.line, end)}) 75 | } 76 | } 77 | return accept(null) 78 | }, 100); 79 | }) 80 | } 81 | 82 | let cm = CodeMirror(el, { 83 | value: main.options.frag, 84 | viewportMargin: Infinity, 85 | lineNumbers: main.options.lineNumbers, 86 | matchBrackets: true, 87 | mode: 'x-shader/x-fragment', 88 | keyMap: 'sublime', 89 | autoCloseBrackets: true, 90 | extraKeys: { 'Ctrl-Space': 'autocomplete' }, 91 | showCursorWhenSelecting: true, 92 | theme: main.options.theme, 93 | dragDrop: false, 94 | indentUnit: main.options.indentUnit, 95 | tabSize: main.options.tabSize, 96 | indentWithTabs: main.options.indentWithTabs, 97 | gutters: main.options.lineNumbers ? ['CodeMirror-linenumbers', 'breakpoints'] : false, 98 | lineWrapping: main.options.lineWrapping, 99 | autofocus: main.options.autofocus 100 | }); 101 | 102 | return cm; 103 | } 104 | 105 | export function unfocusLine(cm, line) { 106 | if (line === null) {return;} 107 | cm.getDoc().addLineClass(line, 'gutter', UNFOCUS_CLASS); 108 | cm.getDoc().addLineClass(line, 'text', UNFOCUS_CLASS); 109 | } 110 | 111 | export function unfocusAll(cm) { 112 | for (let i = 0, j = cm.getDoc().lineCount(); i <= j; i++) 113 | unfocusLine(cm, i); 114 | } 115 | 116 | export function focusLine(cm, line) { 117 | if (line === null) {return;} 118 | cm.getDoc().removeLineClass(line, 'gutter', UNFOCUS_CLASS); 119 | cm.getDoc().removeLineClass(line, 'text', UNFOCUS_CLASS); 120 | } 121 | 122 | export function focusAll(cm) { 123 | for (let i = 0, j = cm.getDoc().lineCount(); i <= j; i++) 124 | focusLine(cm, i); 125 | } 126 | -------------------------------------------------------------------------------- /src/js/core/Shader.js: -------------------------------------------------------------------------------- 1 | import GlslCanvas from "glslCanvas"; 2 | import { subscribeInteractiveDom } from "../tools/interactiveDom"; 3 | import MediaCapture from "../tools/mediaCapture"; 4 | import MenuItem from "../ui/MenuItem"; 5 | import { saveAs } from "../tools/download"; 6 | 7 | var CONTROLS_CLASSNAME = "ge_control"; 8 | var CONTROLS_PANEL_CLASSNAME = "ge_control_panel"; 9 | 10 | export default class Shader { 11 | constructor(main) { 12 | this.main = main; 13 | this.options = main.options; 14 | this.frag = ""; 15 | 16 | // DOM CONTAINER 17 | this.el = document.createElement("div"); 18 | this.el.setAttribute("class", "ge_canvas_container"); 19 | // CREATE AND START GLSLCANVAS 20 | this.elCanvas = document.createElement("canvas"); 21 | this.elCanvas.setAttribute("class", "ge_canvas"); 22 | this.elCanvas.setAttribute( 23 | "data-fragment", 24 | this.options.frag_header + this.options.frag + this.options.frag_footer 25 | ); 26 | this.el.appendChild(this.elCanvas); 27 | let glslcanvas = new GlslCanvas(this.elCanvas, { 28 | premultipliedAlpha: false, 29 | preserveDrawingBuffer: true, 30 | backgroundColor: "rgba(1,1,1,1)", 31 | }); 32 | 33 | let width = this.options.canvas_width || this.options.canvas_size || "250"; 34 | let height = 35 | this.options.canvas_height || this.options.canvas_size || "250"; 36 | glslcanvas.canvas.style.width = width + "px"; 37 | glslcanvas.canvas.style.height = height + "px"; 38 | glslcanvas.resize(); 39 | 40 | this.canvas = glslcanvas; 41 | 42 | if (this.options.imgs && this.options.imgs.length > 0) { 43 | for (let i in this.options.imgs) { 44 | this.canvas.setUniform("u_tex" + i, this.options.imgs[i]); 45 | } 46 | } 47 | 48 | // Media Capture 49 | this.mediaCapture = new MediaCapture(); 50 | this.mediaCapture.setCanvas(this.elCanvas); 51 | this.canvas.on("render", () => { 52 | this.mediaCapture.completeScreenshot(); 53 | }); 54 | 55 | if (main.options.displayMenu) { 56 | // CONTROLS 57 | this.controlsContainer = document.createElement("ul"); 58 | this.controlsContainer.className = CONTROLS_CLASSNAME; 59 | this.controlPanel = document.createElement("ul"); 60 | this.controlPanel.className = CONTROLS_PANEL_CLASSNAME; 61 | this.controlsContainer.appendChild(this.controlPanel); 62 | this.el.appendChild(this.controlsContainer); 63 | this.controls = {}; 64 | // play/stop 65 | // this.controls.playPause = new MenuItem(this.controlPanel, 'ge_control_element', 'pause', (event) => { 66 | this.controls.playPause = new MenuItem( 67 | this.controlPanel, 68 | "ge_control_element", 69 | 'pause', 70 | (event) => { 71 | event.stopPropagation(); 72 | event.preventDefault(); 73 | if (glslcanvas.paused) { 74 | glslcanvas.play(); 75 | // this.controls.playPause.name = 'pause';//'Pause'; 76 | this.controls.playPause.name = 77 | 'pause'; //'Pause'; 78 | } else { 79 | glslcanvas.pause(); 80 | this.controls.playPause.name = 81 | 'play_arrow'; //'Play'; 82 | } 83 | } 84 | ); 85 | // rec 86 | this.isCapturing = false; 87 | // let rec = new MenuItem(this.controlPanel, 'ge_control_element', 'fiber_manual_record', (event) => { 88 | let rec = new MenuItem( 89 | this.controlPanel, 90 | "ge_control_element", 91 | '', 92 | (event) => { 93 | event.stopPropagation(); 94 | event.preventDefault(); 95 | if (this.isCapturing) { 96 | this.stopVideoCapture(); 97 | } else { 98 | this.startVideoCapture(); 99 | } 100 | } 101 | ); 102 | this.controls.rec = rec; 103 | this.controls.rec.button.style.color = "red"; 104 | 105 | // present mode (only if there is a presentation.html file to point to) 106 | // this.controls.presentationMode = new MenuItem(this.controlPanel, 'ge_control_element', 'open_in_new', (event) => { 107 | this.controls.presentationMode = new MenuItem( 108 | this.controlPanel, 109 | "ge_control_element", 110 | 'open_in_new', 111 | (event) => { 112 | event.stopPropagation(); 113 | event.preventDefault(); 114 | if (main.pWindowOpen) { 115 | main.togglePresentationWindow(false); 116 | } else { 117 | main.togglePresentationWindow(true); 118 | } 119 | } 120 | ); 121 | 122 | this.elControl = this.el.getElementsByClassName(CONTROLS_CLASSNAME)[0]; 123 | this.elControl.addEventListener("mouseenter", (event) => { 124 | this.showControls(); 125 | }); 126 | this.elControl.addEventListener("mouseleave", (event) => { 127 | this.hideControls(); 128 | }); 129 | this.elCanvas.addEventListener("mousemove", (event) => { 130 | if (event.offsetY > this.elCanvas.clientHeight * 0.66) { 131 | this.showControls(); 132 | } else { 133 | this.hideControls(); 134 | } 135 | }); 136 | this.hideControls(); 137 | } 138 | 139 | // ========== EVENTS 140 | // Draggable/resizable/snappable 141 | if ( 142 | main.options.canvas_draggable || 143 | main.options.canvas_resizable || 144 | main.options.canvas_snapable 145 | ) { 146 | subscribeInteractiveDom(this.el, { 147 | move: main.options.canvas_draggable, 148 | resize: main.options.canvas_resizable, 149 | snap: main.options.canvas_snapable, 150 | }); 151 | 152 | if (main.options.canvas_size === "halfscreen") { 153 | this.el.snapRight(); 154 | } 155 | 156 | this.el.on("move", (event) => { 157 | event.el.style.width = event.el.clientWidth + "px"; 158 | event.el.style.height = event.el.clientHeight + "px"; 159 | }); 160 | this.el.on("resize", (event) => { 161 | glslcanvas.canvas.style.width = event.el.clientWidth + "px"; 162 | glslcanvas.canvas.style.height = event.el.clientHeight + "px"; 163 | glslcanvas.resize(); 164 | }); 165 | } 166 | 167 | // If there is a menu offset the editor to come after it 168 | if (main.menu) { 169 | this.el.style.top = 170 | (main.menu.el.clientHeight || 171 | main.menu.el.offsetHeight || 172 | main.menu.el.scrollHeight) + "px"; 173 | } 174 | 175 | // Add all this to the main container 176 | main.container.appendChild(this.el); 177 | glslcanvas.resize(); 178 | } 179 | 180 | hideControls() { 181 | if (this.elControl && this.elControl.className === CONTROLS_CLASSNAME) { 182 | this.elControl.className = 183 | CONTROLS_CLASSNAME + " " + CONTROLS_CLASSNAME + "_hidden"; 184 | } 185 | } 186 | 187 | showControls() { 188 | if ( 189 | this.elControl && 190 | this.elControl.className === 191 | CONTROLS_CLASSNAME + " " + CONTROLS_CLASSNAME + "_hidden" 192 | ) { 193 | this.elControl.className = CONTROLS_CLASSNAME; 194 | } 195 | } 196 | 197 | requestRedraw() { 198 | this.canvas.forceRender = true; 199 | this.canvas.render(); 200 | } 201 | 202 | screenshot() { 203 | this.requestRedraw(); 204 | return this.mediaCapture.screenshot(); 205 | } 206 | 207 | startVideoCapture() { 208 | this.requestRedraw(); 209 | if (this.mediaCapture.startVideoCapture()) { 210 | this.isCapturing = true; 211 | this.controls.rec.button.style.color = "white"; 212 | // this.controls.rec.name = 'stop'; 213 | this.controls.rec.name = ''; 214 | } 215 | } 216 | 217 | stopVideoCapture() { 218 | if (this.isCapturing) { 219 | this.isCapturing = false; 220 | this.controls.rec.button.style.color = "red"; 221 | // this.controls.rec.name = 'fiber_manual_record'; 222 | this.controls.rec.name = 'stop'; 223 | this.mediaCapture.stopVideoCapture().then((video) => { 224 | saveAs(video.blob, `${Number(new Date())}.webm`); 225 | }); 226 | } 227 | } 228 | 229 | openWindow() { 230 | this.originalSize = { 231 | width: this.canvas.canvas.clientWidth, 232 | height: this.canvas.canvas.clientHeight, 233 | }; 234 | this.presentationWindow = window.open("", "_blank", "presentationWindow"); 235 | this.setUpPresentationWindow(); 236 | } 237 | 238 | closeWindow() { 239 | if (this.presentationWindow) { 240 | this.presentationWindow.close(); 241 | } 242 | } 243 | 244 | setCanvasSize(w, h) { 245 | this.canvas.canvas.style.width = w + "px"; 246 | this.canvas.canvas.style.height = h + "px"; 247 | } 248 | 249 | setUpPresentationWindow() { 250 | this.presentationWindow.document.body.appendChild(this.canvas.canvas); 251 | var d = this.presentationWindow.document; 252 | var div = d.createElement("div"); 253 | div.appendChild(d.createTextNode("Projector mode")); 254 | var span = this.presentationWindow.document.createElement("span"); 255 | div.appendChild(span); 256 | span.appendChild( 257 | d.createTextNode( 258 | " - If the canvas doesn't update, drag this window and reveal the editor" 259 | ) 260 | ); 261 | d.body.appendChild(div); 262 | 263 | d.title = "GLSL Editor"; 264 | d.body.style.padding = "0"; 265 | d.body.style.margin = "0"; 266 | d.body.style.background = "#171e22"; 267 | d.body.style.overflow = "hidden"; 268 | 269 | div.style.position = "absolute"; 270 | div.style.width = "100%"; 271 | div.style.background = "rgba(0, 0, 0, 0.5)"; 272 | div.style.position = "absolute"; 273 | div.style.top = "0"; 274 | div.style.left = "0"; 275 | div.style.right = "0"; 276 | div.style.padding = "16px"; 277 | div.style.color = "#ffffff"; 278 | div.style.fontSize = "14px"; 279 | div.style.fontFamily = "Helvetica, Geneva, sans-serif"; 280 | div.style.fontWeight = "400"; 281 | div.style.letterSpacing = "0.1em"; 282 | div.style.textAlign = "center"; 283 | div.style.opacity = "1"; 284 | div.style.zIndex = "9999"; 285 | div.style.setProperty("-webkit-transition", "opacity 1.5s"); 286 | div.style.setProperty("-moz-transition", "opacity 1.5s"); 287 | div.style.setProperty("transition", "opacity 1.5s"); 288 | 289 | span.style.color = "rgba(255, 255, 255, 0.5)"; 290 | 291 | setTimeout(() => { 292 | div.style.opacity = 0; 293 | }, 4000); 294 | 295 | this.setCanvasSize( 296 | this.presentationWindow.innerWidth, 297 | this.presentationWindow.innerHeight 298 | ); 299 | this.presentationWindow.addEventListener( 300 | "resize", 301 | this.onPresentationWindowResize.bind(this) 302 | ); 303 | this.presentationWindow.addEventListener( 304 | "unload", 305 | this.onPresentationWindowClose.bind(this) 306 | ); 307 | } 308 | 309 | onPresentationWindowClose() { 310 | this.el.appendChild(this.canvas.canvas); 311 | this.setCanvasSize(this.originalSize.width, this.originalSize.height); 312 | this.canvas.resize(); 313 | 314 | this.main.onClosePresentationWindow(); 315 | this.main.menu.onClosePresentationWindow(); 316 | this.presentationWindow = null; 317 | } 318 | 319 | onPresentationWindowResize() { 320 | if (this.presentationWindow) { 321 | this.setCanvasSize( 322 | this.presentationWindow.innerWidth, 323 | this.presentationWindow.innerHeight 324 | ); 325 | this.canvas.resize(); 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/js/io/BufferManager.js: -------------------------------------------------------------------------------- 1 | // Import CodeMirror 2 | import CodeMirror from 'codemirror'; 3 | import 'codemirror/mode/clike/clike.js'; 4 | 5 | export default class BufferManager { 6 | constructor (main) { 7 | this.main = main; 8 | this.buffers = {}; 9 | this.tabs = {}; 10 | this.current = 'untitled'; 11 | } 12 | 13 | open (name, content) { 14 | if (!this.el) { 15 | // Create DOM element 16 | this.el = document.createElement('ul'); 17 | this.el.className = 'ge_panel'; 18 | } 19 | 20 | if (this.main.change && this.current === 'untitled') { 21 | console.log('Open Current in a different tab'); 22 | this.open(this.current, this.main.getContent()); 23 | } 24 | 25 | this.buffers[name] = CodeMirror.Doc(content, 'x-shader/x-fragment'); 26 | 27 | // Create a new tab 28 | let tab = document.createElement('li'); 29 | tab.setAttribute('class', 'ge_panel_tab'); 30 | tab.textContent = name; 31 | CodeMirror.on(tab, 'click', () => { 32 | this.select(name); 33 | }); 34 | 35 | let close = tab.appendChild(document.createElement('a')); 36 | close.textContent = 'x'; 37 | close.setAttribute('class', 'ge_panel_tab_close'); 38 | CodeMirror.on(close, 'click', () => { 39 | this.close(name); 40 | }); 41 | 42 | this.el.appendChild(tab); 43 | this.tabs[name] = tab; 44 | 45 | if (this.el && !this.panel && this.getLength() > 1) { 46 | // Create Panel CM element 47 | this.panel = this.main.editor.addPanel(this.el, { position: 'top' }); 48 | } 49 | } 50 | 51 | select (name) { 52 | let buf = this.buffers[name]; 53 | 54 | if (buf === undefined) { 55 | return; 56 | } 57 | 58 | if (buf.getEditor()) { 59 | buf = buf.linkedDoc({ sharedHist: true }); 60 | } 61 | let old = this.main.editor.swapDoc(buf); 62 | let linked = old.iterLinkedDocs(function(doc) { 63 | linked = doc; 64 | }); 65 | if (linked) { 66 | // Make sure the document in buffers is the one the other view is looking at 67 | for (let bufferName in this.buffers) { 68 | if (this.buffers[bufferName] === old) { 69 | this.buffers[bufferName] = linked; 70 | } 71 | } 72 | old.unlinkDoc(linked); 73 | } 74 | if (this.main.options.autofocus) { 75 | this.main.editor.focus(); 76 | } 77 | this.main.setContent(this.main.getContent()); 78 | 79 | if (this.tabs[this.current]) { 80 | this.tabs[this.current].setAttribute('class', 'ge_panel_tab'); 81 | } 82 | this.tabs[name].setAttribute('class', 'ge_panel_tab_active'); 83 | this.current = name; 84 | 85 | this.main.editor.setSize(null, 'auto'); 86 | this.main.editor.getWrapperElement().style.height = 'auto'; 87 | 88 | this.main.trigger('new_content', {}); 89 | } 90 | 91 | close (name) { 92 | let needChange = name === this.getCurrent(); 93 | 94 | this.el.removeChild(this.tabs[name]); 95 | delete this.tabs[name]; 96 | delete this.buffers[name]; 97 | 98 | if (this.getLength() === 1) { 99 | this.panel.clear(); 100 | this.panel = undefined; 101 | this.el = undefined; 102 | } 103 | 104 | if (needChange) { 105 | for (let prop in this.tabs) { 106 | this.select(prop); 107 | break; 108 | } 109 | } 110 | } 111 | 112 | getCurrent () { 113 | return this.current; 114 | } 115 | 116 | getLength () { 117 | return Object.keys(this.buffers).length; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/js/io/FileDrop.js: -------------------------------------------------------------------------------- 1 | /* 2 | Original: https://github.com/tangrams/tangram-play/blob/gh-pages/src/js/addons/ui/FileDrop.js 3 | Author: Lou Huang (@saikofish) 4 | */ 5 | 6 | export default class FileDrop { 7 | constructor (main) { 8 | // Set up drag/drop file listeners 9 | main.container.addEventListener('dragenter', (event) => { 10 | // Check to make sure that dropped items are files. 11 | // This prevents other drags (e.g. text in editor) 12 | // from turning on the file drop area. 13 | // See here: http://stackoverflow.com/questions/6848043/how-do-i-detect-a-file-is-being-dragged-rather-than-a-draggable-element-on-my-pa 14 | // Tested in Chrome, Firefox, Safari 8 15 | var types = event.dataTransfer.types; 16 | if (types !== null && ((types.indexOf) ? (types.indexOf('Files') !== -1) : types.contains('application/x-moz-file'))) { 17 | event.preventDefault(); 18 | event.dataTransfer.dropEffect = 'copy'; 19 | } 20 | }, true); 21 | 22 | main.container.addEventListener('dragover', (event) => { 23 | // Required to prevent browser from navigating to a file 24 | // instead of receiving a data transfer 25 | event.preventDefault(); 26 | }, false); 27 | 28 | main.container.addEventListener('dragleave', (event) => { 29 | event.preventDefault(); 30 | }, true); 31 | 32 | main.container.addEventListener('drop', (event) => { 33 | event.preventDefault(); 34 | if (event.dataTransfer.files.length > 0) { 35 | const file = event.dataTransfer.files[0]; 36 | main.open(file); 37 | } 38 | }, false); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/js/io/HashWatch.js: -------------------------------------------------------------------------------- 1 | export default class HashWatch { 2 | constructor (main) { 3 | this.main = main; 4 | this.check(); 5 | 6 | window.addEventListener('hashchange', () => { 7 | this.check(); 8 | }, false); 9 | } 10 | 11 | check() { 12 | if (window.location.hash !== '') { 13 | this.main.options.imgs = []; 14 | 15 | let hashes = location.hash.split('&'); 16 | for (let i in hashes) { 17 | let ext = hashes[i].substr(hashes[i].lastIndexOf('.') + 1); 18 | let path = hashes[i]; 19 | 20 | // Extract hash if is present 21 | if (path.search('#') === 0) { 22 | path = path.substr(1); 23 | } 24 | 25 | let filename = path.split('/').pop(); 26 | 27 | if (ext === 'frag') { 28 | this.main.open(path, filename.replace(/\.[^/.]+$/, '')); 29 | } 30 | else if (ext === 'png' || ext === 'jpg' || ext === 'PNG' || ext === 'JPG') { 31 | this.main.options.imgs.push(path); 32 | } 33 | } 34 | } 35 | 36 | let query = parseQuery(window.location.search.slice(1)); 37 | if (query) { 38 | for (let key in query) { 39 | if (key === 'log') { 40 | if (this.main.bufferManager) { 41 | let logs = query.log.split(','); 42 | for (let i in logs) { 43 | this.main.open('https://thebookofshaders.com/log/' + logs[i] + '.frag', logs[i]); 44 | } 45 | } 46 | else { 47 | this.main.open('https://thebookofshaders.com/log/' + query.log + '.frag', query.log); 48 | } 49 | } 50 | else { 51 | let value = query[key]; 52 | if (value === 'true' || value === 'false') { 53 | value = (value == 'true'); 54 | } 55 | else if (parseFloat(value)) { 56 | value = parseFloat(value); 57 | } 58 | this.main.options[key] = value; 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | function parseQuery (qstr) { 66 | let query = {}; 67 | let a = qstr.split('&'); 68 | for (let i in a) { 69 | let b = a[i].split('='); 70 | query[decodeURIComponent(b[0])] = decodeURIComponent(b[1]); 71 | } 72 | return query; 73 | } 74 | -------------------------------------------------------------------------------- /src/js/io/LocalStorage.js: -------------------------------------------------------------------------------- 1 | /* 2 | Original: https://github.com/tangrams/tangram-play/blob/gh-pages/src/js/addons/LocalStorage.js 3 | Author: Lou Huang (@saikofish) 4 | */ 5 | 6 | /** 7 | * Local storage 8 | * 9 | * Provides a common interface for the application where modules can 10 | * request storage of values across multiple user sessions via the 11 | * browser's LocalStorage API. 12 | * 13 | * Browser support is good, so no fallbacks are implemented. 14 | * This module manages namespacing for Tangram Play to prevent name 15 | * collisions with other libraries, browser extensions, etc. 16 | */ 17 | const LOCAL_STORAGE_PREFIX = 'glslEditor-'; 18 | 19 | const LocalStorage = { 20 | /** 21 | * setItem() 22 | * Namespaces key name to Tangram Play application and adds 23 | * the value to LocalStorage. 24 | */ 25 | setItem (key, value) { 26 | if (window.localStorage) { 27 | window.localStorage.setItem(LOCAL_STORAGE_PREFIX + key, value); 28 | } 29 | }, 30 | 31 | /** 32 | * pushItem() 33 | * Store values as an array. If the key doesn't exist as an object, create it. 34 | * Note that this overwrites an old value if it is present and not a JSON object! 35 | * If it exists, retreive it, serialize it into JSON, push the new value, 36 | * re-encode to a string and then set it back in localStorage. 37 | * No other array methods are implemented. If you need to delete items, etc 38 | * then retrieve the string as normal, do the work in your script, and then 39 | * set it to the new stringified array instead of pushing it. 40 | */ 41 | pushItem (key, value) { 42 | let stored; 43 | stored = this.getItem(key); 44 | // In case there is a previously stored item here that is not 45 | // parseable JSON, don't fail 46 | try { 47 | stored = JSON.parse(stored); 48 | stored.arr = stored.arr || []; 49 | } 50 | catch (e) { 51 | stored = { arr: [] }; 52 | } 53 | stored.arr.push(value); 54 | this.setItem(key, JSON.stringify(stored)); 55 | }, 56 | 57 | /** 58 | * getItem() 59 | * Retrieves value for the given key name and application namespace. 60 | */ 61 | getItem (key) { 62 | if (window.localStorage) { 63 | return window.localStorage.getItem(LOCAL_STORAGE_PREFIX + key); 64 | } 65 | }, 66 | 67 | /** 68 | * removeItem() 69 | * Removes key-value pair under the application namespace. 70 | */ 71 | removeItem (key) { 72 | if (window.localStorage) { 73 | window.localStorage.removeItem(LOCAL_STORAGE_PREFIX + key); 74 | } 75 | }, 76 | 77 | /** 78 | * clear() 79 | * Loops through all values in localStorage under the application 80 | * namespace and removes them, preserving other key-value pairs in 81 | * localStorage. 82 | */ 83 | clear () { 84 | if (window.localStorage) { 85 | for (let key in window.localStorage) { 86 | if (key.indexOf(LOCAL_STORAGE_PREFIX) === 0) { 87 | window.localStorage.removeItem(LOCAL_STORAGE_PREFIX + key); 88 | } 89 | } 90 | } 91 | }, 92 | }; 93 | 94 | export default LocalStorage; 95 | -------------------------------------------------------------------------------- /src/js/io/Share.js: -------------------------------------------------------------------------------- 1 | var lastReplay; 2 | 3 | export function saveOnServer (ge, callback) { 4 | if (!ge.change && lastReplay) { 5 | callback(lastReplay); 6 | return; 7 | } 8 | 9 | let content = ge.getContent(); 10 | let name = ge.getAuthor(); 11 | let title = ge.getTitle(); 12 | 13 | if (name !== '' && title !== '') { 14 | name += '-' + title; 15 | } 16 | 17 | // STORE A COPY on SERVER 18 | let url = 'https://thebookofshaders.com:8080/'; 19 | // let url = 'http://localhost:8080/'; 20 | let data = new FormData(); 21 | data.append('code', content); 22 | 23 | let dataURL = ge.shader.elCanvas.toDataURL('image/png'); 24 | let blobBin = atob(dataURL.split(',')[1]); 25 | let array = []; 26 | for (let i = 0; i < blobBin.length; i++) { 27 | array.push(blobBin.charCodeAt(i)); 28 | } 29 | let file = new Blob([new Uint8Array(array)], { type: 'image/png' }); 30 | data.append('image', file); 31 | 32 | let xhr = new XMLHttpRequest(); 33 | xhr.open('POST', url + 'save', true); 34 | xhr.onload = (event) => { 35 | if (typeof callback === 'function') { 36 | let name = xhr.responseText; 37 | let replay = { 38 | content: content, 39 | name: name, 40 | url: url 41 | }; 42 | callback(replay); 43 | lastReplay = replay; 44 | } 45 | }; 46 | xhr.send(data); 47 | } 48 | 49 | export function createOpenFrameArtwork(glslEditor, name, url, callback) { 50 | const OF_BASE_API_URL = 'https://api.openframe.io/v0'; 51 | const OF_BASE_APP_URL = 'https://openframe.io'; 52 | // const OF_BASE_API_URL = 'http://localhost:8888/api'; // for local testing 53 | // const OF_BASE_APP_URL = 'http://localhost:8000'; // for local testing 54 | let title = glslEditor.getTitle(); 55 | let author = glslEditor.getAuthor(); 56 | glslEditor.getOfToken().then(initiateOfRequest); 57 | 58 | function initiateOfRequest(ofToken) { 59 | let xhr = new XMLHttpRequest(); 60 | if (typeof callback === 'undefined') { 61 | callback = () => {}; 62 | } 63 | // anywhere in the API that user {id} is needed, the alias 'current' can be used for the logged-in user 64 | xhr.open('POST', `${OF_BASE_API_URL}/users/current/created_artwork`); 65 | // set content type to JSON... 66 | xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); 67 | xhr.setRequestHeader('Authorization', ofToken); 68 | xhr.setRequestHeader('access_token', ofToken); 69 | 70 | // This is essential in order to include auth cookies: 71 | xhr.onload = (event) => { 72 | console.log('onload', event); 73 | if (event.currentTarget.status >= 400) { 74 | window.open(`${OF_BASE_APP_URL}/login`, 'login', 'width=500,height=600'); 75 | let successListener = function(e) { 76 | if (e.data === 'success') { 77 | createOpenFrameArtwork(glslEditor, name, url, callback); 78 | } 79 | window.removeEventListener('message', successListener); 80 | }; 81 | window.addEventListener('message', successListener, false); 82 | } 83 | else if (event.currentTarget.status === 200) { 84 | callback(true); 85 | } 86 | else { 87 | callback(false); 88 | } 89 | }; 90 | xhr.onerror = (event) => { 91 | console.log('Status:',event.currentTarget.status); 92 | }; 93 | /* Remote expects underscore keys */ 94 | /* eslint-disable camelcase */ 95 | xhr.send(JSON.stringify({ 96 | title: title, 97 | author_name: author, 98 | is_public: false, 99 | format: 'openframe-glslviewer', 100 | url: 'https://thebookofshaders.com/log/' + name + '.frag', 101 | thumb_url: 'https://thebookofshaders.com/log/' + name + '.png' 102 | /* eslint-enable camelcase */ 103 | })); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/js/tools/common.js: -------------------------------------------------------------------------------- 1 | export function getDomOrigin (el) { 2 | const box = (el.getBoundingClientRect) ? el.getBoundingClientRect() : { top: 0, left: 0 }; 3 | const doc = el && el.ownerDocument; 4 | const body = doc.body; 5 | const win = doc.defaultView || doc.parentWindow || window; 6 | const docElem = doc.documentElement || body.parentNode; 7 | const clientTop = docElem.clientTop || body.clientTop || 0; // border on html or body or both 8 | const clientLeft = docElem.clientLeft || body.clientLeft || 0; 9 | 10 | return { 11 | left: box.left + (win.pageXOffset || docElem.scrollLeft) - clientLeft, 12 | top: box.top + (win.pageYOffset || docElem.scrollTop) - clientTop 13 | }; 14 | } 15 | 16 | export function getDevicePixelRatio (ctx) { 17 | let devicePixelRatio = window.devicePixelRatio || 1; 18 | let backingStoreRatio = ctx.webkitBackingStorePixelRatio || 19 | ctx.mozBackingStorePixelRatio || 20 | ctx.msBackingStorePixelRatio || 21 | ctx.oBackingStorePixelRatio || 22 | ctx.backingStorePixelRatio || 1; 23 | return devicePixelRatio / backingStoreRatio; 24 | } 25 | 26 | export function getJSON(url, callback) { 27 | var xhr = new XMLHttpRequest(); 28 | xhr.open('GET', url, true); 29 | xhr.responseType = 'json'; 30 | xhr.onload = function() { 31 | var status = xhr.status; 32 | if (status === 200) { 33 | callback(null, xhr.response); 34 | } else { 35 | callback(status, xhr.response); 36 | } 37 | }; 38 | xhr.send(); 39 | }; 40 | -------------------------------------------------------------------------------- /src/js/tools/debugging.js: -------------------------------------------------------------------------------- 1 | 2 | export function isCommented(cm, nLine, match) { 3 | let token = cm.getTokenAt({ line: nLine, ch: match.index }); 4 | if (token && token.type) { 5 | return token.type === 'comment'; 6 | } 7 | return false; 8 | } 9 | 10 | export function isLineAfterMain(cm, nLine) { 11 | let totalLines = cm.getDoc().size; 12 | let voidRE = new RegExp('void main\\s*\\(\\s*[void]*\\)', 'i'); 13 | for (let i = 0; i < nLine && i < totalLines; i++) { 14 | // Do not start until being inside the main function 15 | let voidMatch = voidRE.exec(cm.getLine(i)); 16 | if (voidMatch) { 17 | return true; 18 | } 19 | } 20 | return false; 21 | } 22 | 23 | export function getVariableType(cm, sVariable) { 24 | let nLines = cm.getDoc().size; 25 | 26 | // Show line where the value of the variable is been asigned 27 | let uniformRE = new RegExp('\\s*uniform\\s+(float|vec2|vec3|vec4)\\s+' + sVariable + '\\s*;'); 28 | let voidRE = new RegExp('void main\\s*\\(\\s*[void]*\\)', 'i'); 29 | let voidIN = false; 30 | let constructRE = new RegExp('(float|vec\\d)\\s+(' + sVariable + ')\\s*[;]?', 'i'); 31 | for (let i = 0; i < nLines; i++) { 32 | if (!voidIN) { 33 | // Do not start until being inside the main function 34 | let voidMatch = voidRE.exec(cm.getLine(i)); 35 | if (voidMatch) { 36 | voidIN = true; 37 | } 38 | else { 39 | let uniformMatch = uniformRE.exec(cm.getLine(i)); 40 | if (uniformMatch && !isCommented(cm, i, uniformMatch)) { 41 | return uniformMatch[1]; 42 | } 43 | } 44 | } 45 | else { 46 | let constructMatch = constructRE.exec(cm.getLine(i)); 47 | if (constructMatch && constructMatch[1] && !isCommented(cm, i, constructMatch)) { 48 | return constructMatch[1]; 49 | } 50 | } 51 | } 52 | return 'none'; 53 | } 54 | 55 | export function getShaderForTypeVarInLine(cm, sType, sVariable, nLine) { 56 | let frag = ''; 57 | let offset = 1; 58 | for (let i = 0; i < nLine + 1 && i < cm.getDoc().size; i++) { 59 | if (cm.getLine(i)) { 60 | frag += cm.getLine(i) + '\n'; 61 | } 62 | } 63 | 64 | frag += '\tgl_FragColor = '; 65 | if (sType === 'float') { 66 | frag += 'vec4(vec3(' + sVariable + '),1.)'; 67 | } 68 | else if (sType === 'vec2') { 69 | frag += 'vec4(vec3(' + sVariable + ',0.),1.)'; 70 | } 71 | else if (sType === 'vec3') { 72 | frag += 'vec4(' + sVariable + ',1.)'; 73 | } 74 | else if (sType === 'vec4') { 75 | frag += sVariable; 76 | } 77 | frag += ';\n}\n'; 78 | 79 | return frag; 80 | } 81 | 82 | export function getResultRange(testResults) { 83 | let minMS = '10000000.0'; 84 | let minLine = 0; 85 | let maxMS = '0.0'; 86 | let maxLine = 0; 87 | for (let i in testResults) { 88 | if (testResults[i].ms < minMS) { 89 | minMS = testResults[i].ms; 90 | minLine = testResults[i].line; 91 | } 92 | if (testResults[i].ms > maxMS) { 93 | maxMS = testResults[i].ms; 94 | maxLine = testResults[i].line; 95 | } 96 | } 97 | return { min:{line: minLine, ms: minMS}, max:{line: maxLine, ms: maxMS} }; 98 | } 99 | 100 | export function getMedian(values) { 101 | values.sort(function(a,b) {return a - b;}); 102 | 103 | var half = Math.floor(values.length / 2); 104 | 105 | if(values.length % 2) {return values[half];} 106 | else {return (values[half - 1] + values[half]) / 2.0;} 107 | } 108 | 109 | export function getDeltaSum(testResults) { 110 | let total = 0.0; 111 | for (let i in testResults) { 112 | if (testResults[i].delta > 0) { 113 | total += testResults[i].delta; 114 | } 115 | } 116 | return total; 117 | } 118 | 119 | export function getHits(testResults) { 120 | let total = 0; 121 | for (let i in testResults) { 122 | if (testResults[i].delta > 0) { 123 | total++; 124 | } 125 | } 126 | return total; 127 | } 128 | -------------------------------------------------------------------------------- /src/js/tools/download.js: -------------------------------------------------------------------------------- 1 | export function saveAs(blob, filename) { 2 | const url = URL.createObjectURL(blob); 3 | const link = document.createElement("a"); 4 | link.setAttribute("download", filename); 5 | link.setAttribute("href", url); 6 | link.click(); 7 | } -------------------------------------------------------------------------------- /src/js/tools/interactiveDom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Original code from: https://twitter.com/blurspline / https://github.com/zz85 3 | * See post @ http://www.lab4games.net/zz85/blog/2014/11/15/resizing-moving-snapping-windows-with-js-css/ 4 | */ 5 | 6 | import { subscribeMixin } from './mixin'; 7 | 8 | // Thresholds 9 | var FULLSCREEN_MARGINS = -30; 10 | var MARGINS = 10; 11 | 12 | function setBounds(element, x, y, w, h) { 13 | element.style.left = x + 'px'; 14 | element.style.top = y + 'px'; 15 | element.style.width = w + 'px'; 16 | element.style.height = h + 'px'; 17 | } 18 | 19 | export function subscribeInteractiveDom (dom, options) { 20 | subscribeMixin(dom); 21 | 22 | options = options || {}; 23 | options.resize = options.resize !== undefined ? options.resize : false; 24 | options.move = options.move !== undefined ? options.move : false; 25 | options.snap = options.snap !== undefined ? options.snap : false; 26 | 27 | // Minimum resizable area 28 | var minWidth = 100; 29 | var minHeight = 100; 30 | 31 | // End of what's configurable. 32 | var clicked = null; 33 | var onRightEdge, onBottomEdge, onLeftEdge, onTopEdge; 34 | 35 | var rightScreenEdge, bottomScreenEdge; 36 | 37 | var preSnapped; 38 | 39 | var b, x, y; 40 | 41 | var redraw = false; 42 | 43 | var ghostdom = document.createElement('div'); 44 | ghostdom.className = 'ghostdom'; 45 | 46 | if (options.snap) { 47 | dom.parentElement.appendChild(ghostdom); 48 | } 49 | 50 | // Mouse events 51 | dom.addEventListener('mousedown', onMouseDown); 52 | document.addEventListener('mousemove', onMouseMove); 53 | document.addEventListener('mouseup', onMouseUp); 54 | 55 | // Touch events 56 | dom.addEventListener('touchstart', onTouchDown, { passive: false }); 57 | document.addEventListener('touchmove', onTouchMove, { passive: false }); 58 | document.addEventListener('touchend', onTouchEnd, { passive: false }); 59 | 60 | function hintHide() { 61 | setBounds(ghostdom, b.left, b.top, b.width, b.height); 62 | ghostdom.style.opacity = 0; 63 | } 64 | 65 | function onTouchDown (event) { 66 | event.preventDefault(); 67 | event.stopPropagation(); 68 | if (event.touches.length === 1) { 69 | onDown(event.changedTouches[0]); 70 | } 71 | } 72 | 73 | function onTouchMove (event) { 74 | event.preventDefault(); 75 | event.stopPropagation(); 76 | if (event.touches.length === 1) { 77 | onMove(event.changedTouches[0]); 78 | } 79 | } 80 | 81 | function onTouchEnd (event) { 82 | event.preventDefault(); 83 | event.stopPropagation(); 84 | if (event.touches.length === 0) { 85 | onUp(event.changedTouches[0]); 86 | } 87 | } 88 | 89 | function onMouseDown (event) { 90 | event.preventDefault(); 91 | event.stopPropagation(); 92 | onDown(event); 93 | } 94 | 95 | function onMouseMove (event) { 96 | event.preventDefault(); 97 | event.stopPropagation(); 98 | onMove(event); 99 | } 100 | 101 | function onMouseUp (event) { 102 | event.preventDefault(); 103 | event.stopPropagation(); 104 | onUp(event); 105 | } 106 | 107 | function onDown (event) { 108 | calc(event); 109 | var isResizing = options.resize && (onRightEdge || onBottomEdge || onTopEdge || onLeftEdge); 110 | clicked = { 111 | x: x, 112 | y: y, 113 | cx: event.clientX, 114 | cy: event.clientY, 115 | w: b.width, 116 | h: b.height, 117 | isResizing: isResizing, 118 | isMoving: !isResizing && canMove(), 119 | onTopEdge: onTopEdge, 120 | onLeftEdge: onLeftEdge, 121 | onRightEdge: onRightEdge, 122 | onBottomEdge: onBottomEdge 123 | }; 124 | } 125 | 126 | function canMove() { 127 | return options.move && (x > 0 && x < b.width && y > 0 && y < b.height);// && y < 30; 128 | } 129 | 130 | function calc (event) { 131 | b = dom.getBoundingClientRect(); 132 | x = event.clientX - b.left; 133 | y = event.clientY - b.top; 134 | 135 | onTopEdge = y < MARGINS; 136 | onLeftEdge = x < MARGINS; 137 | onRightEdge = x >= b.width - MARGINS; 138 | onBottomEdge = y >= b.height - MARGINS; 139 | 140 | rightScreenEdge = window.innerWidth - MARGINS; 141 | bottomScreenEdge = window.innerHeight - MARGINS; 142 | } 143 | 144 | var e; 145 | 146 | function onMove(event) { 147 | calc(event); 148 | e = event; 149 | redraw = true; 150 | } 151 | 152 | function animate() { 153 | requestAnimationFrame(animate); 154 | 155 | if (!redraw) { 156 | return; 157 | } 158 | redraw = false; 159 | 160 | if (clicked && clicked.isResizing) { 161 | if (clicked.onRightEdge) { 162 | dom.style.width = Math.max(x, minWidth) + 'px'; 163 | } 164 | if (clicked.onBottomEdge) { 165 | dom.style.height = Math.max(y, minHeight) + 'px'; 166 | } 167 | 168 | if (clicked.onLeftEdge) { 169 | var currentWidth = Math.max(clicked.cx - e.clientX + clicked.w, minWidth); 170 | if (currentWidth > minWidth) { 171 | dom.style.width = currentWidth + 'px'; 172 | dom.style.removeProperty('right'); 173 | dom.style.left = e.clientX + 'px'; 174 | } 175 | } 176 | 177 | if (clicked.onTopEdge) { 178 | var currentHeight = Math.max(clicked.cy - e.clientY + clicked.h, minHeight); 179 | if (currentHeight > minHeight) { 180 | dom.style.height = currentHeight + 'px'; 181 | dom.style.removeProperty('bottom'); 182 | dom.style.top = e.clientY + 'px'; 183 | } 184 | } 185 | 186 | hintHide(); 187 | dom.trigger('resize', { finish: false, el: dom }); 188 | return; 189 | } 190 | 191 | if (clicked && clicked.isMoving) { 192 | if (options.snap) { 193 | if (b.top < FULLSCREEN_MARGINS || b.left < FULLSCREEN_MARGINS || b.right > window.innerWidth - FULLSCREEN_MARGINS || b.bottom > window.innerHeight - FULLSCREEN_MARGINS) { 194 | setBounds(ghostdom, 0, 0, window.innerWidth, window.innerHeight); 195 | ghostdom.style.opacity = 0.2; 196 | } 197 | else if (b.top < MARGINS) { 198 | setBounds(ghostdom, 0, 0, window.innerWidth, window.innerHeight / 2); 199 | ghostdom.style.opacity = 0.2; 200 | } 201 | else if (b.left < MARGINS) { 202 | setBounds(ghostdom, 0, 0, window.innerWidth / 2, window.innerHeight); 203 | ghostdom.style.opacity = 0.2; 204 | } 205 | else if (b.right > rightScreenEdge) { 206 | setBounds(ghostdom, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight); 207 | ghostdom.style.opacity = 0.2; 208 | } 209 | else if (b.bottom > bottomScreenEdge) { 210 | setBounds(ghostdom, 0, window.innerHeight / 2, window.innerWidth, window.innerWidth / 2); 211 | ghostdom.style.opacity = 0.2; 212 | } 213 | else { 214 | hintHide(); 215 | } 216 | 217 | if (preSnapped) { 218 | setBounds(dom, 219 | e.clientX - preSnapped.width / 2, 220 | e.clientY - Math.min(clicked.y, preSnapped.height), 221 | preSnapped.width, 222 | preSnapped.height); 223 | return; 224 | } 225 | 226 | // moving 227 | dom.style.removeProperty('right'); 228 | dom.style.removeProperty('bottom'); 229 | dom.style.top = (e.clientY - clicked.y) + 'px'; 230 | dom.style.left = (e.clientX - clicked.x) + 'px'; 231 | } 232 | else { 233 | let x = (e.clientX - clicked.x); 234 | let y = (e.clientY - clicked.y); 235 | 236 | if (x < 0) { 237 | x = 0; 238 | } 239 | else if (y < 0) { 240 | y = 0; 241 | } 242 | else if (x + dom.offsetWidth > window.innerWidth) { 243 | x = window.innerWidth - dom.offsetWidth; 244 | } 245 | else if (y + dom.offsetHeight > window.innerHeight) { 246 | y = window.innerHeight - dom.offsetHeight; 247 | } 248 | 249 | dom.style.removeProperty('right'); 250 | dom.style.removeProperty('bottom'); 251 | dom.style.left = x + 'px'; 252 | dom.style.top = y + 'px'; 253 | } 254 | 255 | dom.trigger('move', { finish: false, el: dom }); 256 | return; 257 | } 258 | // This code executes when mouse moves without clicking 259 | 260 | // style cursor 261 | if (options.resize && (onRightEdge && onBottomEdge || onLeftEdge && onTopEdge)) { 262 | dom.style.cursor = 'nwse-resize'; 263 | } 264 | else if (options.resize && (onRightEdge && onTopEdge || onBottomEdge && onLeftEdge)) { 265 | dom.style.cursor = 'nesw-resize'; 266 | } 267 | else if (options.resize && (onRightEdge || onLeftEdge)) { 268 | dom.style.cursor = 'ew-resize'; 269 | } 270 | else if (options.resize && (onBottomEdge || onTopEdge)) { 271 | dom.style.cursor = 'ns-resize'; 272 | } 273 | else if (canMove()) { 274 | dom.style.cursor = 'move'; 275 | } 276 | else { 277 | dom.style.cursor = 'default'; 278 | } 279 | } 280 | animate(); 281 | 282 | function onUp(e) { 283 | calc(e); 284 | 285 | if (clicked && clicked.isResizing) { 286 | dom.trigger('resize', { finish: true, el: dom }); 287 | } 288 | 289 | if (options.snap && clicked && clicked.isMoving) { 290 | // Snap 291 | var snapped = { 292 | width: b.width, 293 | height: b.height 294 | }; 295 | 296 | if (b.top < FULLSCREEN_MARGINS || b.left < FULLSCREEN_MARGINS || b.right > window.innerWidth - FULLSCREEN_MARGINS || b.bottom > window.innerHeight - FULLSCREEN_MARGINS) { 297 | setBounds(dom, 0, 0, window.innerWidth, window.innerHeight); 298 | preSnapped = snapped; 299 | } 300 | else if (b.top < MARGINS) { 301 | setBounds(dom, 0, 0, window.innerWidth, window.innerHeight / 2); 302 | preSnapped = snapped; 303 | } 304 | else if (b.left < MARGINS) { 305 | setBounds(dom, 0, 0, window.innerWidth / 2, window.innerHeight); 306 | preSnapped = snapped; 307 | } 308 | else if (b.right > rightScreenEdge) { 309 | setBounds(dom, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight); 310 | preSnapped = snapped; 311 | } 312 | else if (b.bottom > bottomScreenEdge) { 313 | setBounds(dom, 0, window.innerHeight / 2, window.innerWidth, window.innerWidth / 2); 314 | preSnapped = snapped; 315 | } 316 | else { 317 | preSnapped = null; 318 | } 319 | hintHide(); 320 | dom.trigger('move', { finish: true, el: dom }); 321 | dom.trigger('resize', { finish: true, el: dom }); 322 | } 323 | clicked = null; 324 | } 325 | 326 | dom.snapRight = function () { 327 | var snapped = { 328 | width: dom.width, 329 | height: dom.height 330 | }; 331 | 332 | setBounds(dom, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight); 333 | preSnapped = snapped; 334 | // hintHide(); 335 | dom.trigger('move', { finish: true, el: dom }); 336 | dom.trigger('resize', { finish: true, el: dom }); 337 | }; 338 | 339 | return dom; 340 | } 341 | -------------------------------------------------------------------------------- /src/js/tools/mediaCapture.js: -------------------------------------------------------------------------------- 1 | /* global MediaRecorder 2 | Author: Brett Camper (@professorlemeza) 3 | URL: https://github.com/tangrams/tangram/blob/master/src/utils/media_capture.js 4 | */ 5 | import {createObjectURL} from './urls'; 6 | 7 | export default class MediaCapture { 8 | constructor() { 9 | this.queueScreenshot = null; 10 | this.videoCapture = null; 11 | } 12 | 13 | setCanvas (canvas) { 14 | this.canvas = canvas; 15 | } 16 | 17 | // Take a screenshot, returns a promise that resolves with the screenshot data when available 18 | screenshot () { 19 | if (this.queueScreenshot != null) { 20 | return this.queueScreenshot.promise; // only capture one screenshot at a time 21 | } 22 | 23 | // Will resolve once rendering is complete and render buffer is captured 24 | this.queueScreenshot = {}; 25 | this.queueScreenshot.promise = new Promise((resolve, reject) => { 26 | this.queueScreenshot.resolve = resolve; 27 | this.queueScreenshot.reject = reject; 28 | }); 29 | return this.queueScreenshot.promise; 30 | } 31 | 32 | // Called after rendering, captures render buffer and resolves promise with the image data 33 | completeScreenshot () { 34 | if (this.queueScreenshot != null) { 35 | // Get data URL, convert to blob 36 | // Strip host/mimetype/etc., convert base64 to binary without UTF-8 mangling 37 | // Adapted from: https://gist.github.com/unconed/4370822 38 | const url = this.canvas.toDataURL('image/png'); 39 | const data = atob(url.slice(22)); 40 | const buffer = new Uint8Array(data.length); 41 | for (let i = 0; i < data.length; ++i) { 42 | buffer[i] = data.charCodeAt(i); 43 | } 44 | const blob = new Blob([buffer], { type: 'image/png' }); 45 | 46 | // Resolve with screenshot data 47 | this.queueScreenshot.resolve({ url, blob, type: 'png' }); 48 | this.queueScreenshot = null; 49 | } 50 | } 51 | 52 | // Starts capturing a video stream from the canvas 53 | startVideoCapture () { 54 | if (typeof window.MediaRecorder !== 'function' || !this.canvas || typeof this.canvas.captureStream !== 'function') { 55 | console.log('warn: Video capture (Canvas.captureStream and/or MediaRecorder APIs) not supported by browser'); 56 | return false; 57 | } 58 | else if (this.videoCapture) { 59 | console.log('warn: Video capture already in progress, call Scene.stopVideoCapture() first'); 60 | return false; 61 | } 62 | 63 | // Start a new capture 64 | try { 65 | let cap = this.videoCapture = {}; 66 | cap.chunks = []; 67 | cap.stream = this.canvas.captureStream(); 68 | cap.options = { mimeType: 'video/webm' }; // TODO: support other format options 69 | cap.mediaRecorder = new MediaRecorder(cap.stream, cap.options); 70 | cap.mediaRecorder.ondataavailable = (event) => { 71 | if (event.data.size > 0) { 72 | cap.chunks.push(event.data); 73 | } 74 | 75 | // Stopped recording? Create the final capture file blob 76 | if (cap.resolve) { 77 | let blob = new Blob(cap.chunks, { type: cap.options.mimeType }); 78 | let url = createObjectURL(blob); 79 | 80 | // Explicitly remove all stream tracks, and set objects to null 81 | if (cap.stream) { 82 | let tracks = cap.stream.getTracks() || []; 83 | tracks.forEach(track => { 84 | track.stop(); 85 | cap.stream.removeTrack(track); 86 | }); 87 | } 88 | cap.stream = null; 89 | cap.mediaRecorder = null; 90 | this.videoCapture = null; 91 | 92 | cap.resolve({ url, blob, type: 'webm' }); 93 | } 94 | }; 95 | cap.mediaRecorder.start(); 96 | } 97 | catch (e) { 98 | this.videoCapture = null; 99 | console.log('error: Scene video capture failed', e); 100 | return false; 101 | } 102 | return true; 103 | } 104 | 105 | // Stops capturing a video stream from the canvas, returns a promise that resolves with the video when available 106 | stopVideoCapture () { 107 | if (!this.videoCapture) { 108 | console.log('warn: No scene video capture in progress, call Scene.startVideoCapture() first'); 109 | return Promise.resolve({}); 110 | } 111 | 112 | // Promise that will resolve when final stream is available 113 | this.videoCapture.promise = new Promise((resolve, reject) => { 114 | this.videoCapture.resolve = resolve; 115 | this.videoCapture.reject = reject; 116 | }); 117 | 118 | // Stop recording 119 | this.videoCapture.mediaRecorder.stop(); 120 | 121 | return this.videoCapture.promise; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/js/tools/mixin.js: -------------------------------------------------------------------------------- 1 | /* 2 | Add events to a class or object: 3 | class MyClass { 4 | constructor() { 5 | subscribeMixin(this); // Add the mixing functions to the class 6 | ... 7 | this.trigger('something', { owner: this, content: 'that'}); // trigger an event passing some arguments 8 | 9 | Subscribe to events by doing: 10 | myClass.on('something', (args) => { 11 | console.log(args); 12 | }); 13 | 14 | Unsubscribe to events by doing: 15 | myClass.off('something'); 16 | 17 | or more presicelly: 18 | myClass.off('something', (args) => { 19 | console.log(args); 20 | }); 21 | 22 | Unsubscribe to all events by: 23 | myClass.offAll(); 24 | */ 25 | 26 | export function subscribeMixin (target) { 27 | var listeners = new Set(); 28 | 29 | return Object.assign(target, { 30 | 31 | on (type, f) { 32 | let listener = {}; 33 | listener[type] = f; 34 | listeners.add(listener); 35 | }, 36 | 37 | off (type, f) { 38 | if (f) { 39 | let listener = {}; 40 | listener[type] = f; 41 | listeners.delete(listener); 42 | } 43 | else { 44 | for (let item of listeners) { 45 | for (let key of Object.keys(item)) { 46 | if (key === type) { 47 | listeners.delete(item); 48 | return; 49 | } 50 | } 51 | } 52 | } 53 | }, 54 | 55 | offAll () { 56 | listeners.clear(); 57 | }, 58 | 59 | trigger (event, ...data) { 60 | for (var listener of listeners) { 61 | if (typeof listener[event] === 'function') { 62 | listener[event](...data); 63 | } 64 | } 65 | }, 66 | 67 | listSubscriptions () { 68 | for (let item of listeners) { 69 | console.log(item); 70 | } 71 | } 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/js/tools/urls.js: -------------------------------------------------------------------------------- 1 | let _createObjectURL; 2 | export function createObjectURL (url) { 3 | if (_createObjectURL === undefined) { 4 | _createObjectURL = (window.URL && window.URL.createObjectURL) || (window.webkitURL && window.webkitURL.createObjectURL); 5 | if (typeof _createObjectURL !== 'function') { 6 | _createObjectURL = null; 7 | console.log('window.URL.createObjectURL (or vendor prefix) not found, unable to create local blob URLs'); 8 | } 9 | } 10 | 11 | if (_createObjectURL) { 12 | return _createObjectURL(url); 13 | } 14 | else { 15 | return url; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/js/ui/ErrorsDisplay.js: -------------------------------------------------------------------------------- 1 | export default class ErrorsDisplay { 2 | constructor(main) { 3 | this.main = main; 4 | 5 | // private variables 6 | this.widgets = []; 7 | 8 | // EVENTS 9 | this.main.shader.canvas.on('error', (arg) => { 10 | if (this.main.visualDebugger && this.main.visualDebugger.testing) { 11 | this.clean(); 12 | } 13 | else { 14 | this.clean(); 15 | this.addError(arg); 16 | } 17 | }); 18 | 19 | this.main.editor.on('changes', (cm, changesObjs) => { 20 | if (this.main.shader.canvas.isValid) { 21 | this.clean(); 22 | } 23 | }); 24 | } 25 | 26 | clean() { 27 | for (let i = 0; i < this.widgets.length; i++) { 28 | this.main.editor.removeLineWidget(this.widgets[i]); 29 | } 30 | this.widgets.length = 0; 31 | } 32 | 33 | addError(args) { 34 | let re = /ERROR:\s+\d+:(\d+):\s+('.*)/g; 35 | let matches = re.exec(args.error); 36 | if (matches) { 37 | let numLines = 0; 38 | if (this.main.options.frag_header.length > 0) 39 | numLines += (this.main.options.frag_header.match(/\r?\n/g) || '').length; 40 | 41 | let line = parseInt(matches[1]) - numLines; 42 | let er = matches[2]; 43 | let msg = document.createElement('div'); 44 | 45 | let icon = msg.appendChild(document.createElement('span')); 46 | icon.className = 'ge-error-icon'; 47 | icon.innerHTML = 'x'; 48 | msg.appendChild(document.createTextNode(er)); 49 | msg.className = 'ge-error'; 50 | this.widgets.push(this.main.editor.addLineWidget(line, msg));//, { coverGutter: false, noHScroll: true })); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/js/ui/ExportIcon.js: -------------------------------------------------------------------------------- 1 | import ExportModal from './modals/ExportModal'; 2 | 3 | export default class ExportIcon { 4 | constructor (main) { 5 | this.main = main; 6 | 7 | this.el = document.createElement('div'); 8 | this.el.setAttribute('class', 'ge_export_icon'); 9 | this.el.innerHTML = '△'; 10 | // this.el.innerHTML = 'more_vert'; 11 | this.el.addEventListener('click', (event) => { 12 | if (main.change || !this.modal) { 13 | this.modal = new ExportModal('ge_export', { main: main }); 14 | } 15 | this.modal.presentModal(event.target.offsetLeft, event.target.offsetTop); 16 | }, true); 17 | 18 | this.main.container.appendChild(this.el); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/js/ui/Helpers.js: -------------------------------------------------------------------------------- 1 | import ColorPicker from './pickers/ColorPicker'; 2 | import Vec3Picker from './pickers/Vec3Picker'; 3 | import Vec2Picker from './pickers/Vec2Picker'; 4 | import FloatPicker from './pickers/FloatPicker'; 5 | 6 | import Color from './pickers/types/Color'; 7 | 8 | import Modal from './modals/Modal'; 9 | 10 | // Return all pattern matches with captured groups 11 | RegExp.prototype.execAll = function(string) { 12 | let match = null; 13 | let matches = []; 14 | while (match = this.exec(string)) { 15 | let matchArray = []; 16 | for (let i in match) { 17 | if (parseInt(i) == i) { 18 | matchArray.push(match[i]); 19 | } 20 | } 21 | matchArray.index = match.index; 22 | matches.push(matchArray); 23 | } 24 | return matches; 25 | }; 26 | 27 | export default class Helpers { 28 | constructor (main) { 29 | this.main = main; 30 | this.main.debugging = false; 31 | 32 | let style = window.getComputedStyle(main.editor.getWrapperElement(), null); 33 | let bgColor = new Color(style.background !== '' ? style.background : style.backgroundColor); 34 | let fgColor = new Color(style.color); 35 | 36 | this.properties = { 37 | bgColor: bgColor.getString('rgb'), 38 | fnColor: fgColor.getString('rgb'), 39 | dimColor: 'rgb(127, 127, 127)', 40 | selColor: 'rgb(40, 168, 107)', 41 | linkButton: true 42 | }; 43 | 44 | // EVENTS 45 | let wrapper = this.main.editor.getWrapperElement(); 46 | wrapper.addEventListener('contextmenu', (event) => { 47 | let cursor = this.main.editor.getCursor(true); 48 | let token = this.main.editor.getTokenAt(cursor); 49 | if (token.type === 'variable') { 50 | this.main.visualDebugger.debug(token.string, cursor.line); 51 | } 52 | else { 53 | this.main.update(); 54 | } 55 | }); 56 | 57 | wrapper.addEventListener('mouseup', (event) => { 58 | // bail out if we were doing a selection and not a click 59 | if (this.main.editor.somethingSelected()) { 60 | return; 61 | } 62 | 63 | let cursor = this.main.editor.getCursor(true); 64 | 65 | // see if there is a match on the cursor click 66 | let match = this.getMatch(cursor); 67 | let token = this.main.editor.getTokenAt(cursor); 68 | if (match) { 69 | this.main.visualDebugger.clean(event); 70 | this.main.update(); 71 | 72 | // Toggles the trackpad to be off if it's already present. 73 | if (this.activeModal && this.activeModal.isVisible) { 74 | this.activeModal.removeModal(); 75 | return; 76 | } 77 | 78 | if (match.type === 'color') { 79 | this.activeModal = new ColorPicker(match.string, this.properties); 80 | this.activeModal.showAt(this.main.editor); 81 | this.activeModal.on('changed', (color) => { 82 | let newColor = color.getString('vec'); 83 | let start = { line: cursor.line, ch: match.start }; 84 | let end = { line: cursor.line, ch: match.end }; 85 | match.end = match.start + newColor.length; 86 | this.main.editor.replaceRange(newColor, start, end, `+${match.type}`); 87 | }); 88 | 89 | this.activeModal.on('linkButton', (color) => { 90 | this.activeModal = new Vec3Picker(color.getString('vec'), this.properties); 91 | this.activeModal.showAt(this.main.editor); 92 | this.activeModal.on('changed', (dir) => { 93 | let newDir = dir.getString('vec3'); 94 | let start = { line: cursor.line, ch: match.start }; 95 | let end = { line: cursor.line, ch: match.end }; 96 | match.end = match.start + newDir.length; 97 | this.main.editor.replaceRange(newDir, start, end, `+${match.type}`); 98 | }); 99 | }); 100 | } 101 | if (match.type === 'vec3') { 102 | this.activeModal = new Vec3Picker(match.string, this.properties); 103 | this.activeModal.showAt(this.main.editor); 104 | this.activeModal.on('changed', (dir) => { 105 | let newDir = dir.getString('vec3'); 106 | let start = { line: cursor.line, ch: match.start }; 107 | let end = { line: cursor.line, ch: match.end }; 108 | match.end = match.start + newDir.length; 109 | this.main.editor.replaceRange(newDir, start, end, `+${match.type}`); 110 | }); 111 | } 112 | else if (match.type === 'vec2') { 113 | this.activeModal = new Vec2Picker(match.string, this.properties); 114 | this.activeModal.showAt(this.main.editor); 115 | this.activeModal.on('changed', (pos) => { 116 | let newpos = pos.getString(); 117 | let start = { line: cursor.line, ch: match.start }; 118 | let end = { line: cursor.line, ch: match.end }; 119 | match.end = match.start + newpos.length; 120 | this.main.editor.replaceRange(newpos, start, end, `+${match.type}`); 121 | }); 122 | } 123 | else if (match.type === 'number') { 124 | this.activeModal = new FloatPicker(match.string, this.properties); 125 | this.activeModal.showAt(this.main.editor); 126 | this.activeModal.on('changed', (number) => { 127 | let newNumber = number.getString(); 128 | let start = { line: cursor.line, ch: match.start }; 129 | let end = { line: cursor.line, ch: match.end }; 130 | match.end = match.start + newNumber.length; 131 | this.main.editor.replaceRange(newNumber, start, end, `+${match.type}`); 132 | }); 133 | } 134 | } 135 | else if (this.main.options.tooltips && (token.type === 'builtin' || token.type === 'variable-3')) { 136 | this.main.visualDebugger.clean(event); 137 | let html = '

Learn more about: ' + token.string + '

'; 138 | this.activeModal = new Modal('ge_tooltip', { innerHTML: html }); 139 | this.activeModal.showAt(this.main.editor); 140 | } 141 | else if (token.type === 'variable') { 142 | if (this.main.visualDebugger) { 143 | this.main.visualDebugger.clean(event); 144 | this.main.visualDebugger.iluminate(token.string); 145 | } 146 | } 147 | }); 148 | } 149 | 150 | getMatch (cursor) { 151 | let types = ['color', 'vec3' ,'vec2', 'number']; 152 | let rta; 153 | for (let i in types) { 154 | rta = this.getTypeMatch(cursor, types[i]); 155 | if (rta) { 156 | return rta; 157 | } 158 | } 159 | return; 160 | } 161 | 162 | getTypeMatch (cursor, type) { 163 | if (!type) { 164 | return; 165 | } 166 | let re; 167 | switch(type.toLowerCase()) { 168 | case 'color': 169 | re = /vec[3|4]\([\d|.|,\s]*\)/g; 170 | break; 171 | case 'vec3': 172 | re = /vec3\([-|\d|.|,\s]*\)/g; 173 | break; 174 | case 'vec2': 175 | re = /vec2\([-|\d|.|,\s]*\)/g; 176 | break; 177 | case 'number': 178 | re = /[-]?\d+\.\d+|\d+\.|\.\d+/g; 179 | break; 180 | default: 181 | console.error('invalid match selection'); 182 | return; 183 | } 184 | let line = this.main.editor.getLine(cursor.line); 185 | let matches = re.execAll(line); 186 | 187 | if (matches) { 188 | for (let i = 0; i < matches.length; i++) { 189 | let val = matches[i][0]; 190 | let len = val.length; 191 | let start = matches[i].index; 192 | let end = matches[i].index + len; 193 | if (cursor.ch >= start && cursor.ch <= end) { 194 | return { 195 | type: type, 196 | start: start, 197 | end: end, 198 | string: val 199 | }; 200 | } 201 | } 202 | } 203 | return; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/js/ui/Menu.js: -------------------------------------------------------------------------------- 1 | import MenuItem from './MenuItem'; 2 | import ExportModal from './modals/ExportModal'; 3 | 4 | export default class Menu { 5 | constructor (main) { 6 | this.main = main; 7 | this.menus = {}; 8 | 9 | // CREATE MENU Container 10 | this.el = document.createElement('ul'); 11 | this.el.setAttribute('class', 'ge_menu_bar'); 12 | 13 | // NEW 14 | this.menus.new = new MenuItem(this.el, 'ge_menu', 'add New', (event) => { 15 | main.new(); 16 | }); 17 | 18 | // OPEN 19 | this.fileInput = document.createElement('input'); 20 | this.fileInput.setAttribute('type', 'file'); 21 | this.fileInput.setAttribute('accept', 'text/x-yaml'); 22 | this.fileInput.style.display = 'none'; 23 | this.fileInput.addEventListener('change', (event) => { 24 | main.open(event.target.files[0]); 25 | }); 26 | this.menus.open = new MenuItem(this.el, 'ge_menu', 'folder_open Open', (event) => { 27 | this.fileInput.click(); 28 | }); 29 | 30 | // this.menus.autoupdate.button.style.color = main.autoupdate ? 'white' : 'gray'; 31 | 32 | // TEST 33 | this.menus.test = new MenuItem(this.el, 'ge_menu', 'timeline Test', (event) => { 34 | main.visualDebugger.check(); 35 | }); 36 | 37 | // SHARE 38 | this.menus.share = new MenuItem(this.el, 'ge_menu', 'arrow_upward Export', (event) => { 39 | if (main.change || !this.exportModal) { 40 | this.exportModal = new ExportModal('ge_export', { main: main, position: 'fixed' }); 41 | } 42 | 43 | let bbox = this.menus.share.el.getBoundingClientRect(); 44 | this.exportModal.presentModal(bbox.left - 5, bbox.top + bbox.height + 5); 45 | }); 46 | 47 | 48 | // AUTOUPDATE 49 | this.menus.autoupdate = new MenuItem(this.el, 'ge_menu', ' autorenew Update: ON', (event) => { 50 | if (main.autoupdate) { 51 | main.autoupdate = false; 52 | this.menus.autoupdate.name = 'autorenew Update: OFF'; 53 | // this.menus.autoupdate.button.style.color = 'gray'; 54 | } 55 | else { 56 | main.autoupdate = true; 57 | main.update(); 58 | this.menus.autoupdate.name = 'autorenew Update: ON'; 59 | // this.menus.autoupdate.button.style.color = 'white'; 60 | } 61 | }); 62 | 63 | main.container.appendChild(this.el); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/js/ui/MenuItem.js: -------------------------------------------------------------------------------- 1 | export default class MenuItem { 2 | constructor (container, className, name, onClick) { 3 | this.el = document.createElement('li'); 4 | this.button = document.createElement('button'); 5 | this.button.className = className + '_button'; 6 | this.el.appendChild(this.button); 7 | this.el.setAttribute('class', className); 8 | this.button.innerHTML = name; 9 | this.className = className; 10 | this.hiddenClass = className + '--hidden'; 11 | 12 | // Attach listeners, including those for tooltip behavior 13 | this.button.addEventListener('click', onClick, true); 14 | 15 | if (container) { 16 | container.appendChild(this.el); 17 | } 18 | } 19 | 20 | set name (name) { 21 | this.button.innerHTML = name; 22 | } 23 | 24 | hide () { 25 | this.el.setAttribute('class', this.className + ' ' + this.hiddenClass); 26 | } 27 | 28 | show () { 29 | this.el.setAttribute('class', this.className); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/js/ui/VisualDebugger.js: -------------------------------------------------------------------------------- 1 | import { isCommented, isLineAfterMain, getVariableType, getShaderForTypeVarInLine, getResultRange, getDeltaSum, getHits, getMedian } from '../tools/debugging'; 2 | import { unfocusLine, focusLine, unfocusAll, focusAll } from '../core/Editor.js'; 3 | 4 | var mainGE = {}; 5 | var N_SAMPLES = 30; 6 | 7 | export default class VisualDebugger { 8 | constructor (main) { 9 | this.main = main; 10 | this.breakpoint = null; 11 | mainGE = main; 12 | 13 | this.testing = false; 14 | this.testingFrag = ''; 15 | this.testingLine = 0; 16 | this.testingResults = []; 17 | this.testingSamples = []; 18 | 19 | this.main.editor.on('gutterClick', (cm, n) => { 20 | let info = cm.lineInfo(n); 21 | if (info && info.gutterMarkers && info.gutterMarkers.breakpoints) { 22 | // Check for an active variable (a variable that have been declare or modify in this line) 23 | let variableRE = new RegExp('\\s*[float|vec2|vec3|vec4]?\\s+([\\w|\\_]*)[\\.\\w]*?\\s+[\\+|\\-|\\\\|\\*]?\\=', 'i'); 24 | let match = variableRE.exec(info.text); 25 | if (match) { 26 | this.debug(match[1], info.line); 27 | this.breakpoint = info.line; 28 | } 29 | } 30 | }); 31 | } 32 | 33 | check() { 34 | // Clean previus records 35 | this.testingResults = []; 36 | 37 | let cm = this.main.editor; 38 | let nLines = cm.getDoc().size; 39 | 40 | let mainStartsAt = 0; 41 | for (let i = 0; i < nLines; i++) { 42 | if (isLineAfterMain(cm, i)) { 43 | mainStartsAt = i; 44 | break; 45 | } 46 | } 47 | this.testLine(mainStartsAt); 48 | } 49 | 50 | testLine(nLine) { 51 | let cm = mainGE.editor; 52 | let visualDebugger = mainGE.visualDebugger; 53 | 54 | // If is done testing... 55 | if (nLine >= cm.getDoc().size) { 56 | visualDebugger.testingLine = 0; 57 | visualDebugger.testing = false; 58 | 59 | let results = visualDebugger.testingResults; 60 | let range = getResultRange(results); 61 | let sum = getDeltaSum(results); 62 | let hits = getHits(results); 63 | 64 | console.log('Test: ',range.max.ms + 'ms', results); 65 | cm.clearGutter('breakpoints'); 66 | for (let i in results) { 67 | let pct = (results[i].delta / sum) * 100; 68 | let size = (results[i].delta / sum) * 30; 69 | let markerHTML = '
' + results[i].ms.toFixed(2); 70 | if (results[i].delta > 0.) { 71 | markerHTML += ''; 76 | } 77 | 78 | cm.setGutterMarker(results[i].line, 'breakpoints', makeMarker(markerHTML + '
')); 79 | } 80 | return; 81 | } 82 | 83 | if (isLineAfterMain(cm, nLine)) { 84 | // If the line is inside the main function 85 | let shader = mainGE.shader.canvas; 86 | 87 | // Check for an active variable (a variable that have been declare or modify in this line) 88 | let variableRE = new RegExp('\\s*[float|vec2|vec3|vec4]?\\s+([\\w|\\_]*)[\\.\\w]*?\\s+[\\+|\\-|\\\\|\\*]?\\=', 'i'); 89 | let match = variableRE.exec(cm.getLine(nLine)); 90 | if (match) { 91 | // if there is an active variable, get what type is 92 | let variable = match[1]; 93 | let type = getVariableType(cm, variable); 94 | if (type === 'none') { 95 | // If it fails on finding the type keep going with the test on another line 96 | visualDebugger.testLine(nLine + 1); 97 | return; 98 | } 99 | 100 | // Prepare 101 | visualDebugger.testing = true; 102 | visualDebugger.testingLine = nLine; 103 | visualDebugger.testingFrag = getShaderForTypeVarInLine(cm, type, variable, nLine); 104 | visualDebugger.testingSamples = []; 105 | 106 | unfocusAll(cm); 107 | focusLine(cm, nLine); 108 | mainGE.debugging = true; 109 | 110 | shader.test(this.onTest, visualDebugger.testingFrag); 111 | } 112 | else { 113 | visualDebugger.testLine(nLine + 1); 114 | } 115 | } 116 | else { 117 | // If the line is not inside main function, test the next one... 118 | visualDebugger.testLine(nLine + 1); 119 | } 120 | } 121 | 122 | onTest (target) { 123 | let cm = mainGE.editor; 124 | let shader = mainGE.shader.canvas; 125 | let visualDebugger = mainGE.visualDebugger; 126 | 127 | // If the test shader compiled... 128 | if (target.wasValid) { 129 | // get data, process and store. 130 | let elapsedMs = target.timeElapsedMs; 131 | 132 | if (visualDebugger.testingSamples.length < N_SAMPLES - 1) { 133 | visualDebugger.testingSamples.push(elapsedMs); 134 | shader.test(visualDebugger.onTest, visualDebugger.testingFrag); 135 | } 136 | else { 137 | focusAll(cm); 138 | mainGE.debugging = false; 139 | visualDebugger.testingSamples.push(elapsedMs); 140 | elapsedMs = getMedian(visualDebugger.testingSamples); 141 | 142 | let range = getResultRange(visualDebugger.testingResults); 143 | let delta = elapsedMs - range.max.ms; 144 | if (visualDebugger.testingResults.length === 0) { 145 | delta = 0.0; 146 | } 147 | visualDebugger.testingResults.push({line:visualDebugger.testingLine, ms:target.timeElapsedMs, delta:delta}); 148 | // console.log('Testing line:', visualDebugger.testingLine, elapsedMs, delta, range); 149 | 150 | // Create gutter marker 151 | cm.setGutterMarker(visualDebugger.testingLine, 152 | 'breakpoints', 153 | makeMarker(elapsedMs.toFixed(2))); 154 | 155 | // Test next line 156 | visualDebugger.testLine(visualDebugger.testingLine + 1); 157 | }; 158 | } 159 | else { 160 | console.log(target); 161 | // Test next line 162 | visualDebugger.testLine(visualDebugger.testingLine + 1); 163 | } 164 | } 165 | 166 | debug (variable, nLine) { 167 | focusAll(this.main.editor); 168 | this.main.debugging = false; 169 | 170 | if (isLineAfterMain(this.main.editor, nLine)) { 171 | var type = getVariableType(this.main.editor, variable); 172 | if (type !== 'none') { 173 | event.preventDefault(); 174 | this.main.shader.canvas.load(getShaderForTypeVarInLine(this.main.editor, type, variable, nLine)); 175 | unfocusAll(this.main.editor); 176 | focusLine(this.main.editor, nLine); 177 | this.main.debugging = true; 178 | } 179 | } 180 | else { 181 | this.main.update(); 182 | } 183 | } 184 | 185 | iluminate (variable) { 186 | if (this.main.debbuging && this.variable === this.variable) { 187 | return; 188 | } 189 | // this.clean(); 190 | 191 | let cm = this.main.editor; 192 | 193 | // Highlight all calls to a variable 194 | this.overlay = searchOverlay(variable, true); 195 | cm.addOverlay(this.overlay); 196 | if (cm.showMatchesOnScrollbar) { 197 | if (this.annotate) { 198 | this.annotate.clear(); this.annotate = null; 199 | } 200 | this.annotate = cm.showMatchesOnScrollbar(variable, true); 201 | } 202 | } 203 | 204 | clean (event) { 205 | if (event && event.target && event.target.className === 'ge_assing_marker') { 206 | return; 207 | } 208 | 209 | let cm = this.main.editor; 210 | cm.clearGutter('breakpoints'); 211 | if (this.overlay) { 212 | cm.removeOverlay(this.overlay, true); 213 | } 214 | this.type = null; 215 | if (this.main.debbuging) { 216 | this.main.shader.canvas.load(this.main.options.frag_header + this.main.editor.getValue() + this.main.options.frag_footer); 217 | } 218 | this.main.debbuging = false; 219 | } 220 | } 221 | 222 | function makeMarker(html, extraClass) { 223 | let marker = document.createElement('div'); 224 | marker.setAttribute('class', 'ge_assing_marker'); 225 | marker.innerHTML = html; 226 | return marker; 227 | } 228 | 229 | function searchOverlay(query, caseInsensitive) { 230 | if (typeof query === 'string') { 231 | query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), caseInsensitive ? 'gi' : 'g'); 232 | } 233 | else if (!query.global) { 234 | query = new RegExp(query.source, query.ignoreCase ? 'gi' : 'g'); 235 | } 236 | 237 | return { 238 | token: function(stream) { 239 | query.lastIndex = stream.pos; 240 | var match = query.exec(stream.string); 241 | if (match && match.index === stream.pos) { 242 | stream.pos += match[0].length || 1; 243 | return 'searching'; 244 | } 245 | else if (match) { 246 | stream.pos = match.index; 247 | } 248 | else { 249 | stream.skipToEnd(); 250 | } 251 | } 252 | }; 253 | } 254 | -------------------------------------------------------------------------------- /src/js/ui/modals/ExportModal.js: -------------------------------------------------------------------------------- 1 | import MenuItem from '../MenuItem'; 2 | import Modal from './Modal'; 3 | // import { saveOnServer, createOpenFrameArtwork } from '../../io/share'; 4 | 5 | export default class ExportModal extends Modal { 6 | constructor (CSS_PREFIX, properties) { 7 | super(CSS_PREFIX, properties); 8 | this.main = properties.main; 9 | 10 | this.save = new MenuItem(this.el, 'ge_sub_menu', 'Download file', (event) => { 11 | properties.main.download(); 12 | }); 13 | 14 | // this.codeURL = new MenuItem(this.el, 'ge_sub_menu', 'Code URL...', (event) => { 15 | // saveOnServer(this.main, (event) => { 16 | // prompt('Use this url to share your code', 'http://thebookofshaders.com/edit.php?log=' + event.name); 17 | // this.removeModal(); 18 | // }); 19 | // }); 20 | 21 | // this.shaderURL = new MenuItem(this.el, 'ge_sub_menu', 'Artwork URL...', (event) => { 22 | // saveOnServer(this.main, (event) => { 23 | // prompt('Use this url to share your artwork', 'http://player.thebookofshaders.com/?log=' + event.name); 24 | // this.removeModal(); 25 | // }); 26 | // }); 27 | 28 | // let shareOF = new MenuItem(this.el, 'ge_sub_menu', 'Artwork to [o]', (event) => { 29 | // shareOF.el.innerHTML = 'Artwork to [o]: adding to collection'; 30 | // saveOnServer(this.main, (event) => { 31 | // createOpenFrameArtwork(this.main, event.name, event.url, (success) => { 32 | // if (success) { 33 | // shareOF.el.innerHTML = 'Artwork to [o]: added!'; 34 | // } 35 | // else { 36 | // shareOF.el.innerHTML = 'Artwork to [o]: failed :('; 37 | // } 38 | // setTimeout(() => { 39 | // shareOF.el.innerHTML = '[o]'; 40 | // this.removeModal(); 41 | // }, 4000); 42 | // }); 43 | // }); 44 | // }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/js/ui/modals/Modal.js: -------------------------------------------------------------------------------- 1 | import { subscribeMixin } from '../../tools/mixin'; 2 | 3 | export default class Modal { 4 | constructor (CSS_PREFIX, properties) { 5 | subscribeMixin(this); 6 | this.CSS_PREFIX = CSS_PREFIX; 7 | 8 | properties = properties || {}; 9 | for (let prop in properties) { 10 | this[prop] = properties[prop]; 11 | } 12 | 13 | this.el = document.createElement('div'); 14 | this.el.className = this.CSS_PREFIX + '_modal ge_modal'; 15 | this.el.style.backgroundColor = this.bgColor; 16 | this.el.innerHTML = this.innerHTML || ''; 17 | 18 | if (this.elements) { 19 | for (let i = 0; i < this.elements.length; i++) { 20 | this.el.appendChild(this.elements[i]); 21 | } 22 | } 23 | 24 | this.isVisible = false; 25 | } 26 | 27 | close () { 28 | this.trigger('close'); 29 | } 30 | 31 | showAt (cm) { 32 | let cursor = cm.cursorCoords(true, 'page'); 33 | let x = cursor.left; 34 | let y = cursor.top; 35 | 36 | y += 30; 37 | 38 | this.presentModal(x, y); 39 | } 40 | 41 | presentModal (x, y) { 42 | // Listen for interaction outside of the modal 43 | window.setTimeout(() => { 44 | this.onClickOutsideHandler = addEvent(document.body, 'click', this.onClickOutside, this); 45 | this.onKeyPressHandler = addEvent(window, 'keydown', this.onKeyPress, this); 46 | }, 0); 47 | this.isVisible = true; 48 | 49 | this.el.style.left = x + 'px'; 50 | this.el.style.top = y + 'px'; 51 | this.el.style.width = this.width + 'px'; 52 | this.el.style.height = this.height + 'px'; 53 | 54 | if (this.position) { 55 | this.el.style.position = this.position; 56 | } 57 | 58 | document.body.appendChild(this.el); 59 | 60 | this.trigger('show'); 61 | } 62 | 63 | getModalClass() { 64 | return this.CSS_PREFIX + 'modal'; 65 | } 66 | 67 | onKeyPress (event) { 68 | this.removeModal(); 69 | } 70 | 71 | onClickOutside (event) { 72 | // HACKY!! 73 | // A click event fires on the body after mousedown - mousemove, simultaneously with 74 | // mouseup. So if someone started a mouse action inside the modal and then 75 | // mouseup'd outside of it, it fires a click event on the body, thus, causing the 76 | // modal to disappear when the user does not expect it to, since the mouse down event 77 | // did not start outside the modal. 78 | // There might be (or should be) a better way to track this, but right now, just cancel 79 | // the event if the target ends up being on the body directly rather than on one of the 80 | // other child elements. 81 | if (event.target === document.body) { 82 | return; 83 | } 84 | // end this specific hacky part 85 | let target = event.target; 86 | if (target) { 87 | while (target !== document.documentElement && !target.classList.contains(this.getModalClass())) { 88 | target = target.parentNode; 89 | } 90 | 91 | if (!target.classList.contains(this.getModalClass())) { 92 | this.removeModal(); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Removes modal from DOM and destroys related event listeners 99 | */ 100 | removeModal () { 101 | if (this.el && this.el.parentNode) { 102 | this.el.parentNode.removeChild(this.el); 103 | } 104 | removeEvent(document.body, 'click', this.onClickOutsideHandler); 105 | this.onClickOutsideHandler = null; 106 | removeEvent(window, 'keydown', this.onKeyPressHandler); 107 | this.onKeyPressHandler = null; 108 | 109 | this.close(); 110 | this.isVisible = false; 111 | } 112 | } 113 | 114 | /* Event handling */ 115 | export function addEvent (element, event, callback, caller) { 116 | let handler; 117 | element.addEventListener(event, handler = function (e) { 118 | callback.call(caller, e); 119 | }, false); 120 | return handler; 121 | } 122 | 123 | export function removeEvent (element, event, callback) { 124 | element.removeEventListener(event, callback, false); 125 | } 126 | -------------------------------------------------------------------------------- /src/js/ui/pickers/ColorPicker.js: -------------------------------------------------------------------------------- 1 | /* 2 | Original: https://github.com/tangrams/tangram-play/blob/gh-pages/src/js/addons/ui/widgets/ColorPickerModal.js 3 | Author: Lou Huang (@saikofish) 4 | */ 5 | 6 | import Picker from './Picker'; 7 | import Color from './types/Color'; 8 | import { addEvent, removeEvent } from './Picker'; 9 | import { getDevicePixelRatio } from '../../tools/common'; 10 | 11 | import { subscribeInteractiveDom } from '../../tools/interactiveDom'; 12 | 13 | // Some common use variables 14 | let currentTarget; 15 | let currentTargetHeight = 0; 16 | let domCache; 17 | 18 | export default class ColorPicker extends Picker { 19 | constructor (color = 'vec3(1.0,0.0,0.0)', properties = {}) { 20 | super('ge_colorpicker_', properties); 21 | 22 | this.width = 250; // in pixels 23 | this.height = 250; // in pixels 24 | 25 | this.disc = { width: 200, height: 200 }; 26 | this.barlum = { width: 25, height: 200 }; 27 | 28 | this.setValue(color); 29 | this.init(); 30 | } 31 | 32 | init() { 33 | if (!domCache) { 34 | let modal = document.createElement('div'); 35 | let patch = document.createElement('div'); 36 | let map = document.createElement('div'); 37 | let disc = document.createElement('canvas'); 38 | let cover = document.createElement('div'); 39 | let cursor = document.createElement('div'); 40 | let barbg = document.createElement('div'); 41 | let barwhite = document.createElement('div'); 42 | let barlum = document.createElement('canvas'); 43 | let barcursors = document.createElement('div'); 44 | let leftcursor = document.createElement('div'); 45 | let rightcursor = document.createElement('div'); 46 | 47 | modal.className = this.CSS_PREFIX + 'modal ge_picker_modal'; 48 | modal.style.backgroundColor = this.bgColor; 49 | patch.className = this.CSS_PREFIX + 'patch'; 50 | patch.style.backgroundColor = this.bgColor; 51 | map.className = this.CSS_PREFIX + 'hsv-map'; 52 | disc.className = this.CSS_PREFIX + 'disc'; 53 | disc.style.backgroundColor = this.bgColor; 54 | cover.className = this.CSS_PREFIX + 'disc-cover'; 55 | cursor.className = this.CSS_PREFIX + 'disc-cursor'; 56 | barbg.className = this.CSS_PREFIX + 'bar-bg'; 57 | barwhite.className = this.CSS_PREFIX + 'bar-white'; 58 | barlum.className = this.CSS_PREFIX + 'bar-luminance'; 59 | barcursors.className = this.CSS_PREFIX + 'bar-cursors'; 60 | leftcursor.className = this.CSS_PREFIX + 'bar-cursor-left'; 61 | rightcursor.className = this.CSS_PREFIX + 'bar-cursor-right'; 62 | 63 | map.id = 'cp-map'; 64 | barcursors.id = 'cp-bar'; 65 | 66 | modal.appendChild(patch); 67 | modal.appendChild(map); 68 | 69 | map.appendChild(disc); 70 | map.appendChild(cover); 71 | map.appendChild(cursor); 72 | map.appendChild(barbg); 73 | map.appendChild(barwhite); 74 | map.appendChild(barlum); 75 | map.appendChild(barcursors); 76 | barcursors.appendChild(leftcursor); 77 | barcursors.appendChild(rightcursor); 78 | 79 | domCache = modal; 80 | } 81 | 82 | // Returns a clone of the cached document fragment 83 | this.el = domCache.cloneNode(true); 84 | subscribeInteractiveDom(this.el, { move: true, resize: false, snap: false }); 85 | 86 | // TODO: Improve these references 87 | // The caching of references is likely to be important for speed 88 | this.dom = {}; 89 | this.dom.hsvMap = this.el.querySelector('.ge_colorpicker_hsv-map'); 90 | this.dom.hsvMapCover = this.dom.hsvMap.children[1]; // well... 91 | this.dom.hsvMapCursor = this.dom.hsvMap.children[2]; 92 | this.dom.hsvBarBGLayer = this.dom.hsvMap.children[3]; 93 | this.dom.hsvBarWhiteLayer = this.dom.hsvMap.children[4]; 94 | this.dom.hsvBarCursors = this.dom.hsvMap.children[6]; 95 | this.dom.hsvLeftCursor = this.dom.hsvBarCursors.children[0]; 96 | this.dom.hsvRightCursor = this.dom.hsvBarCursors.children[1]; 97 | 98 | this.dom.colorDisc = this.el.querySelector('.ge_colorpicker_disc'); 99 | this.dom.luminanceBar = this.el.querySelector('.ge_colorpicker_bar-luminance'); 100 | 101 | if (this.linkButton) { 102 | let lbutton = document.createElement('div'); 103 | lbutton.innerHTML = '+'; 104 | lbutton.className = this.CSS_PREFIX + 'link-button'; 105 | lbutton.style.color = this.fgColor; 106 | this.el.appendChild(lbutton); 107 | 108 | lbutton.addEventListener('click', () => { 109 | this.trigger('linkButton', this.value); 110 | if (typeof this.linkButton === 'function') { 111 | this.linkButton(this.value); 112 | } 113 | this.removeModal(); 114 | }); 115 | } 116 | } 117 | 118 | draw () { 119 | // Render color patch 120 | let patch = this.el.querySelector('.ge_colorpicker_patch'); 121 | patch.style.backgroundColor = this.value.getString('rgb'); 122 | 123 | // Render HSV picker 124 | let color = this.value.colors; 125 | let colorDiscRadius = this.dom.colorDisc.offsetHeight / 2; 126 | let pi2 = Math.PI * 2; 127 | let x = Math.cos(pi2 - color.hsv.h * pi2); 128 | let y = Math.sin(pi2 - color.hsv.h * pi2); 129 | let r = color.hsv.s * (colorDiscRadius - 5); 130 | 131 | this.dom.hsvMapCover.style.opacity = 1 - color.hsv.v / 255; 132 | // this is the faster version... 133 | this.dom.hsvBarWhiteLayer.style.opacity = 1 - color.hsv.s; 134 | this.dom.hsvBarBGLayer.style.backgroundColor = 'rgb(' + 135 | color.hueRGB.r + ',' + 136 | color.hueRGB.g + ',' + 137 | color.hueRGB.b + ')'; 138 | 139 | this.dom.hsvMapCursor.style.cssText = 140 | 'left: ' + (x * r + colorDiscRadius) + 'px;' + 141 | 'top: ' + (y * r + colorDiscRadius) + 'px;' + 142 | 'border-color: ' + (color.luminance > 0.22 ? '#333;' : '#ddd'); 143 | 144 | if (color.luminance > 0.22) { 145 | this.dom.hsvBarCursors.classList.add('ge_colorpicker_dark'); 146 | } 147 | else { 148 | this.dom.hsvBarCursors.classList.remove('ge_colorpicker_dark'); 149 | } 150 | 151 | if (this.dom.hsvLeftCursor) { 152 | this.dom.hsvLeftCursor.style.top = this.dom.hsvRightCursor.style.top = ((1 - color.hsv.v / 255) * colorDiscRadius * 2) + 'px'; 153 | } 154 | } 155 | 156 | presentModal (x, y) { 157 | super.presentModal(x, y); 158 | 159 | // // Listen for interaction on the HSV map 160 | this.onHsvDownHandler = addEvent(this.dom.hsvMap, 'mousedown', this.onHsvDown, this); 161 | 162 | let colorDisc = this.dom.colorDisc; 163 | 164 | if (colorDisc.getContext) { 165 | // HSV color wheel with white center 166 | let diskContext = colorDisc.getContext('2d'); 167 | let ratio = getDevicePixelRatio(diskContext); 168 | let width = this.disc.width / ratio; 169 | let height = this.disc.height / ratio; 170 | this.dom.colorDisc.width = width * ratio; 171 | this.dom.colorDisc.height = height * ratio; 172 | diskContext.scale(ratio, ratio); 173 | 174 | drawDisk( 175 | diskContext, 176 | [width / 2, height / 2], 177 | [width / 2 - 1, height / 2 - 1], 178 | 360, 179 | function (ctx, angle) { 180 | let gradient = ctx.createRadialGradient(1, 1, 1, 1, 1, 0); 181 | gradient.addColorStop(0, 'hsl(' + (360 - angle + 0) + ', 100%, 50%)'); 182 | gradient.addColorStop(1, '#fff'); 183 | 184 | ctx.fillStyle = gradient; 185 | ctx.fill(); 186 | } 187 | ); 188 | 189 | // gray border 190 | drawCircle( 191 | diskContext, 192 | [width / 2, height / 2], 193 | [width / 2, height / 2], 194 | this.bgColor,// '#303030', 195 | 2 / ratio 196 | ); 197 | 198 | // draw the luminanceBar bar 199 | let ctx = this.dom.luminanceBar.getContext('2d'); 200 | this.dom.luminanceBar.width = this.barlum.width; 201 | this.dom.luminanceBar.height = this.barlum.height * ratio; 202 | ctx.scale(ratio, ratio); 203 | let gradient = ctx.createLinearGradient(0, 0, 0, this.barlum.height / ratio); 204 | 205 | gradient.addColorStop(0, 'transparent'); 206 | gradient.addColorStop(1, 'black'); 207 | 208 | ctx.fillStyle = gradient; 209 | ctx.fillRect(0, 0, 30, 200); 210 | } 211 | this.draw(); 212 | } 213 | 214 | /** 215 | * Updates only the color value of the color picker 216 | * and the view. Designed to be called by external modules 217 | * so that it can update its internal value from an outside source. 218 | * Does no DOM creation & other initialization work. 219 | */ 220 | setValue (color) { 221 | this.value = new Color(color); 222 | } 223 | 224 | /* ---------------------------------- */ 225 | /* ---- HSV-circle color picker ----- */ 226 | /* ---------------------------------- */ 227 | 228 | // Actions when user mouses down on HSV color map 229 | onHsvDown (event) { 230 | let target = event.target || event.srcElement; 231 | event.preventDefault(); 232 | 233 | currentTarget = target.id ? target : target.parentNode; 234 | currentTargetHeight = currentTarget.offsetHeight; // as diameter of circle 235 | 236 | // Starts listening for mousemove and mouseup events 237 | this.onHsvMoveHandler = addEvent(this.el, 'mousemove', this.onHsvMove, this); 238 | this.onHsvUpHandler = addEvent(window, 'mouseup', this.onHsvUp, this); 239 | 240 | this.onHsvMove(event); 241 | 242 | // Hides mouse cursor and begins rendering loop 243 | this.dom.hsvMap.classList.add('ge_colorpicker_no-cursor'); 244 | this.renderer.start(); 245 | } 246 | 247 | // Actions when user moves around on HSV color map 248 | onHsvMove (event) { 249 | event.preventDefault(); 250 | event.stopPropagation(); 251 | 252 | let r, x, y, h, s; 253 | if (event.target === this.dom.hsvMapCover && currentTarget === this.dom.hsvMap) { // the circle 254 | r = currentTargetHeight / 2, 255 | x = event.offsetX - r, 256 | y = event.offsetY - r, 257 | h = (360 - ((Math.atan2(y, x) * 180 / Math.PI) + (y < 0 ? 360 : 0))) / 360, 258 | s = (Math.sqrt((x * x) + (y * y)) / r); 259 | this.value.set({ h, s }, 'hsv'); 260 | } 261 | else if (event.target === this.dom.hsvBarCursors && currentTarget === this.dom.hsvBarCursors) { // the luminanceBar 262 | let v = (currentTargetHeight - (event.offsetY)) / currentTargetHeight; 263 | v = Math.max(0, Math.min(1, v)) * 255; 264 | this.value.set({ v: v }, 'hsv'); 265 | } 266 | 267 | this.trigger('changed', this.value); 268 | } 269 | 270 | // Actions when user mouses up on HSV color map 271 | onHsvUp (event) { 272 | // Stops rendering and returns mouse cursor 273 | this.renderer.stop(); 274 | this.dom.hsvMap.classList.remove('ge_colorpicker_no-cursor'); 275 | this.destroyEvents(); 276 | } 277 | 278 | // Destroy event listeners that exist during mousedown colorpicker interaction 279 | destroyEvents () { 280 | removeEvent(this.el, 'mousemove', this.onHsvMoveHandler); 281 | this.onHsvMoveHandler = null; 282 | removeEvent(window, 'mouseup', this.onHsvUpHandler); 283 | this.onHsvUpHandler = null; 284 | } 285 | 286 | close () { 287 | this.destroyEvents(); 288 | removeEvent(this.dom.hsvMap, 'mousedown', this.onHsvDownHandler); 289 | this.onHsvDownHandler = null; 290 | } 291 | } 292 | 293 | // generic function for drawing a canvas disc 294 | function drawDisk (ctx, coords, radius, steps, colorCallback) { 295 | let x = coords[0] || coords; // coordinate on x-axis 296 | let y = coords[1] || coords; // coordinate on y-axis 297 | let a = radius[0] || radius; // radius on x-axis 298 | let b = radius[1] || radius; // radius on y-axis 299 | let angle = 360; 300 | let coef = Math.PI / 180; 301 | 302 | ctx.save(); 303 | ctx.translate(x - a, y - b); 304 | ctx.scale(a, b); 305 | 306 | steps = (angle / steps) || 360; 307 | 308 | for (; angle > 0 ; angle -= steps) { 309 | ctx.beginPath(); 310 | if (steps !== 360) { 311 | ctx.moveTo(1, 1); // stroke 312 | } 313 | ctx.arc(1, 1, 1, 314 | (angle - (steps / 2) - 1) * coef, 315 | (angle + (steps / 2) + 1) * coef); 316 | 317 | if (colorCallback) { 318 | colorCallback(ctx, angle); 319 | } 320 | else { 321 | ctx.fillStyle = 'black'; 322 | ctx.fill(); 323 | } 324 | } 325 | ctx.restore(); 326 | } 327 | 328 | function drawCircle (ctx, coords, radius, color, width) { // uses drawDisk 329 | width = width || 1; 330 | radius = [ 331 | (radius[0] || radius) - width / 2, 332 | (radius[1] || radius) - width / 2 333 | ]; 334 | drawDisk(ctx, coords, radius, 1, function (ctx, angle) { 335 | ctx.restore(); 336 | ctx.lineWidth = width; 337 | ctx.strokeStyle = color || '#000'; 338 | ctx.stroke(); 339 | }); 340 | } 341 | -------------------------------------------------------------------------------- /src/js/ui/pickers/FloatPicker.js: -------------------------------------------------------------------------------- 1 | import Picker from './Picker'; 2 | import Float from './types/Float'; 3 | 4 | export default class FloatPicker extends Picker { 5 | constructor (number, properties) { 6 | super('ge_floatpicker_', properties); 7 | 8 | this.width = this.width || 250; 9 | this.height = this.height || 40; 10 | 11 | this.prevOffset = 0; 12 | this.scale = 2; 13 | 14 | this.setValue(number || 1); 15 | this.create(); 16 | } 17 | 18 | draw () { 19 | this.ctx.clearRect(0, 0, this.width, this.height); 20 | 21 | // horizontal line 22 | this.ctx.strokeStyle = this.dimColor; 23 | this.ctx.lineWidth = 1; 24 | this.ctx.beginPath(); 25 | this.ctx.moveTo(0, 0.5 + this.height * 0.5); 26 | this.ctx.lineTo(0 + this.width, 0.5 + this.height * 0.5); 27 | this.ctx.closePath(); 28 | this.ctx.stroke(); 29 | 30 | // vertical line 31 | this.ctx.strokeStyle = this.fnColor; 32 | this.ctx.lineWidth = 1; 33 | this.ctx.beginPath(); 34 | this.ctx.moveTo(this.width * 0.5, 0); 35 | this.ctx.lineTo(this.width * 0.5, this.height); 36 | this.ctx.closePath(); 37 | this.ctx.stroke(); 38 | 39 | // Triangle line 40 | this.ctx.fillStyle = this.overPoint ? this.selColor : this.fnColor; 41 | this.ctx.beginPath(); 42 | this.ctx.moveTo(this.width * 0.5, 5); 43 | this.ctx.lineTo(this.width * 0.48, 0); 44 | this.ctx.lineTo(this.width * 0.52, 0); 45 | this.ctx.closePath(); 46 | this.ctx.fill(); 47 | 48 | let times = 3; 49 | let unit = 40; 50 | let step = this.width / unit; 51 | let sections = unit * times; 52 | 53 | let offsetX = this.offsetX; 54 | 55 | if (Math.abs(this.offsetX - this.width * 0.5) > this.width * 0.5) { 56 | offsetX = (this.offsetX - this.width * 0.5) % (this.width * 0.5) + this.width; 57 | } 58 | 59 | this.ctx.strokeStyle = this.dimColor; 60 | this.ctx.beginPath(); 61 | for (let i = 0; i < sections; i++) { 62 | let l = (i % (unit / 2) === 0) ? this.height * 0.35 : (i % (unit / 4) === 0) ? this.height * 0.2 : this.height * 0.1; 63 | this.ctx.moveTo(i * step - offsetX, this.height * 0.5 - l); 64 | this.ctx.lineTo(i * step - offsetX, this.height * 0.5 + l); 65 | } 66 | this.ctx.stroke(); 67 | 68 | let val = Math.round(((this.value - this.min) / this.range) * this.width); 69 | 70 | // point 71 | this.ctx.strokeStyle = this.overPoint ? this.selColor : this.fnColor; 72 | this.ctx.lineWidth = 1; 73 | this.ctx.beginPath(); 74 | this.ctx.moveTo(this.offsetX + val, this.height * 0.5); 75 | this.ctx.lineTo(this.offsetX + val, this.height); 76 | this.ctx.closePath(); 77 | this.ctx.stroke(); 78 | 79 | this.overPoint = false; 80 | } 81 | 82 | onMouseDown (event) { 83 | this.prevOffset = event.offsetX; 84 | super.onMouseDown(event); 85 | } 86 | 87 | // Actions when user moves around on HSV color map 88 | onMouseMove (event) { 89 | let x = event.offsetX; 90 | 91 | let vel = x - this.prevOffset; 92 | let offset = this.offsetX - vel; 93 | 94 | let center = this.width / this.scale; 95 | this.setValue(offset / center); 96 | this.prevOffset = x; 97 | 98 | // fire 'changed' 99 | var number = new Float(this.getValue()); 100 | this.trigger('changed', number); 101 | this.overPoint = true; 102 | } 103 | 104 | setValue (value) { 105 | if (typeof value === 'string') { 106 | this.value = parseFloat(value); 107 | } 108 | else if (typeof value === 'number') { 109 | this.value = value; 110 | } 111 | let center = (this.width / this.scale); 112 | this.offsetX = this.value * center; 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /src/js/ui/pickers/Picker.js: -------------------------------------------------------------------------------- 1 | /* 2 | Original: https://github.com/tangrams/tangram-play/blob/gh-pages/src/js/addons/ui/widgets/ColorPickerModal.js 3 | Author: Lou Huang (@saikofish) 4 | */ 5 | 6 | import { getDevicePixelRatio } from '../../tools/common'; 7 | import { subscribeMixin } from '../../tools/mixin'; 8 | 9 | export default class Picker { 10 | constructor (CSS_PREFIX, properties) { 11 | subscribeMixin(this); 12 | this.CSS_PREFIX = CSS_PREFIX; 13 | 14 | this.bgColor = 'rgb(46, 48, 51)'; 15 | this.dimColor = 'rgb(100, 100, 100)'; 16 | this.fnColor = 'rgb(230, 230, 230)'; 17 | this.selColor = 'rgb(133, 204, 196)'; 18 | 19 | properties = properties || {}; 20 | for (let prop in properties) { 21 | this[prop] = properties[prop]; 22 | } 23 | 24 | /** 25 | * This initializes the renderer. It uses requestAnimationFrame() to 26 | * smoothly render changes in the color picker as user interacts with it. 27 | */ 28 | this.renderer = { 29 | // Stores a reference to the animation rendering loop. 30 | frame: null, 31 | 32 | drawFrame: () => { 33 | if (!this.el) { 34 | return; 35 | } 36 | this.draw(); 37 | }, 38 | 39 | // Starts animation rendering loop 40 | start: () => { 41 | this.renderer.drawFrame(); 42 | this.renderer.frame = window.requestAnimationFrame(this.renderer.start); 43 | }, 44 | 45 | // Stops animation rendering loop 46 | stop: () => { 47 | window.cancelAnimationFrame(this.renderer.frame); 48 | } 49 | }; 50 | this.isVisible = false; 51 | } 52 | 53 | create () { 54 | this.el = document.createElement('div'); 55 | this.el.className = this.CSS_PREFIX + 'modal ge_picker_modal'; 56 | this.el.style.backgroundColor = this.bgColor; 57 | 58 | this.canvas = document.createElement('canvas'); 59 | this.canvas.className = this.CSS_PREFIX + 'canvas ge_picker_canvas'; 60 | this.canvas.style.backgroundColor = this.bgColor; 61 | 62 | this.el.appendChild(this.canvas); 63 | this.ctx = this.canvas.getContext('2d'); 64 | 65 | let ratio = getDevicePixelRatio(this.ctx); 66 | this.canvas.width = this.width * ratio; 67 | this.canvas.height = this.height * ratio; 68 | this.ctx.scale(ratio, ratio); 69 | } 70 | 71 | draw () { 72 | // render rutine 73 | } 74 | 75 | close () { 76 | // Close rutine 77 | this.destroyEvents(); 78 | removeEvent(this.el, 'mousedown', this.onMouseDownHandler); 79 | this.onMouseDownHandler = null; 80 | } 81 | 82 | destroyEvents () { 83 | removeEvent(this.el, 'mousemove', this.onMouseMoveHandler); 84 | this.onMouseMoveHandler = null; 85 | removeEvent(window, 'mouseup', this.onMouseUpHandler); 86 | this.onMouseUpHandler = null; 87 | } 88 | 89 | setValue (value) { 90 | this.value = value; 91 | } 92 | 93 | getValue () { 94 | return this.value; 95 | } 96 | 97 | showAt (cm) { 98 | let cursor = cm.cursorCoords(true, 'page'); 99 | let x = cursor.left; 100 | let y = cursor.top; 101 | 102 | x -= this.width * 0.5; 103 | y += 30; 104 | 105 | // // Check if desired x, y will be outside the viewport. 106 | // // Do not allow the modal to disappear off the edge of the window. 107 | // x = (x + this.width < window.innerWidth) ? x : (window.innerWidth - 20 - this.width); 108 | // y = (y + this.height < window.innerHeight) ? y : (window.innerHeight - 20 - this.height); 109 | 110 | this.presentModal(x, y); 111 | } 112 | 113 | presentModal (x, y) { 114 | // Listen for interaction outside of the modal 115 | window.setTimeout(() => { 116 | this.onClickOutsideHandler = addEvent(document.body, 'click', this.onClickOutside, this); 117 | this.onKeyPressHandler = addEvent(window, 'keydown', this.onKeyPress, this); 118 | }, 0); 119 | this.isVisible = true; 120 | 121 | this.el.style.left = x + 'px'; 122 | this.el.style.top = y + 'px'; 123 | this.el.style.width = this.width + 'px'; 124 | this.el.style.height = this.height + 'px'; 125 | document.body.appendChild(this.el); 126 | 127 | this.onMouseDownHandler = addEvent(this.el, 'mousedown', this.onMouseDown, this); 128 | 129 | this.renderer.drawFrame(); 130 | } 131 | 132 | /** 133 | * Removes modal from DOM and destroys related event listeners 134 | */ 135 | removeModal () { 136 | if (this.el && this.el.parentNode) { 137 | this.el.parentNode.removeChild(this.el); 138 | } 139 | removeEvent(document.body, 'click', this.onClickOutsideHandler); 140 | this.onClickOutsideHandler = null; 141 | removeEvent(window, 'keydown', this.onKeyPressHandler); 142 | this.onKeyPressHandler = null; 143 | 144 | this.close(); 145 | this.isVisible = false; 146 | } 147 | 148 | onKeyPress (event) { 149 | this.removeModal(); 150 | } 151 | 152 | onClickOutside (event) { 153 | // HACKY!! 154 | // A click event fires on the body after mousedown - mousemove, simultaneously with 155 | // mouseup. So if someone started a mouse action inside the modal and then 156 | // mouseup'd outside of it, it fires a click event on the body, thus, causing the 157 | // modal to disappear when the user does not expect it to, since the mouse down event 158 | // did not start outside the modal. 159 | // There might be (or should be) a better way to track this, but right now, just cancel 160 | // the event if the target ends up being on the body directly rather than on one of the 161 | // other child elements. 162 | if (event.target === document.body) { 163 | return; 164 | } 165 | // end this specific hacky part 166 | 167 | let target = event.target; 168 | 169 | while (target !== document.documentElement && !target.classList.contains(this.CSS_PREFIX + 'modal')) { 170 | target = target.parentNode; 171 | } 172 | 173 | if (!target.classList.contains(this.CSS_PREFIX + 'modal')) { 174 | this.removeModal(); 175 | } 176 | } 177 | 178 | onMouseDown (event) { 179 | event.preventDefault(); 180 | 181 | // Starts listening for mousemove and mouseup events 182 | this.onMouseMoveHandler = addEvent(this.el, 'mousemove', this.onMouseMove, this); 183 | this.onMouseUpHandler = addEvent(window, 'mouseup', this.onMouseUp, this); 184 | 185 | this.onMouseMove(event); 186 | 187 | this.renderer.start(); 188 | } 189 | 190 | onMouseMove (event) { 191 | } 192 | 193 | onMouseUp (event) { 194 | this.renderer.stop(); 195 | this.destroyEvents(); 196 | } 197 | } 198 | 199 | /* Event handling */ 200 | export function addEvent (element, event, callback, caller) { 201 | let handler; 202 | element.addEventListener(event, handler = function (e) { 203 | callback.call(caller, e); 204 | }, false); 205 | return handler; 206 | } 207 | 208 | export function removeEvent (element, event, callback) { 209 | element.removeEventListener(event, callback, false); 210 | } 211 | -------------------------------------------------------------------------------- /src/js/ui/pickers/Vec2Picker.js: -------------------------------------------------------------------------------- 1 | import Picker from './Picker'; 2 | import Vector from './types/Vector'; 3 | 4 | export default class Vec2Picker extends Picker { 5 | constructor (pos, properties) { 6 | super('ge_vec2picker_', properties); 7 | 8 | this.width = this.width || 200; 9 | this.height = this.height || 200; 10 | 11 | this.min = this.min || -1; 12 | this.max = this.max || 1; 13 | this.size = this.size || 6; 14 | this.range = this.max - this.min; 15 | this.overPoint = false; 16 | 17 | let center = ((this.range / 2) - this.max) * -1; 18 | this.setValue(pos || [center,center]); 19 | this.create(); 20 | } 21 | 22 | draw () { 23 | this.ctx.clearRect(0, 0, this.width, this.height); 24 | 25 | // frame 26 | this.ctx.strokeStyle = this.dimColor; 27 | this.ctx.lineWidth = 2; 28 | this.ctx.strokeRect(0, 0, this.width, this.height); 29 | 30 | this.ctx.beginPath(); 31 | this.ctx.lineWidth = 0.25; 32 | let sections = 20; 33 | let step = this.width / sections; 34 | for (let i = 0; i < sections; i++) { 35 | this.ctx.moveTo(i * step, 0); 36 | this.ctx.lineTo(i * step, this.height); 37 | this.ctx.moveTo(0, i * step); 38 | this.ctx.lineTo(this.width, i * step); 39 | } 40 | this.ctx.stroke(); 41 | 42 | // horizontal line 43 | this.ctx.strokeStyle = this.dimColor; 44 | this.ctx.lineWidth = 1.0; 45 | this.ctx.beginPath(); 46 | this.ctx.moveTo(0, 0.5 + this.height * 0.5); 47 | this.ctx.lineTo(this.width, 0.5 + this.height * 0.5); 48 | this.ctx.closePath(); 49 | this.ctx.stroke(); 50 | 51 | // vertical line 52 | this.ctx.beginPath(); 53 | this.ctx.moveTo(0.5 + this.width * 0.5, 0); 54 | this.ctx.lineTo(0.5 + this.width * 0.5, this.height); 55 | this.ctx.closePath(); 56 | this.ctx.stroke(); 57 | 58 | // // Triangle line 59 | // this.ctx.fillStyle = this.dimColor; 60 | // this.ctx.beginPath(); 61 | // this.ctx.moveTo(this.width * 0.5, 5); 62 | // this.ctx.lineTo(this.width * 0.48, 0); 63 | // this.ctx.lineTo(this.width * 0.52, 0); 64 | // this.ctx.closePath(); 65 | // this.ctx.fill(); 66 | 67 | let x = Math.round(((this.value.x - this.min) / this.range) * this.width); 68 | let y = Math.round(((1 - (this.value.y - this.min) / this.range)) * this.height); 69 | 70 | let half = this.size / 2; 71 | 72 | if (x < half) { 73 | x = half; 74 | } 75 | if (x > this.width - half) { 76 | x = this.width - half; 77 | } 78 | if (y < half) { 79 | y = half; 80 | } 81 | if (y > this.height - half) { 82 | y = this.height - half; 83 | } 84 | 85 | // point 86 | this.ctx.fillStyle = this.overPoint ? this.selColor : this.fnColor; 87 | this.ctx.beginPath(); 88 | let radius = this.overPoint ? 4 : 2; 89 | this.ctx.arc(x, y, radius, 0, 2 * Math.PI, false); 90 | this.ctx.fill(); 91 | 92 | this.ctx.restore(); 93 | this.overPoint = false; 94 | } 95 | 96 | // Actions when user moves around on HSV color map 97 | onMouseMove (event) { 98 | let x = event.offsetX; 99 | let y = event.offsetY; 100 | 101 | this.value.x = ((this.range / this.width) * x) - (this.range - this.max); 102 | this.value.y = (((this.range / this.height) * y) - (this.range - this.max)) * -1; 103 | 104 | // fire 'changed' 105 | this.trigger('changed', this.value); 106 | this.overPoint = true; 107 | } 108 | 109 | setValue (pos) { 110 | this.value = new Vector(pos); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/js/ui/pickers/Vec3Picker.js: -------------------------------------------------------------------------------- 1 | import Picker from './Picker'; 2 | import Vector from './types/Vector'; 3 | import Matrix from './types/Matrix'; 4 | import { addEvent, removeEvent } from './Picker'; 5 | 6 | export default class Vec3Picker extends Picker { 7 | constructor (dir, properties) { 8 | super('ge_vec3picker_', properties); 9 | 10 | this.width = this.width || 200; 11 | this.height = this.width || 200; 12 | this.scale = 50; 13 | 14 | this.setValue(dir || [0, 0, 1]); 15 | this.create(); 16 | 17 | this.camera = new Matrix(); 18 | this.shapes = []; 19 | this.center = [0, 0, 0]; 20 | 21 | this.shapes.push({ 22 | edgeColour: this.dimColor, 23 | nodes: [[this.width / 2 - 50, this.height / 2, 100], [this.width / 2 + 50, this.height / 2, 100], 24 | [this.width / 2, this.height / 2 - 50, 100], [this.width / 2, this.height / 2 + 50, 100], 25 | [this.width / 2, this.height / 2, 50], [this.width / 2, this.height / 2, 150]], 26 | edges: [[0,1], [2,3], [4,5]] 27 | }); 28 | 29 | this.shapes.push({ 30 | textColour: this.fnColor, 31 | nodes: [[this.width / 2 + 68, this.height / 2, 100], [this.width / 2 - 68, this.height / 2, 100], 32 | [this.width / 2, this.height / 2 + 68, 100], [this.width / 2, this.height / 2 - 68, 100], 33 | [this.width / 2, this.height / 2, 168], [this.width / 2, this.height / 2, 32]], 34 | text: ['x', '-x', 'y', '-y', 'z', '-z'] 35 | }); 36 | 37 | this.setCenter(this.width / 2, this.height / 2, 100); 38 | 39 | // Mouse events 40 | this.dragOffset = [0, 0]; 41 | this.overPoint = false; 42 | } 43 | 44 | setCenter (x, y, z) { 45 | for (let s in this.shapes) { 46 | let shape = this.shapes[s]; 47 | 48 | for (let n in shape.nodes) { 49 | shape.nodes[n][0] -= x; 50 | shape.nodes[n][1] -= y; 51 | shape.nodes[n][2] -= z; 52 | } 53 | } 54 | this.center = [x, y, z]; 55 | } 56 | 57 | viewFromCamera (node) { 58 | let A = this.camera.getMult(node); 59 | A.add(this.center); 60 | return [A.x, this.height - A.y]; 61 | } 62 | 63 | draw () { 64 | this.ctx.clearRect(0, 0, this.width, this.height); 65 | 66 | for (let s in this.shapes) { 67 | let shape = this.shapes[s]; 68 | if (shape.edgeColour) { 69 | this.drawShapeEdges(shape); 70 | } 71 | if (shape.nodeColour) { 72 | this.drawShapeNodes(shape); 73 | } 74 | if (shape.text) { 75 | this.drawShapeText(shape); 76 | } 77 | } 78 | 79 | this.drawShapeEdges({ 80 | edgeColour: this.fnColor, 81 | nodes: [[0,0,0], this.point], 82 | edges: [[0,1]] 83 | }); 84 | 85 | this.drawShapeNodes({ 86 | nodeColour: this.overPoint ? this.selColor : this.fnColor, 87 | nodeRadius: this.overPoint ? 4 : 2, 88 | nodes: [this.point] 89 | }); 90 | } 91 | 92 | drawShapeEdges (shape) { 93 | let nodes = shape.nodes; 94 | 95 | this.ctx.strokeStyle = shape.edgeColour; 96 | for (let e in shape.edges) { 97 | let coord = this.viewFromCamera(nodes[shape.edges[e][0]]); 98 | this.ctx.lineWidth = 1; 99 | this.ctx.beginPath(); 100 | this.ctx.moveTo(coord[0], coord[1]); 101 | coord = this.viewFromCamera(nodes[shape.edges[e][1]]); 102 | this.ctx.lineTo(coord[0], coord[1]); 103 | this.ctx.stroke(); 104 | } 105 | } 106 | 107 | drawShapeNodes (shape) { 108 | let radius = shape.nodeRadius || 4; 109 | this.ctx.fillStyle = shape.nodeColour; 110 | for (let n in shape.nodes) { 111 | let coord = this.viewFromCamera(shape.nodes[n]); 112 | this.ctx.beginPath(); 113 | this.ctx.arc(coord[0], coord[1], radius, 0 , 2 * Math.PI, false); 114 | this.ctx.fill(); 115 | } 116 | } 117 | 118 | drawShapeText (shape) { 119 | this.ctx.fillStyle = shape.textColour; 120 | for (let n in shape.nodes) { 121 | let coord = this.viewFromCamera(shape.nodes[n]); 122 | this.ctx.textBaseline = 'middle'; 123 | this.ctx.fillText(shape.text[n], coord[0], coord[1]); 124 | } 125 | } 126 | 127 | onMouseDown (event) { 128 | let mouse = [event.offsetX, event.offsetY]; 129 | this.dragOffset = mouse; 130 | 131 | let pos = new Vector(this.viewFromCamera(this.point)); 132 | let diff = pos.getSub(mouse); 133 | this.overPoint = diff.getLength() < 10; 134 | 135 | super.onMouseDown(event); 136 | this.onMouseUpHandler = addEvent(this.el, 'dblclick', this.onDbClick, this); 137 | } 138 | 139 | // Actions when user moves around on HSV color map 140 | onMouseMove (event) { 141 | let x = event.offsetX; 142 | let y = event.offsetY; 143 | 144 | var dx = 0.01 * (x - this.dragOffset[0]); 145 | var dy = 0.01 * (y - this.dragOffset[1]); 146 | 147 | if (this.overPoint) { 148 | let invM = this.camera.getInv(); 149 | let vel = invM.getMult([dx, -dy, 0.0]); 150 | vel.mult(2); 151 | this.value.add(vel); 152 | this.point = [this.value.x * this.scale, this.value.y * this.scale, this.value.z * this.scale]; 153 | // fire 'changed' 154 | this.trigger('changed', this.value); 155 | } 156 | else { 157 | this.camera.rotateX(dy); 158 | this.camera.rotateY(dx); 159 | } 160 | 161 | this.dragOffset = [x, y]; 162 | } 163 | 164 | onDbClick (event) { 165 | let mouse = new Vector([event.offsetX, event.offsetY]); 166 | let axis = { 167 | x: [68, 0, 0], 168 | negX: [-68, 0, 0], 169 | y: [0, 68, 100], 170 | negY: [0, -68, 0] 171 | }; 172 | let selected = ''; 173 | for (let i in axis) { 174 | let pos = new Vector(this.viewFromCamera(axis[i])); 175 | let diff = pos.getSub(mouse); 176 | if (diff.getLength() < 10) { 177 | selected = i; 178 | break; 179 | } 180 | } 181 | this.camera = new Matrix(); 182 | 183 | if (selected === 'x') { 184 | this.camera.rotateY(-1.57079632679); 185 | } 186 | else if (selected === 'negX') { 187 | this.camera.rotateY(1.57079632679); 188 | } 189 | else if (selected === 'y') { 190 | this.camera.rotateX(-1.57079632679); 191 | } 192 | else if (selected === 'negY') { 193 | this.camera.rotateX(1.57079632679); 194 | } 195 | 196 | this.draw(); 197 | } 198 | 199 | destroyEvents () { 200 | super.destroyEvents(); 201 | removeEvent(this.el, 'dblclick', this.onDbClick); 202 | this.onMouseMoveHandler = null; 203 | } 204 | 205 | setValue (dir) { 206 | this.value = new Vector(dir); 207 | this.point = [this.value.x * this.scale, this.value.y * this.scale, this.value.z * this.scale]; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/js/ui/pickers/types/Color.js: -------------------------------------------------------------------------------- 1 | import ColorConverter from './ColorConverter'; 2 | import { getColorAsRGB, getValueRanges, getLuminance, limitValue } from './ColorConverter'; 3 | 4 | export default class Color { 5 | constructor (color) { 6 | this.colors = {}; 7 | this.set(color); 8 | } 9 | 10 | set (color, type) { // color only full range 11 | if (typeof color === 'number') { 12 | type = type ? type : 'rgb'; 13 | this.colors[type] = {}; 14 | for (var n = 3; n--;) { 15 | let m = type[n] || type.charAt(n); // IE7 16 | this.colors[type][m] = color; 17 | } 18 | } 19 | else if (typeof color === 'string') { 20 | let parts = color.replace(/(?:#|\)|%)/g, '').split('('); 21 | if (parts[1]) { 22 | let values = (parts[1] || '').split(/,\s*/); 23 | type = type ? type : (parts[1] ? parts[0].substr(0, 3) : 'rgb'); 24 | this.set(values, type); 25 | } 26 | else { 27 | this.set(getColorAsRGB(color), 'rgb'); 28 | } 29 | } 30 | else if (color) { 31 | if (Array.isArray(color)) { 32 | let m = ''; 33 | type = type || 'rgb'; 34 | 35 | this.colors[type] = this.colors[type] || {}; 36 | for (let n = 3; n--;) { 37 | m = type[n] || type.charAt(n); // IE7 38 | let i = color.length >= 3 ? n : 0; 39 | this.colors[type][m] = parseFloat(color[i]); 40 | } 41 | 42 | if (color.length === 4) { 43 | this.colors.alpha = parseFloat(color[3]); 44 | } 45 | } 46 | else if (type) { 47 | for (let n in color) { 48 | this.colors[type][n] = limitValue(color[n] / getValueRanges(type)[n][1], 0, 1) * getValueRanges(type)[n][1]; 49 | } 50 | } 51 | } 52 | 53 | if (!type) { 54 | return; 55 | } 56 | 57 | if (type !== 'rgb') { 58 | var convert = ColorConverter; 59 | this.colors.rgb = convert[type + '2rgb'](this.colors[type]); 60 | } 61 | this.convert(type); 62 | this.colors.hueRGB = ColorConverter.hue2RGB(this.colors.hsv.h); 63 | this.colors.luminance = getLuminance(this.colors.rgb); 64 | } 65 | 66 | convert (type) { 67 | let convert = ColorConverter, 68 | ranges = getValueRanges(), 69 | exceptions = { hsl: 'hsv', cmyk: 'cmy', rgb: type }; 70 | 71 | if (type !== 'alpha') { 72 | for (let typ in ranges) { 73 | if (!ranges[typ][typ]) { // no alpha|HEX 74 | if (type !== typ && typ !== 'XYZ') { 75 | let from = exceptions[typ] || 'rgb'; 76 | this.colors[typ] = convert[from + '2' + typ](this.colors[from]); 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | get (type) { 84 | if (type !== 'rgb') { 85 | var convert = ColorConverter; 86 | this.colors[type] = convert['rgb2' + type](this.colors['rgb']); 87 | return this.colors[type]; 88 | } 89 | else { 90 | return this.colors['rgb']; 91 | } 92 | } 93 | 94 | getString (type) { 95 | if (type === 'HEX') { 96 | var convert = ColorConverter; 97 | return convert['rgb2' + type](this.colors['rgb']); 98 | } 99 | else { 100 | let color = this.get(type); 101 | let str = type, 102 | m = ''; 103 | if (type === 'vec') { 104 | str += this.colors.alpha ? 4 : 3; 105 | } 106 | str += '('; 107 | for (let n = 0; n < 3; n++) { 108 | m = type[n] || type.charAt(n); // IE7 109 | if (type === 'vec') { 110 | str += (color[m]).toFixed(3); 111 | } 112 | else { 113 | str += Math.floor(color[m]); 114 | } 115 | if (n !== 2) { 116 | str += ','; 117 | } 118 | } 119 | 120 | if (this.colors.alpha) { 121 | str += ',' + (this.colors.alpha).toFixed(3); 122 | } 123 | return str += ')'; 124 | } 125 | } 126 | 127 | uniformType () { 128 | if (this.colors.alpha) { 129 | return 'vec4'; 130 | } 131 | return 'vec3'; 132 | } 133 | 134 | uniformValue () { 135 | var vec = this.get('vec'); 136 | var arr = [vec.v, vec.e, vec.c]; 137 | if (this.colors.alpha) { 138 | arr.push(this.colors.alpha); 139 | } 140 | return arr; 141 | } 142 | 143 | uniformMethod (type) { 144 | if (this.colors.alpha) { 145 | return '4f'; 146 | } 147 | return '3f'; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/js/ui/pickers/types/ColorConverter.js: -------------------------------------------------------------------------------- 1 | var valueRanges = { 2 | rgb: { r: [0, 255], g: [0, 255], b: [0, 255] }, 3 | hsv: { h: [0, 1], s: [0, 1], v: [0, 255] }, 4 | hsl: { h: [0, 360], s: [0, 100], l: [0, 100] }, 5 | cmy: { c: [0, 100], m: [0, 100], y: [0, 100] }, 6 | cmyk: { c: [0, 100], m: [0, 100], y: [0, 100], k: [0, 100] }, 7 | Lab: { L: [0, 100], a: [-128, 127], b: [-128, 127] }, 8 | XYZ: { X: [0, 100], Y: [0, 100], Z: [0, 100] }, 9 | vec: { v: [0, 1], e: [0, 1], c: [0, 1] }, 10 | alpha: { alpha: [0, 1] }, 11 | HEX: { HEX: [0, 16777215] } // maybe we don't need this 12 | }; 13 | 14 | // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html for more 15 | var XYZMatrix = { // Observer = 2° (CIE 1931), Illuminant = D65 16 | X: [0.4124564, 0.3575761, 0.1804375], 17 | Y: [0.2126729, 0.7151522, 0.0721750], 18 | Z: [0.0193339, 0.1191920, 0.9503041], 19 | R: [3.2404542, -1.5371385, -0.4985314], 20 | G: [-0.9692660, 1.8760108, 0.0415560], 21 | B: [0.0556434, -0.2040259, 1.0572252] 22 | }; 23 | 24 | var XYZReference = { 25 | X: XYZMatrix.X[0] + XYZMatrix.X[1] + XYZMatrix.X[2], 26 | Y: XYZMatrix.Y[0] + XYZMatrix.Y[1] + XYZMatrix.Y[2], 27 | Z: XYZMatrix.Z[0] + XYZMatrix.Z[1] + XYZMatrix.Z[2] 28 | }; 29 | 30 | var luminance = { r: 0.2126, g: 0.7152, b: 0.0722 }; // W3C 2.0 31 | 32 | var _colors; 33 | 34 | export default class ColorConverter { 35 | // ------------------------ VEC ------------------------ // 36 | static vec2rgb (vec) { 37 | return { 38 | r: vec.v * valueRanges['rgb']['r'][1], 39 | g: vec.e * valueRanges['rgb']['g'][1], 40 | b: vec.c * valueRanges['rgb']['b'][1] 41 | }; 42 | } 43 | 44 | static rgb2vec (rgb) { 45 | return { 46 | v: rgb.r / valueRanges['rgb']['r'][1], 47 | e: rgb.g / valueRanges['rgb']['g'][1], 48 | c: rgb.b / valueRanges['rgb']['b'][1] 49 | }; 50 | } 51 | 52 | // ------------------------ HEX ------------------------ // 53 | 54 | static RGB2HEX (rgb) { 55 | return ( 56 | (rgb.r < 16 ? '0' : '') + rgb.r.toString(16) + 57 | (rgb.g < 16 ? '0' : '') + rgb.g.toString(16) + 58 | (rgb.b < 16 ? '0' : '') + rgb.b.toString(16) 59 | ).toUpperCase(); 60 | } 61 | 62 | static HEX2rgb (HEX) { 63 | HEX = HEX.split(''); // IE7 64 | return { 65 | r: parseInt(HEX[0] + HEX[HEX[3] ? 1 : 0], 16) / 255, 66 | g: parseInt(HEX[HEX[3] ? 2 : 1] + (HEX[3] || HEX[1]), 16) / 255, 67 | b: parseInt((HEX[4] || HEX[2]) + (HEX[5] || HEX[2]), 16) / 255 68 | }; 69 | } 70 | 71 | // ------------------------ HUE ------------------------ // 72 | 73 | static hue2RGB (hue) { 74 | var h = hue * 6, 75 | // mod = ~~h % 6, // Math.floor(h) -> faster in most browsers 76 | mod = Math.floor(h), 77 | i = h === 6 ? 0 : (h - mod); 78 | return { 79 | r: Math.round([1, 1 - i, 0, 0, i, 1][mod] * 255), 80 | g: Math.round([i, 1, 1, 1 - i, 0, 0][mod] * 255), 81 | b: Math.round([0, 0, i, 1, 1, 1 - i][mod] * 255) 82 | }; 83 | } 84 | 85 | // ------------------------ HSV ------------------------ // 86 | 87 | static rgb2hsv (rgb) { // faster 88 | var r = rgb.r, 89 | g = rgb.g, 90 | b = rgb.b, 91 | k = 0, 92 | chroma, 93 | min, 94 | s; 95 | 96 | if (g < b) { 97 | g = b + (b = g, 0); 98 | k = -1; 99 | } 100 | min = b; 101 | if (r < g) { 102 | r = g + (g = r, 0); 103 | k = -2 / 6 - k; 104 | min = Math.min(g, b); // g < b ? g : b; ??? 105 | } 106 | chroma = r - min; 107 | s = r ? (chroma / r) : 0; 108 | return { 109 | h: s < 1e-15 ? ((_colors && _colors.hsl && _colors.hsl.h) || 0) : 110 | chroma ? Math.abs(k + (g - b) / (6 * chroma)) : 0, 111 | s: r ? (chroma / r) : ((_colors && _colors.hsv && _colors.hsv.s) || 0), // ??_colors.hsv.s || 0 112 | v: r 113 | }; 114 | } 115 | 116 | static hsv2rgb (hsv) { 117 | var h = hsv.h * 6, 118 | s = hsv.s, 119 | v = hsv.v, 120 | // i = ~~h, // Math.floor(h) -> faster in most browsers 121 | i = Math.floor(h), 122 | f = h - i, 123 | p = v * (1 - s), 124 | q = v * (1 - f * s), 125 | t = v * (1 - (1 - f) * s), 126 | mod = i % 6; 127 | 128 | return { 129 | r: [v, q, p, p, t, v][mod], 130 | g: [t, v, v, q, p, p][mod], 131 | b: [p, p, t, v, v, q][mod] 132 | }; 133 | } 134 | 135 | // ------------------------ HSL ------------------------ // 136 | 137 | static hsv2hsl (hsv) { 138 | var l = (2 - hsv.s) * hsv.v, 139 | s = hsv.s * hsv.v; 140 | 141 | s = !hsv.s ? 0 : l < 1 ? (l ? s / l : 0) : s / (2 - l); 142 | 143 | return { 144 | h: hsv.h, 145 | s: !hsv.v && !s ? ((_colors && _colors.hsl && _colors.hsl.s) || 0) : s, // ??? 146 | l: l / 2 147 | }; 148 | } 149 | 150 | static rgb2hsl (rgb, dependent) { // not used in Color 151 | var hsv = ColorConverter.rgb2hsv(rgb); 152 | 153 | return ColorConverter.hsv2hsl(dependent ? hsv : (_colors.hsv = hsv)); 154 | } 155 | 156 | static hsl2rgb (hsl) { 157 | var h = hsl.h * 6, 158 | s = hsl.s, 159 | l = hsl.l, 160 | v = l < 0.5 ? l * (1 + s) : (l + s) - (s * l), 161 | m = l + l - v, 162 | sv = v ? ((v - m) / v) : 0, 163 | // sextant = ~~h, // Math.floor(h) -> faster in most browsers 164 | sextant = Math.floor(h), 165 | fract = h - sextant, 166 | vsf = v * sv * fract, 167 | t = m + vsf, 168 | q = v - vsf, 169 | mod = sextant % 6; 170 | 171 | return { 172 | r: [v, q, m, m, t, v][mod], 173 | g: [t, v, v, q, m, m][mod], 174 | b: [m, m, t, v, v, q][mod] 175 | }; 176 | } 177 | 178 | // ------------------------ CMYK ------------------------ // 179 | // Quote from Wikipedia: 180 | // 'Since RGB and CMYK spaces are both device-dependent spaces, there is no 181 | // simple or general conversion formula that converts between them. 182 | // Conversions are generally done through color management systems, using 183 | // color profiles that describe the spaces being converted. Nevertheless, the 184 | // conversions cannot be exact, since these spaces have very different gamuts.' 185 | // Translation: the following are just simple RGB to CMY(K) and visa versa conversion functions. 186 | 187 | static rgb2cmy (rgb) { 188 | return { 189 | c: 1 - rgb.r, 190 | m: 1 - rgb.g, 191 | y: 1 - rgb.b 192 | }; 193 | } 194 | 195 | static cmy2cmyk (cmy) { 196 | var k = Math.min(Math.min(cmy.c, cmy.m), cmy.y), 197 | t = 1 - k || 1e-20; 198 | 199 | return { // regular 200 | c: (cmy.c - k) / t, 201 | m: (cmy.m - k) / t, 202 | y: (cmy.y - k) / t, 203 | k: k 204 | }; 205 | } 206 | 207 | static cmyk2cmy (cmyk) { 208 | var k = cmyk.k; 209 | 210 | return { // regular 211 | c: cmyk.c * (1 - k) + k, 212 | m: cmyk.m * (1 - k) + k, 213 | y: cmyk.y * (1 - k) + k 214 | }; 215 | } 216 | 217 | static cmy2rgb (cmy) { 218 | return { 219 | r: 1 - cmy.c, 220 | g: 1 - cmy.m, 221 | b: 1 - cmy.y 222 | }; 223 | } 224 | 225 | static rgb2cmyk (rgb) { 226 | var cmy = ColorConverter.rgb2cmy(rgb); // doppelt?? 227 | return ColorConverter.cmy2cmyk(cmy); 228 | } 229 | 230 | static cmyk2rgb (cmyk) { 231 | var cmy = ColorConverter.cmyk2cmy(cmyk); // doppelt?? 232 | return ColorConverter.cmy2rgb(cmy); 233 | } 234 | 235 | // ------------------------ LAB ------------------------ // 236 | 237 | static XYZ2rgb (XYZ) { 238 | var M = XYZMatrix, 239 | X = XYZ.X, 240 | Y = XYZ.Y, 241 | Z = XYZ.Z, 242 | r = X * M.R[0] + Y * M.R[1] + Z * M.R[2], 243 | g = X * M.G[0] + Y * M.G[1] + Z * M.G[2], 244 | b = X * M.B[0] + Y * M.B[1] + Z * M.B[2], 245 | N = 1 / 2.4; 246 | 247 | M = 0.0031308; 248 | 249 | r = (r > M ? 1.055 * Math.pow(r, N) - 0.055 : 12.92 * r); 250 | g = (g > M ? 1.055 * Math.pow(g, N) - 0.055 : 12.92 * g); 251 | b = (b > M ? 1.055 * Math.pow(b, N) - 0.055 : 12.92 * b); 252 | 253 | return { 254 | r: limitValue(r, 0, 1), 255 | g: limitValue(g, 0, 1), 256 | b: limitValue(b, 0, 1) 257 | }; 258 | } 259 | 260 | static rgb2XYZ (rgb) { 261 | var M = XYZMatrix, 262 | r = rgb.r, 263 | g = rgb.g, 264 | b = rgb.b, 265 | N = 0.04045; 266 | 267 | r = (r > N ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92); 268 | g = (g > N ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92); 269 | b = (b > N ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92); 270 | 271 | return { 272 | X: r * M.X[0] + g * M.X[1] + b * M.X[2], 273 | Y: r * M.Y[0] + g * M.Y[1] + b * M.Y[2], 274 | Z: r * M.Z[0] + g * M.Z[1] + b * M.Z[2] 275 | }; 276 | } 277 | 278 | static XYZ2Lab (XYZ) { 279 | var R = XYZReference, 280 | X = XYZ.X / R.X, 281 | Y = XYZ.Y / R.Y, 282 | Z = XYZ.Z / R.Z, 283 | N = 16 / 116, 284 | M = 1 / 3, 285 | K = 0.008856, 286 | L = 7.787037; 287 | 288 | X = X > K ? Math.pow(X, M) : (L * X) + N; 289 | Y = Y > K ? Math.pow(Y, M) : (L * Y) + N; 290 | Z = Z > K ? Math.pow(Z, M) : (L * Z) + N; 291 | 292 | return { 293 | L: (116 * Y) - 16, 294 | a: 500 * (X - Y), 295 | b: 200 * (Y - Z) 296 | }; 297 | } 298 | 299 | static Lab2XYZ (Lab) { 300 | var R = XYZReference, 301 | Y = (Lab.L + 16) / 116, 302 | X = Lab.a / 500 + Y, 303 | Z = Y - Lab.b / 200, 304 | X3 = Math.pow(X, 3), 305 | Y3 = Math.pow(Y, 3), 306 | Z3 = Math.pow(Z, 3), 307 | N = 16 / 116, 308 | K = 0.008856, 309 | L = 7.787037; 310 | 311 | return { 312 | X: (X3 > K ? X3 : (X - N) / L) * R.X, 313 | Y: (Y3 > K ? Y3 : (Y - N) / L) * R.Y, 314 | Z: (Z3 > K ? Z3 : (Z - N) / L) * R.Z 315 | }; 316 | } 317 | 318 | static rgb2Lab (rgb) { 319 | var XYZ = ColorConverter.rgb2XYZ(rgb); 320 | 321 | return ColorConverter.XYZ2Lab(XYZ); 322 | } 323 | 324 | static Lab2rgb (Lab) { 325 | var XYZ = ColorConverter.Lab2XYZ(Lab); 326 | 327 | return ColorConverter.XYZ2rgb(XYZ); 328 | } 329 | } 330 | 331 | export function limitValue(value, min, max) { 332 | // return Math.max(min, Math.min(max, value)); // faster?? 333 | return (value > max ? max : value < min ? min : value); 334 | } 335 | 336 | export function getLuminance(rgb, normalized) { 337 | var div = normalized ? 1 : 255, 338 | RGB = [rgb.r / div, rgb.g / div, rgb.b / div]; 339 | 340 | for (var i = RGB.length; i--;) { 341 | RGB[i] = RGB[i] <= 0.03928 ? RGB[i] / 12.92 : Math.pow(((RGB[i] + 0.055) / 1.055), 2.4); 342 | } 343 | return ((luminance.r * RGB[0]) + (luminance.g * RGB[1]) + (luminance.b * RGB[2])); 344 | } 345 | 346 | export function getColorAsRGB (color) { 347 | // Create a test element to apply a CSS color and retrieve 348 | // a normalized value from. 349 | let test = document.createElement('div'); 350 | test.style.backgroundColor = color; 351 | 352 | // Chrome requires the element to be in DOM for styles to be computed. 353 | document.body.appendChild(test); 354 | 355 | // Get the computed style from the browser, in the format of 356 | // rgb(x, x, x) 357 | let normalized = window.getComputedStyle(test).backgroundColor; 358 | 359 | // In certain cases getComputedStyle() may return 360 | // 'transparent' as a value, which is useless(?) for the current 361 | // color picker. According to specifications, transparent 362 | // is a black with 0 alpha - rgba(0, 0, 0, 0) - but because 363 | // the picker does not currently handle alpha, we return the 364 | // black value. 365 | if (normalized === 'transparent') { 366 | normalized = 'rgb(0, 0, 0)'; 367 | } 368 | 369 | // Garbage collection 370 | test.parentNode.removeChild(test); 371 | 372 | return normalized; 373 | } 374 | 375 | export function getValueRanges(type) { 376 | if (!type) { 377 | return valueRanges; 378 | } 379 | else { 380 | return valueRanges[type]; 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/js/ui/pickers/types/Float.js: -------------------------------------------------------------------------------- 1 | 2 | export default class Float { 3 | constructor (value) { 4 | this.value = value; 5 | } 6 | 7 | getString () { 8 | return this.value.toFixed(3); 9 | } 10 | 11 | uniformType () { 12 | return 'float'; 13 | } 14 | 15 | uniformValue () { 16 | return [this.value]; 17 | } 18 | 19 | uniformMethod () { 20 | return '1f'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/js/ui/pickers/types/Matrix.js: -------------------------------------------------------------------------------- 1 | import Vector from './Vector'; 2 | 3 | // TODO: 4 | // - generalize this for mat2, mat3 and mat4 5 | 6 | export default class Matrix { 7 | constructor(m, type) { 8 | this.dim = 3; 9 | this.value = [ 10 | [1, 0, 0], 11 | [0, 1, 0], 12 | [0, 0, 1]]; 13 | if (m) { 14 | this.set(m, type); 15 | } 16 | } 17 | 18 | set (m, type) { 19 | if (m.value[0][0]) { 20 | this.value = m.value; 21 | this.dim = m.dim; 22 | } 23 | else if (m[0][0]) { 24 | this.value = m; 25 | } 26 | } 27 | 28 | rotateX (theta) { 29 | let c = Math.cos(theta); 30 | let s = Math.sin(theta); 31 | let T = [ 32 | [1, 0, 0], 33 | [0, c, -s], 34 | [0, s, c]]; 35 | 36 | this.value = this.getTransform(T); 37 | } 38 | 39 | rotateY (theta) { 40 | let c = Math.cos(theta); 41 | let s = Math.sin(theta); 42 | let T = [ 43 | [c, 0, s], 44 | [0, 1, 0], 45 | [-s, 0, c]]; 46 | 47 | this.value = this.getTransform(T); 48 | } 49 | 50 | getMult (v) { 51 | if (v[0][0] || (v.value && v.value[0][0])) { 52 | // TODO: what If v is a matrix 53 | console.log('TODO: what If v is a matrix'); 54 | } 55 | else { 56 | // If v is a vector 57 | let A = new Vector(v); 58 | let B = []; 59 | for (let i = 0; i < A.dim; i++) { 60 | B.push(A.value[0] * this.value[i][0] + A.value[1] * this.value[i][1] + A.value[2] * this.value[i][2]); 61 | } 62 | return new Vector(B); 63 | } 64 | } 65 | 66 | getTransform (m) { 67 | let newMatrix = []; 68 | for (let row in m) { 69 | let t = m[row]; 70 | let newRow = []; 71 | newRow.push(t[0] * this.value[0][0] + t[1] * this.value[1][0] + t[2] * this.value[2][0]); 72 | newRow.push(t[0] * this.value[0][1] + t[1] * this.value[1][1] + t[2] * this.value[2][1]); 73 | newRow.push(t[0] * this.value[0][2] + t[1] * this.value[1][2] + t[2] * this.value[2][2]); 74 | newMatrix.push(newRow); 75 | } 76 | return newMatrix; 77 | } 78 | 79 | getInv() { 80 | let M = new Matrix(); 81 | let determinant = this.value[0][0] * (this.value[1][1] * this.value[2][2] - this.value[2][1] * this.value[1][2]) - 82 | this.value[0][1] * (this.value[1][0] * this.value[2][2] - this.value[1][2] * this.value[2][0]) + 83 | this.value[0][2] * (this.value[1][0] * this.value[2][1] - this.value[1][1] * this.value[2][0]); 84 | let invdet = 1 / determinant; 85 | M.value[0][0] = (this.value[1][1] * this.value[2][2] - this.value[2][1] * this.value[1][2]) * invdet; 86 | M.value[0][1] = -(this.value[0][1] * this.value[2][2] - this.value[0][2] * this.value[2][1]) * invdet; 87 | M.value[0][2] = (this.value[0][1] * this.value[1][2] - this.value[0][2] * this.value[1][1]) * invdet; 88 | M.value[1][0] = -(this.value[1][0] * this.value[2][2] - this.value[1][2] * this.value[2][0]) * invdet; 89 | M.value[1][1] = (this.value[0][0] * this.value[2][2] - this.value[0][2] * this.value[2][0]) * invdet; 90 | M.value[1][2] = -(this.value[0][0] * this.value[1][2] - this.value[1][0] * this.value[0][2]) * invdet; 91 | M.value[2][0] = (this.value[1][0] * this.value[2][1] - this.value[2][0] * this.value[1][1]) * invdet; 92 | M.value[2][1] = -(this.value[0][0] * this.value[2][1] - this.value[2][0] * this.value[0][1]) * invdet; 93 | M.value[2][2] = (this.value[0][0] * this.value[1][1] - this.value[1][0] * this.value[0][1]) * invdet; 94 | return M; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/js/ui/pickers/types/Vector.js: -------------------------------------------------------------------------------- 1 | export default class Vector { 2 | constructor (vec, type) { 3 | this.value = [0,0]; 4 | this.dim = 2; 5 | this.set(vec, type); 6 | } 7 | 8 | set (vec, type) { 9 | if (typeof vec === 'number') { 10 | type = type || 'vec2'; 11 | this.set([vec], type); 12 | } 13 | else if (typeof vec === 'string') { 14 | let parts = vec.replace(/(?:#|\)|\]|%)/g, '').split('('); 15 | let strValues = (parts[1] || parts[0].replace(/(\[)/g, '')).split(/,\s*/); 16 | type = type || (parts[1] ? parts[0].substr(0, 4) : 'vec' + strValues.length); 17 | let values = []; 18 | for (let i in strValues) { 19 | values.push(parseFloat(strValues[i])); 20 | } 21 | this.set(values, type); 22 | } 23 | else if (vec) { 24 | if (Array.isArray(vec)) { 25 | this.value = []; 26 | this.value.length = 0; 27 | this.dim = type ? Number(type.substr(3, 4)) : vec.length; 28 | let filler = vec.length === 1 ? vec[0] : 0; 29 | for (let i = 0; i < this.dim; i++) { 30 | this.value.push(vec[i] || filler); 31 | } 32 | } 33 | else if (vec.dim) { 34 | this.value = vec.value; 35 | this.dim = vec.dim; 36 | } 37 | } 38 | } 39 | 40 | set x (v) { 41 | this.value[0] = v; 42 | } 43 | 44 | set y (v) { 45 | this.value[1] = v; 46 | } 47 | 48 | set z (v) { 49 | if (this.dim < 3) { 50 | while (this.dim < 3) { 51 | this.value.push(0); 52 | } 53 | this.dim = 3; 54 | } 55 | this.value[2] = v; 56 | } 57 | 58 | set w (v) { 59 | if (this.dim < 4) { 60 | while (this.dim < 4) { 61 | this.value.push(0); 62 | } 63 | this.dim = 4; 64 | } 65 | this.value[3] = v; 66 | } 67 | 68 | get x () { 69 | return this.value[0] || 0.0; 70 | } 71 | 72 | get y () { 73 | return this.value[1] || 0.0; 74 | } 75 | 76 | get z () { 77 | return this.value[2] || 0.0 ; 78 | } 79 | 80 | get w () { 81 | return this.value[3] || 0.0; 82 | } 83 | 84 | getString(type) { 85 | type = type || 'vec' + this.dim; 86 | 87 | let len = this.dim; 88 | let str = ''; 89 | let head = type + '('; 90 | let end = ')'; 91 | 92 | if (type === 'array') { 93 | head = '['; 94 | end = ']'; 95 | len = this.dim; 96 | } 97 | else { 98 | len = Number(type.substr(3, 4)); 99 | } 100 | 101 | str = head; 102 | for (let i = 0; i < len; i++) { 103 | str += this.value[i].toFixed(3); 104 | if (i !== len - 1) { 105 | str += ','; 106 | } 107 | } 108 | return str += end; 109 | } 110 | 111 | uniformType () { 112 | return 'vec' + this.dim; 113 | } 114 | 115 | uniformValue () { 116 | var arr = []; 117 | for (let i = 0; i < this.dim; i++) { 118 | arr.push(this.value[i]); 119 | } 120 | return arr; 121 | } 122 | 123 | uniformMethod () { 124 | return this.dim + 'f'; 125 | } 126 | 127 | // VECTOR OPERATIONS 128 | 129 | add (v) { 130 | if (typeof v === 'number') { 131 | for (let i = 0; i < this.dim; i++) { 132 | this.value[i] = this.value[i] + v; 133 | } 134 | } 135 | else { 136 | let A = new Vector(v); 137 | let lim = Math.min(this.dim, A.dim); 138 | for (let i = 0; i < lim; i++) { 139 | this.value[i] = this.value[i] + A.value[i]; 140 | } 141 | } 142 | } 143 | 144 | sub (v) { 145 | if (typeof v === 'number') { 146 | for (let i = 0; i < this.dim; i++) { 147 | this.value[i] = this.value[i] - v; 148 | } 149 | } 150 | else { 151 | let A = new Vector(v); 152 | let lim = Math.min(this.dim, A.dim); 153 | for (let i = 0; i < lim; i++) { 154 | this.value[i] = this.value[i] - A.value[i]; 155 | } 156 | } 157 | } 158 | 159 | mult (v) { 160 | if (typeof v === 'number') { 161 | // Mulitply by scalar 162 | for (let i = 0; i < this.dim; i++) { 163 | this.value[i] = this.value[i] * v; 164 | } 165 | } 166 | else { 167 | // Multiply two vectors 168 | let A = new Vector(v); 169 | let lim = Math.min(this.dim, A.dim); 170 | for (let i = 0; i < lim; i++) { 171 | this.value[i] = this.value[i] * A.value[i]; 172 | } 173 | } 174 | } 175 | 176 | div (v) { 177 | if (typeof v === 'number') { 178 | // Mulitply by scalar 179 | for (let i = 0; i < this.dim; i++) { 180 | this.value[i] = this.value[i] / v; 181 | } 182 | } 183 | else { 184 | // Multiply two vectors 185 | let A = new Vector(v); 186 | let lim = Math.min(this.dim, A.dim); 187 | for (let i = 0; i < lim; i++) { 188 | this.value[i] = this.value[i] / A.value[i]; 189 | } 190 | } 191 | } 192 | 193 | normalize () { 194 | let l = this.getLength(); 195 | this.div(l); 196 | } 197 | 198 | getAdd (v) { 199 | var A = new Vector(this); 200 | A.add(v); 201 | return A; 202 | } 203 | 204 | getSub (v) { 205 | var A = new Vector(this); 206 | A.sub(v); 207 | return A; 208 | } 209 | 210 | getMult (v) { 211 | var A = new Vector(this); 212 | A.mult(v); 213 | return A; 214 | } 215 | 216 | getDiv (v) { 217 | var A = new Vector(this); 218 | A.div(v); 219 | return A; 220 | } 221 | 222 | getLengthSq () { 223 | if (this.dim === 2) { 224 | return (this.value[0] * this.value[0] + this.value[1] * this.value[1]); 225 | } 226 | else { 227 | return (this.value[0] * this.value[0] + this.value[1] * this.value[1] + this.value[2] * this.value[2]); 228 | } 229 | } 230 | 231 | getLength () { 232 | return Math.sqrt(this.getLengthSq()); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | // Module to control application life. 3 | const app = electron.app; 4 | const BrowserWindow = electron.BrowserWindow; 5 | var Menu = electron.Menu; 6 | var mainWindow = null; 7 | 8 | const path = require('path'); 9 | const url = require('url'); 10 | 11 | // Keep a global reference of the window object, if you don't, the window will 12 | // be closed automatically when the JavaScript object is garbage collected. 13 | 14 | 15 | function createWindow () { 16 | // Create the browser window. 17 | mainWindow = new BrowserWindow({ 18 | title: 'GlslEditor', 19 | width: 1000, 20 | minWidth: 700, 21 | height: 700, 22 | minHeight: 700 23 | }); 24 | mainWindow.setTitle('GlslEditor'); 25 | // and load the index.html of the app. 26 | mainWindow.loadURL(url.format({ 27 | pathname: path.join(__dirname, 'index.html'), 28 | protocol: 'file:', 29 | slashes: true 30 | })); 31 | 32 | // Emitted when the window is closed. 33 | mainWindow.on('closed', function () { 34 | // Dereference the window object, usually you would store windows 35 | // in an array if your app supports multi windows, this is the time 36 | // when you should delete the corresponding element. 37 | mainWindow = null; 38 | }); 39 | } 40 | 41 | // This method will be called when Electron has finished 42 | // initialization and is ready to create browser windows. 43 | // Some APIs can only be used after this event occurs. 44 | app.on('ready', createWindow); 45 | 46 | // Quit when all windows are closed. 47 | app.on('window-all-closed', function () { 48 | app.quit(); 49 | }); 50 | 51 | app.on('activate', function () { 52 | // On OS X it's common to re-create a window in the app when the 53 | // dock icon is clicked and there are no other windows open. 54 | if (mainWindow === null) { 55 | createWindow(); 56 | } 57 | }); 58 | 59 | --------------------------------------------------------------------------------