Source: frontend-image-editor-capability-effects/src/main/resources/META-INF/resources/EffectsComponent.es.js

  1. /**
  2. * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
  3. *
  4. * This library is free software; you can redistribute it and/or modify it under
  5. * the terms of the GNU Lesser General Public License as published by the Free
  6. * Software Foundation; either version 2.1 of the License, or (at your option)
  7. * any later version.
  8. *
  9. * This library is distributed in the hope that it will be useful, but WITHOUT
  10. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  12. * details.
  13. */
  14. import {async, core} from 'metal';
  15. import Component from 'metal-component';
  16. import Soy from 'metal-soy';
  17. import componentTemplates from './EffectsComponent.soy';
  18. import './EffectsControls.soy';
  19. /**
  20. * Creates an Effects component.
  21. */
  22. class EffectsComponent extends Component {
  23. /**
  24. * @inheritDoc
  25. */
  26. attached() {
  27. this.cache_ = {};
  28. async.nextTick(() => {
  29. this.getImageEditorImageData()
  30. .then(imageData =>
  31. Promise.resolve(this.generateThumbnailImageData_(imageData))
  32. )
  33. .then(previewImageData =>
  34. this.generateThumbnails_(previewImageData)
  35. )
  36. .then(() => this.prefetchEffects_());
  37. });
  38. }
  39. /**
  40. * @inheritDoc
  41. */
  42. detached() {
  43. this.cache_ = {};
  44. }
  45. /**
  46. * Returns <code>true</code> if the carousel can be scrolled to the right.
  47. *
  48. * @private
  49. * @return {boolean} <code>True</code> if the carousel can be scrolled to
  50. * the right; <code>false</code> otherwise.
  51. */
  52. canScrollForward_() {
  53. const carousel = this.refs.carousel;
  54. const continer = this.refs.carouselContainer;
  55. const offset = Math.abs(parseInt(carousel.style.marginLeft || 0, 10));
  56. const viewportWidth = parseInt(continer.offsetWidth, 10);
  57. const maxContentWidth = parseInt(carousel.offsetWidth, 10);
  58. return offset + viewportWidth < maxContentWidth;
  59. }
  60. /**
  61. * Generates a thumbnail for a given effect.
  62. *
  63. * @param {String} effect The effect to generate the thumbnail for.
  64. * @param {ImageData} imageData The image data to which the effect is applied.
  65. * @return {Promise} A promise that resolves when the thumbnail
  66. * is generated.
  67. */
  68. generateThumbnail_(effect, imageData) {
  69. const promise = this.spawnWorker_({
  70. effect,
  71. imageData
  72. });
  73. promise.then(imageData => {
  74. const canvas = this.element.querySelector(
  75. '#' + this.ref + effect + ' canvas'
  76. );
  77. canvas.getContext('2d').putImageData(imageData, 0, 0);
  78. });
  79. return promise;
  80. }
  81. /**
  82. * Generates the complete set of thumbnails for the component effects.
  83. *
  84. * @param {ImageData} imageData The thumbnail image data (small version).
  85. * @return {Promise} A promise that resolves when the thumbnails
  86. * are generated.
  87. */
  88. generateThumbnails_(imageData) {
  89. return Promise.all(
  90. this.effects.map(effect =>
  91. this.generateThumbnail_(effect, imageData)
  92. )
  93. );
  94. }
  95. /**
  96. * Generates a resized version of the image data to generate the thumbnails
  97. * more efficiently.
  98. *
  99. * @param {ImageData} imageData The original image data.
  100. * @return {ImageData} The resized image data.
  101. */
  102. generateThumbnailImageData_(imageData) {
  103. const thumbnailSize = this.thumbnailSize;
  104. const imageWidth = imageData.width;
  105. const imageHeight = imageData.height;
  106. const rawCanvas = document.createElement('canvas');
  107. rawCanvas.width = imageWidth;
  108. rawCanvas.height = imageHeight;
  109. rawCanvas.getContext('2d').putImageData(imageData, 0, 0);
  110. const commonSize = imageWidth > imageHeight ? imageHeight : imageWidth;
  111. const canvas = document.createElement('canvas');
  112. canvas.width = thumbnailSize;
  113. canvas.height = thumbnailSize;
  114. const context = canvas.getContext('2d');
  115. context.drawImage(
  116. rawCanvas,
  117. imageWidth - commonSize,
  118. imageHeight - commonSize,
  119. commonSize,
  120. commonSize,
  121. 0,
  122. 0,
  123. thumbnailSize,
  124. thumbnailSize
  125. );
  126. return context.getImageData(0, 0, thumbnailSize, thumbnailSize);
  127. }
  128. /**
  129. * Prefetches all the effect results.
  130. *
  131. * @return {Promise} A promise that resolves when all the effects
  132. * are prefetched.
  133. */
  134. prefetchEffects_() {
  135. return new Promise(resolve => {
  136. if (!this.isDisposed()) {
  137. const missingEffects = this.effects.filter(
  138. effect => !this.cache_[effect]
  139. );
  140. if (!missingEffects.length) {
  141. resolve();
  142. } else {
  143. this.getImageEditorImageData()
  144. .then(imageData =>
  145. this.process(imageData, missingEffects[0])
  146. )
  147. .then(() => this.prefetchEffects_());
  148. }
  149. }
  150. });
  151. }
  152. /**
  153. * Applies the selected effect to the image.
  154. *
  155. * @param {ImageData} imageData The image data representation of the image.
  156. * @return {Promise} A promise that resolves when the webworker
  157. * finishes processing the image.
  158. */
  159. preview(imageData) {
  160. return this.process(imageData);
  161. }
  162. /**
  163. * Notifies the editor that the component wants to generate a new preview of
  164. * the current image.
  165. *
  166. * @param {MouseEvent} event The mouse event.
  167. */
  168. previewEffect(event) {
  169. this.currentEffect_ = event.delegateTarget.getAttribute('data-effect');
  170. this.requestImageEditorPreview();
  171. }
  172. /**
  173. * Applies the selected effect to the image.
  174. *
  175. * @param {ImageData} imageData The image data representation of the image.
  176. * @param {String} effectName The effect to apply to the image.
  177. * @return {Promise} A promise that resolves when the webworker
  178. * finishes processing the image.
  179. */
  180. process(imageData, effectName) {
  181. const effect = effectName || this.currentEffect_;
  182. let promise = this.cache_[effect];
  183. if (!promise) {
  184. promise = this.spawnWorker_({
  185. effect,
  186. imageData
  187. });
  188. this.cache_[effect] = promise;
  189. }
  190. return promise;
  191. }
  192. /**
  193. * Makes the carousel scroll left to reveal options of the visible area.
  194. *
  195. * @return void
  196. */
  197. scrollLeft() {
  198. const carousel = this.refs.carousel;
  199. const itemWidth = this.refs.carouselFirstItem.offsetWidth || 0;
  200. const marginLeft = parseInt(carousel.style.marginLeft || 0, 10);
  201. if (marginLeft < 0) {
  202. const newMarginValue = Math.min(marginLeft + itemWidth, 0);
  203. this.carouselOffset = newMarginValue + 'px';
  204. }
  205. }
  206. /**
  207. * Makes the carousel scroll right to reveal options of the visible area.
  208. *
  209. * @return void
  210. */
  211. scrollRight() {
  212. if (this.canScrollForward_()) {
  213. const carousel = this.refs.carousel;
  214. const itemWidth = this.refs.carouselFirstItem.offsetWidth || 0;
  215. const marginLeft = parseInt(carousel.style.marginLeft || 0, 10);
  216. this.carouselOffset = marginLeft - itemWidth + 'px';
  217. }
  218. }
  219. /**
  220. * Spawns a webworker to process the image in a different thread.
  221. *
  222. * @param {String} workerURI The URI of the worker to spawn.
  223. * @param {Object} message The image and effect preset.
  224. * @return {Promise} A promise that resolves when the webworker
  225. * finishes processing the image.
  226. */
  227. spawnWorker_(message) {
  228. return new Promise(resolve => {
  229. const processWorker = new Worker(
  230. this.modulePath + '/EffectsWorker.js'
  231. );
  232. processWorker.onmessage = event => resolve(event.data);
  233. processWorker.postMessage(message);
  234. });
  235. }
  236. }
  237. /**
  238. * State definition.
  239. *
  240. * @static
  241. * @type {!Object}
  242. */
  243. EffectsComponent.STATE = {
  244. /**
  245. * Offset in pixels (<code>px</code> postfix) for the carousel item.
  246. *
  247. * @type {String}
  248. */
  249. carouselOffset: {
  250. validator: core.isString,
  251. value: '0'
  252. },
  253. /**
  254. * Array of available effects.
  255. *
  256. * @type {Object}
  257. */
  258. effects: {
  259. validator: core.isArray,
  260. value: [
  261. 'none',
  262. 'ruby',
  263. 'absinthe',
  264. 'chroma',
  265. 'atari',
  266. 'tripel',
  267. 'ailis',
  268. 'flatfoot',
  269. 'pyrexia',
  270. 'umbra',
  271. 'rouge',
  272. 'idyll',
  273. 'glimmer',
  274. 'elysium',
  275. 'nucleus',
  276. 'amber',
  277. 'paella',
  278. 'aureus',
  279. 'expanse',
  280. 'orchid'
  281. ]
  282. },
  283. /**
  284. * Injected helper that retrieves the editor image data.
  285. *
  286. * @type {Function}
  287. */
  288. getImageEditorImageData: {
  289. validator: core.isFunction
  290. },
  291. /**
  292. * Path of this module.
  293. *
  294. * @type {Function}
  295. */
  296. modulePath: {
  297. validator: core.isString
  298. },
  299. /**
  300. * Injected helper that retrieves the editor image data.
  301. *
  302. * @type {Function}
  303. */
  304. requestImageEditorPreview: {
  305. validator: core.isFunction
  306. },
  307. /**
  308. * Size of the thumbnails (size x size).
  309. *
  310. * @type {Number}
  311. */
  312. thumbnailSize: {
  313. validator: core.isNumber,
  314. value: 55
  315. }
  316. };
  317. Soy.register(EffectsComponent, componentTemplates);
  318. export default EffectsComponent;