Source: 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. }
  143. else {
  144. this.getImageEditorImageData()
  145. .then((imageData) =>
  146. this.process(imageData, missingEffects[0])
  147. )
  148. .then(() => this.prefetchEffects_());
  149. }
  150. }
  151. });
  152. }
  153. /**
  154. * Applies the selected effect to the image.
  155. *
  156. * @param {ImageData} imageData The image data representation of the image.
  157. * @return {Promise} A promise that resolves when the webworker
  158. * finishes processing the image.
  159. */
  160. preview(imageData) {
  161. return this.process(imageData);
  162. }
  163. /**
  164. * Notifies the editor that the component wants to generate a new preview of
  165. * the current image.
  166. *
  167. * @param {MouseEvent} event The mouse event.
  168. */
  169. previewEffect(event) {
  170. this.currentEffect_ = event.delegateTarget.getAttribute('data-effect');
  171. this.requestImageEditorPreview();
  172. }
  173. /**
  174. * Applies the selected effect to the image.
  175. *
  176. * @param {ImageData} imageData The image data representation of the image.
  177. * @param {String} effectName The effect to apply to the image.
  178. * @return {Promise} A promise that resolves when the webworker
  179. * finishes processing the image.
  180. */
  181. process(imageData, effectName) {
  182. const effect = effectName || this.currentEffect_;
  183. let promise = this.cache_[effect];
  184. if (!promise) {
  185. promise = this.spawnWorker_({
  186. effect,
  187. imageData,
  188. });
  189. this.cache_[effect] = promise;
  190. }
  191. return promise;
  192. }
  193. /**
  194. * Makes the carousel scroll left to reveal options of the visible area.
  195. *
  196. * @return void
  197. */
  198. scrollLeft() {
  199. const carousel = this.refs.carousel;
  200. const itemWidth = this.refs.carouselFirstItem.offsetWidth || 0;
  201. const marginLeft = parseInt(carousel.style.marginLeft || 0, 10);
  202. if (marginLeft < 0) {
  203. const newMarginValue = Math.min(marginLeft + itemWidth, 0);
  204. this.carouselOffset = newMarginValue + 'px';
  205. }
  206. }
  207. /**
  208. * Makes the carousel scroll right to reveal options of the visible area.
  209. *
  210. * @return void
  211. */
  212. scrollRight() {
  213. if (this.canScrollForward_()) {
  214. const carousel = this.refs.carousel;
  215. const itemWidth = this.refs.carouselFirstItem.offsetWidth || 0;
  216. const marginLeft = parseInt(carousel.style.marginLeft || 0, 10);
  217. this.carouselOffset = marginLeft - itemWidth + 'px';
  218. }
  219. }
  220. /**
  221. * Spawns a webworker to process the image in a different thread.
  222. *
  223. * @param {String} workerURI The URI of the worker to spawn.
  224. * @param {Object} message The image and effect preset.
  225. * @return {Promise} A promise that resolves when the webworker
  226. * finishes processing the image.
  227. */
  228. spawnWorker_(message) {
  229. return new Promise((resolve) => {
  230. const processWorker = new Worker(
  231. this.modulePath + '/EffectsWorker.js'
  232. );
  233. processWorker.onmessage = (event) => resolve(event.data);
  234. processWorker.postMessage(message);
  235. });
  236. }
  237. }
  238. /**
  239. * State definition.
  240. *
  241. * @static
  242. * @type {!Object}
  243. */
  244. EffectsComponent.STATE = {
  245. /**
  246. * Offset in pixels (<code>px</code> postfix) for the carousel item.
  247. *
  248. * @type {String}
  249. */
  250. carouselOffset: {
  251. validator: core.isString,
  252. value: '0',
  253. },
  254. /**
  255. * Array of available effects.
  256. *
  257. * @type {Object}
  258. */
  259. effects: {
  260. validator: core.isArray,
  261. value: [
  262. 'none',
  263. 'ruby',
  264. 'absinthe',
  265. 'chroma',
  266. 'atari',
  267. 'tripel',
  268. 'ailis',
  269. 'flatfoot',
  270. 'pyrexia',
  271. 'umbra',
  272. 'rouge',
  273. 'idyll',
  274. 'glimmer',
  275. 'elysium',
  276. 'nucleus',
  277. 'amber',
  278. 'paella',
  279. 'aureus',
  280. 'expanse',
  281. 'orchid',
  282. ],
  283. },
  284. /**
  285. * Injected helper that retrieves the editor image data.
  286. *
  287. * @type {Function}
  288. */
  289. getImageEditorImageData: {
  290. validator: core.isFunction,
  291. },
  292. /**
  293. * Path of this module.
  294. *
  295. * @type {Function}
  296. */
  297. modulePath: {
  298. validator: core.isString,
  299. },
  300. /**
  301. * Injected helper that retrieves the editor image data.
  302. *
  303. * @type {Function}
  304. */
  305. requestImageEditorPreview: {
  306. validator: core.isFunction,
  307. },
  308. /**
  309. * Size of the thumbnails (size x size).
  310. *
  311. * @type {Number}
  312. */
  313. thumbnailSize: {
  314. validator: core.isNumber,
  315. value: 55,
  316. },
  317. };
  318. Soy.register(EffectsComponent, componentTemplates);
  319. export default EffectsComponent;