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