PageRenderTime 88ms CodeModel.GetById 17ms app.highlight 66ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/views/shader_editor_view.js

https://github.com/ajanthanm/cssfilterlab
JavaScript | 631 lines | 512 code | 97 blank | 22 comment | 62 complexity | 60973935ee57c20c5793aab610f80bfe MD5 | raw file
  1/*
  2 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
  3 * 
  4 * Licensed under the Apache License, Version 2.0 (the "License");
  5 * you may not use this file except in compliance with the License.
  6 * You may obtain a copy of the License at
  7 * 
  8 *     http://www.apache.org/licenses/LICENSE-2.0
  9 * 
 10 * Unless required by applicable law or agreed to in writing, software
 11 * distributed under the License is distributed on an "AS IS" BASIS,
 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 * See the License for the specific language governing permissions and
 14 * limitations under the License.
 15 */
 16
 17(function() {
 18    function ShaderEditorView(filterList, github) {
 19        this.filterList = filterList;
 20        this.github = github;
 21
 22        this.configEditorEl = $("#config-editor");
 23        this.shaderNameEl = this.configEditorEl.find(".config-shader-name");
 24        this.shaderMixBlendModeEl = this.configEditorEl.find(".config-shader-mix-blend-mode");
 25        this.shaderMeshColumnsEl = this.configEditorEl.find(".config-shader-mesh-columns");
 26        this.shaderMeshRowsEl = this.configEditorEl.find(".config-shader-mesh-rows");
 27        this.configParametersEl = this.configEditorEl.find(".config-parameters");
 28        this.configParametersWrapperEl = this.configEditorEl.find(".config-parameters-wrapper");
 29        this.configParameterEl = this.configEditorEl.find(".config-parameter").detach();
 30
 31        this.vertexShaderEditorEl = $("#vertex-shader .code-view");
 32        this.fragmentShaderEditorEl = $("#fragment-shader .code-view");
 33
 34        this.errorsEl = $("#shader-errors");
 35        this.vertexShaderErrorsEl = $("#shader-errors .vertex-shader");
 36        this.fragmentShaderErrorsEl = $("#shader-errors .fragment-shader");
 37
 38        // error for missing CodeMirror lib
 39        this.codeViewErrorEl = $("#code-view-error");
 40
 41        this.saveToGistEl = $("#post-gist-button");
 42        this.saveToGistEl.click(this.onSaveToGistClicked.bind(this));   
 43
 44        this.githubPopupEl = $("#github-popup");
 45        this.githubLinkEl = $("#gist-link");
 46        this.githubIframeEl = $("#github-login");
 47        this.githubPublicGistEl = $("#save-public-gist");
 48        this.githubPopupCloseEl = this.githubPopupEl.find(".gist-popup-close");
 49        this.githubPopupCloseEl.click(this.onCloseGistPopupClicked.bind(this));   
 50        this.githubSaveGistWithLoginEL = $("#save-gist-with-login");
 51        this.githubSaveGistWithLoginEL.click(this.onSaveGistWithLoginClicked.bind(this));
 52        this.githubLoginLinkEl = $("#github-login-link");
 53        this.githubLoginLinkEl.click(this.onGithubLoginLinkClicked.bind(this));
 54        this.githubSaveAnonymousGistEl = $("#save-anonymous-gist");
 55        this.githubSaveAnonymousGistEl.click(this.onSaveAnonymousGistClicked.bind(this));   
 56
 57        this.loaderView = new Global.LoadingProgressView($("#shader-errors .loader"));
 58        this.angleLib = new Global.AngleLib();
 59        this.angleLib.on("progress", this.onAngleLibLoadingProgress.bind(this));
 60        this.angleLib.on("completed", this.onAngleLibLoadingCompleted.bind(this));
 61
 62        this.vertexShaderEditor = new Global.ShaderCodeEditorView(this.angleLib, "vertex",
 63                this.vertexShaderEditorEl, this.vertexShaderErrorsEl);
 64
 65        this.fragmentShaderEditor = new Global.ShaderCodeEditorView(this.angleLib, "fragment",
 66                this.fragmentShaderEditorEl, this.fragmentShaderErrorsEl);
 67        
 68        // Use the timer to avoid updating the shader after each keystroke.
 69        this.shaderTextChangeTimers = {
 70            "vertex": new Global.Timer(300),
 71            "fragment": new Global.Timer(300)
 72        };
 73        this.shaderTextChangeTimers["vertex"].on("timerFired", this.onShaderTextChangedTimerFired.bind(this, "vertex"));
 74        this.shaderTextChangeTimers["fragment"].on("timerFired", this.onShaderTextChangedTimerFired.bind(this, "fragment"));
 75
 76        this.shaderConfigChangeTimer = new Global.Timer(300);
 77        this.shaderConfigChangeTimer.on("timerFired", this._filterModelChanged.bind(this));
 78
 79        this.vertexShaderEditor.on("valueChanged", this.onShaderTextChanged.bind(this, "vertex"));
 80        this.vertexShaderEditor.on("uniformsDetected", this.onShaderUniformsDetected.bind(this, "vertex"));
 81
 82        this.fragmentShaderEditor.on("valueChanged", this.onShaderTextChanged.bind(this, "fragment"));
 83        this.fragmentShaderEditor.on("uniformsDetected", this.onShaderUniformsDetected.bind(this, "fragment"));
 84
 85
 86        this.configDockPanel = new Global.DockPanel("Filter Configuration");
 87        $("#config-editor").appendTo(this.configDockPanel.el);
 88
 89        this.vertexDockPanel = new Global.DockPanel("Vertex Shader");
 90        $("#vertex-shader").appendTo(this.vertexDockPanel.el);
 91        this.vertexDockPanel.on("activeStateChanged", this.onActiveStateChanged.bind(this, this.vertexShaderEditor));
 92
 93        this.fragmentDockPanel = new Global.DockPanel("Fragment Shader");
 94        $("#fragment-shader").appendTo(this.fragmentDockPanel.el);
 95        this.fragmentDockPanel.on("activeStateChanged", this.onActiveStateChanged.bind(this, this.fragmentShaderEditor));
 96        
 97        this.errorsDockPanel = new Global.DockPanel("Errors");
 98        this.errorsEl.appendTo(this.errorsDockPanel.el);
 99        
100        this.onWindowResizedCallback = this.onWindowResized.bind(this);
101        this.onDockResizedCallback = this.onDockResized.bind(this);
102
103        this.panelsAdded = false;
104        this.filterModel = null;
105    }
106
107    ShaderEditorView.prototype = {
108        
109        onAngleLibLoadingProgress: function(value) {
110            this.loaderView.setValue(value);
111        },
112
113        onAngleLibLoadingCompleted: function() {
114            this.loaderView.hide();
115        },
116
117        show: function() {
118            if (!this.panelsAdded) {
119                this.panelsAdded = true;
120                
121                this.editorColumn = this.dockView.addVerticalColumn()
122
123                this.editorColumn.addCloseButton()
124                        .setMinSize(400)
125                        .setWidth(600)
126                        .addContainer()
127                            .add(this.configDockPanel)
128                            .add(this.vertexDockPanel)
129                            .add(this.fragmentDockPanel)
130                        .column
131                            .addContainer()
132                                .setHeight(100)
133                                .add(this.errorsDockPanel)
134                        .column
135                            .on("columnClosed", this.onColumnClosed.bind(this))
136                                                                               
137                // move the github save button in the tablist
138                this.editorColumn.items[0].tabListEl.append(this.saveToGistEl);
139                
140                $("body").bind("dockViewResized", this.onDockResizedCallback);
141                window.addEventListener("resize", this.onWindowResizedCallback, false);
142            }
143        },
144        
145        hide: function(){
146            this.editorColumn.onColumnCloseButtonClicked()
147        },
148        
149        onColumnClosed: function() {
150            $("body").unbind("columnResized", this.onDockResizedCallback);
151            window.removeEventListener("resize", this.onWindowResizedCallback, false);
152            this.panelsAdded = false;
153            this.configDockPanel.setActive(false);
154            this.vertexDockPanel.setActive(false);
155            this.fragmentDockPanel.setActive(false);
156        },
157
158        updateModel: function(filterModel) {
159            var self = this;
160
161            this.configEditorEl.find(".config-parameter").remove();
162
163            this.filterModel = new Global.ActiveObject(filterModel);
164
165            this.updateTitle();
166            
167            this.filterModel
168                .bindToTextInput("label", this.shaderNameEl);
169
170            var mix = this.filterModel.get("mix");
171            if (!mix)
172                mix = this.filterModel.set("mix", {"blendMode": "normal"});
173            mix.bindToSelectInput("blendMode", this.shaderMixBlendModeEl);
174            
175            var mesh = this.filterModel.get("mesh");
176            if (!mesh)
177                mesh = this.filterModel.set("mesh", {"columns": 1, "rows": 1});
178            mesh.bindToSelectInput("columns", this.shaderMeshColumnsEl)
179                .bindToSelectInput("rows", this.shaderMeshRowsEl);
180
181            var params = this.filterModel.get("params");
182            self.configParametersEl.empty();
183
184            var paramsByKey = {};
185            var first = true;
186            $.each(params.properties, function(key, value) {
187                var item = self.createParameterItem(key, first);
188                paramsByKey["_" + key] = item;
189                first = false;
190            });
191            params.on("propertyAdded", function(key, value) {
192                var item = self.createParameterItem(key);
193                paramsByKey["_" + key] = item;
194            });
195            params.on("propertyRemoved", function(key) {
196                var item = paramsByKey["_" + key];
197                delete paramsByKey["_" + key];
198                if (!item)
199                    return;
200                if (self._activeParameter == key)
201                    self.configEditorEl.find(".config-parameter").remove();
202                item.remove();
203            });
204            
205            this.filterModel.on("rootValueChanged", this.filterModelChanged.bind(this));
206        },
207
208        createParameterItem: function(key, first) {
209            var el = $("<li />")
210                .text(key)
211                .appendTo(this.configParametersEl);
212            el.click(this.onParamterItemClicked.bind(this, el, key));
213            if (first)
214                this.activateParameter(el, key);
215            return el;
216        },
217
218        onParamterItemClicked: function(el, key) {
219            this.activateParameter(el, key);
220            return false;
221        },
222
223        activateParameter: function(parameterEl, key) {
224            var values = this.filterModel.get("params"),
225                config = this.filterModel.get("config"),
226                self = this;
227            this._activeParameter = key;
228
229            this.configParametersEl.find("li").removeClass("active");
230            parameterEl.addClass("active");
231
232            config.bind(key, function(paramConfig) {
233                // Param config can be null when the value is deleted
234                if (!paramConfig)
235                    return;
236                var el = self.configParameterEl.clone();
237                self.configEditorEl.find(".config-parameter").remove();
238                self.configParametersWrapperEl.after(el);
239                el.find(".config-parameter-label").text(key);
240                el.find(".config-parameter-delete").click(function() {
241                    config.remove(key);
242                    values.remove(key);
243                    return false;
244                });
245                
246                paramConfig.bindToSelectInput("type", el.find(".config-parameter-type"));
247                paramConfig.bind("type", function(type, oldValue) {
248                    if (!oldValue || type == oldValue)
249                        return;
250                    var valueType = self.parametersByType[type];
251                    if (!valueType)
252                        return;
253                    values.set(key, valueType.value);
254                    config.set(key, valueType.config);
255                });
256
257                if (paramConfig.get("type") == "warp" && !values.get(key))
258                    values.set(key, Global.WarpHelpers.generateWarpPoints());
259                
260                var defaultValueEl = el.find(".config-default-value");
261                defaultValueEl.empty();
262
263                var EditorClass = Global.Controls.get(paramConfig.get("type"));
264                if (!EditorClass)
265                    return;
266
267                var editor = new EditorClass(self, key, config.toJSON()),
268                    table = $("<table class='paramsTable' />").appendTo(defaultValueEl),
269                    tr = $("<tr class='field' />")
270                        .appendTo(table);
271                editor.pushControls(tr);
272                editor.setSource(values);
273
274                var configParameterRangeEl = el.find(".config-parameter-range");
275                configParameterRangeEl.hide();
276                if (self.rangeTypes.indexOf(paramConfig.get("type")) != -1) {
277                    configParameterRangeEl.show();
278                    paramConfig.bindToTextInput("min", configParameterRangeEl.find(".config-parameter-range-min"));
279                    paramConfig.bindToTextInput("max", configParameterRangeEl.find(".config-parameter-range-max"));
280                    paramConfig.bindToTextInput("step", configParameterRangeEl.find(".config-parameter-range-step"));
281                    paramConfig.bind("min", editor.updateConfig.bind(editor, paramConfig));
282                    paramConfig.bind("max", editor.updateConfig.bind(editor, paramConfig));
283                    paramConfig.bind("step", editor.updateConfig.bind(editor, paramConfig));
284                }
285            });
286        },
287
288        loadFilter: function(filter) {
289            // Prevent change events while loading first version.
290            this.filter = null;
291
292            this.show();
293
294            this.filter = filter;
295            this.updateModel(filter.original);
296
297            // tripwire for missing CodeMirror library
298            if (this.vertexShaderEditor.isSupported){
299                this.vertexShaderEditor.setValue(filter.editedSource_vertex || "");
300                this.fragmentShaderEditor.setValue(filter.editedSource_fragment || "");
301            } else if (!this.haveAddedCodeMirrorError) {
302                this.haveAddedCodeMirrorError = true;
303                this.vertexShaderEditorEl.append(this.codeViewErrorEl.clone(true).show())
304                this.fragmentShaderEditorEl.append(this.codeViewErrorEl.clone(true).show())
305            }
306            
307            if (this.lastEditor)
308                this.lastEditor.refresh();
309        },
310
311        onShaderTextChanged: function(type, newValue) {
312            if (!this.filter)
313                return;
314            this.shaderTextChangeTimers[type].invoke([this.filter, newValue]);
315        },
316
317        onShaderTextChangedTimerFired: function(type, filter, newValue) {
318            this.filterList.filterUpdate(filter, type, newValue);
319        },
320
321        defaultUniforms: ["u_meshBox", "u_tileSize", "u_meshSize", "u_projectionMatrix", "u_texture", "u_textureSize", "u_contentTexture"],
322        onShaderUniformsDetected: function(type, uniforms) {
323            if (!this.filter)
324                return;
325            var self = this,
326                hadChanges = false;
327            $.each(uniforms, function(i, uniform) {
328                if (self.defaultUniforms.indexOf(uniform.name) != -1)
329                    return;
330                if (self.addUniform(uniform))
331                    hadChanges = true;
332            });
333            if (hadChanges) 
334                this.updateFilterConfig();
335        },
336
337        rangeTypes: ["range", "vec2", "vec3", "vec4"],
338
339        parametersByType: {
340            "color": {
341                config: {
342                    type: 'color',
343                    mixer: 'mixVector',
344                    generator: 'vector'
345                },
346                value: [0, 0, 0, 1]
347            },
348            "unknown": {
349                config: {
350                    type: 'unknown'
351                },
352                value: "0"
353            },
354            "checkbox": {
355                config: {
356                    type: 'checkbox',
357                    mixer: 'dontMix'
358                },
359                value: 0
360            },
361            "range": {
362                config: {
363                    type: 'range',
364                    min: -1000,
365                    max: 1000,
366                    step: 0.01
367                },
368                value: 0
369            },
370
371            "vec2": {
372                config: {
373                    type: 'vec2',
374                    generator: 'vector',
375                    mixer: 'mixVector',
376                    min: -1000,
377                    max: 1000,
378                    step: 0.01
379                },
380                value: [0, 0]
381            },
382
383            "vec3": {
384                config: {
385                    type: 'vec3',
386                    generator: 'vector',
387                    mixer: 'mixVector',
388                    min: -1000,
389                    max: 1000,
390                    step: 0.01
391                },
392                value: [0, 0, 0]
393            },
394
395            "vec4": {
396                config: {
397                    type: 'vec4',
398                    generator: 'vector',
399                    mixer: 'mixVector',
400                    min: -1000,
401                    max: 1000,
402                    step: 0.01
403                },
404                value: [0, 0, 0, 0]
405            },
406
407            "transform": {
408                config: {
409                    type: 'transform',
410                    generator: 'transform',
411                    mixer: {
412                        fn: 'mixHash',
413                        params: ['mixNumber']
414                    }
415                },
416                value: {
417                    rotationX: 0,
418                    rotationY: 0,
419                    rotationZ: 0
420                }
421            },
422
423            "warp": {
424                config: {
425                    type: 'warp',
426                    generator: 'warpArray',
427                    mixer: 'mixVectorOfVectors'
428                },
429                value: null
430            }
431        },
432
433        isSameType: function(oldType, newType) {
434            if (oldType == newType || 
435               (oldType == "color" && newType == "vec4") ||
436               (oldType == "checkbox" && newType == "range") ||
437               (newType == "unknown") ||
438               (oldType == "unknown"))
439                return true;
440
441            return false;
442        },
443
444        addUniform: function(uniform) {
445            var value;
446            if (uniform.size != 1)
447                return false;
448            switch (uniform.type) {
449            case "vec2":
450                value = this.parametersByType["vec2"];
451                break;
452            case "vec3":
453                value = this.parametersByType["vec3"];
454                break;
455            case "vec4":
456                value = this.parametersByType["vec4"];
457                break;
458            case "float":
459                value = this.parametersByType["range"];
460                break;
461            case "mat4":
462                value = this.parametersByType["transform"];
463                break;
464            default:
465                value = this.parametersByType["unknown"];
466                break;
467            }
468
469            if (!value)
470                return false;
471            
472            if (this.filter.original.config.hasOwnProperty(uniform.name) &&
473                this.isSameType(this.filter.original.config[uniform.name].type, value.config.type))
474                return false;
475
476            this.filter.original.config[uniform.name] = value.config;
477            this.filter.original.params[uniform.name] = value.value;
478            return true;
479        },
480
481        updateFilterConfig: function() {
482            this.updateModel(this.filter.original);
483            this.filter.reload(this.filter.original);
484            this.filterList.forkConfigUpdate(this.filter);
485        },
486
487        filterModelChanged: function() {
488            if (!this.filter || !this.filterModel)
489                return;
490            this.shaderConfigChangeTimer.invoke([]);
491        },
492
493        _filterModelChanged: function() {
494            if (!this.filter || !this.filterModel)
495                return;
496            this.updateTitle();
497            var jsonFilterModel = this.filterModel.toJSON();
498            this.filter.reload(jsonFilterModel);
499            this.filterList.forkConfigUpdate(this.filter);
500        },
501
502        updateTitle: function() {
503            if (!this.filterModel)
504                return;
505            this.configDockPanel.container.column.setTitle("Editing shader: " + this.filterModel.label);
506        },
507
508        onWindowResized: function() {
509            if (!this.lastEditor)
510                return;
511            this.lastEditor.refresh();
512        },
513
514        onDockResized: function(ev, direction) {
515            if (!this.lastEditor)
516                return; 
517            this.lastEditor.refresh(); 
518        },
519
520        onActiveStateChanged: function(editor, active) {
521            if (active) {
522                this.lastEditor = editor;
523                this.lastEditor.setActive(true);
524            } else {
525                editor.setActive(false);
526            }
527        },
528
529        onSaveToGistClicked: function() {
530            if (!this.filter)
531                return false;
532            this.githubPopupEl
533                .removeClass("saved saving login")
534                .addClass("info")
535                .show();
536            return false;
537        },
538
539        onSaveGistWithLoginClicked: function() {
540            this.githubPopupEl
541                .removeClass("saved saving info")
542                .addClass("login")
543                .show();
544            this.onGithubLoginCallback = this.github.on("login", this.onGithubLogin.bind(this));
545            this.openGithubLogin();
546            return false;
547        },
548
549        openGithubLogin: function() {
550            var loginUrl = this.github.getLoginUrl();
551            window.open(loginUrl, "css_filterlab_github_login", "width=1015,height=500");
552        },
553
554        onGithubLoginLinkClicked: function() {
555            this.openGithubLogin();
556            return false;
557        },
558
559        onGithubLogin: function(err, token) {
560            console.log(err, token);
561            if (err) {
562                this.githubPopupEl
563                    .addClass("login-failed")
564                    .show();
565            } else {
566                this.saveGist(token);
567            }
568        },
569
570        onSaveAnonymousGistClicked: function() {
571            this.saveGist(null);
572            return false;
573        },
574
575        saveGist: function(token) {
576            if (!this.filter)
577                return;
578            
579            this.githubPopupEl
580                .removeClass("info login")
581                .addClass("saving");
582
583            var filter = this.filter,
584                data = filter.getData(),
585                self = this,
586                filterConfig = filter.original,
587                name = filterConfig.label || filter.name || "Filter";
588            filterConfig.createdWith = "CSS FilterLab";
589            
590            var files = {
591                    "config.json": {
592                        "content": JSON.stringify(filterConfig, null, 4)
593                    },
594                    "shader.vs": {
595                        content: filter.editedSource_vertex
596                    },
597                    "shader.fs": {
598                        content: filter.editedSource_fragment
599                    }
600                };
601
602            var publicGist = this.githubPublicGistEl.is(":checked");
603            
604            // anonymous gists cannot be patched
605            this.github.postGist(token, null, publicGist, name, files, function(response) {
606                data.gist_id = response.id;
607                data.gist_html_url = response.html_url;
608                self.filterList.forkConfigUpdate(filter);
609
610                self.githubLinkEl.attr("href", response.html_url).text(response.html_url);
611                self.githubPopupEl.removeClass("saving").addClass("saved");
612            });
613        },
614
615        removeGithubCallback: function() {
616            if (this.onGithubLoginCallback) {
617                this.github.off("login", this.onGithubLoginCallback);
618                this.onGithubLoginCallback = null;
619            }
620        },
621
622        onCloseGistPopupClicked: function() {
623            this.removeGithubCallback();
624            this.githubPopupEl.hide();
625            return false;
626        }
627
628    }   
629
630    Global.ShaderEditorView = ShaderEditorView;
631})();