├── .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 | 
4 |
5 | [](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 | 
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 | 
72 |
73 | - Inline Trackpad for '''vec2'''
74 |
75 | 
76 |
77 | - Slider for floats
78 |
79 | - Inline error display
80 |
81 | 
82 |
83 | - Breakpoints for variables
84 |
85 | 
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 |
--------------------------------------------------------------------------------