/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
import {async, core} from 'metal';
import Component from 'metal-component';
import Soy from 'metal-soy';
import componentTemplates from './EffectsComponent.soy';
import './EffectsControls.soy';
/**
* Creates an Effects component.
*/
class EffectsComponent extends Component {
/**
* @inheritDoc
*/
attached() {
this.cache_ = {};
async.nextTick(() => {
this.getImageEditorImageData()
.then((imageData) =>
Promise.resolve(this.generateThumbnailImageData_(imageData))
)
.then((previewImageData) =>
this.generateThumbnails_(previewImageData)
)
.then(() => this.prefetchEffects_());
});
}
/**
* @inheritDoc
*/
detached() {
this.cache_ = {};
}
/**
* Returns <code>true</code> if the carousel can be scrolled to the right.
*
* @private
* @return {boolean} <code>True</code> if the carousel can be scrolled to
* the right; <code>false</code> otherwise.
*/
canScrollForward_() {
const carousel = this.refs.carousel;
const continer = this.refs.carouselContainer;
const offset = Math.abs(parseInt(carousel.style.marginLeft || 0, 10));
const viewportWidth = parseInt(continer.offsetWidth, 10);
const maxContentWidth = parseInt(carousel.offsetWidth, 10);
return offset + viewportWidth < maxContentWidth;
}
/**
* Generates a thumbnail for a given effect.
*
* @param {String} effect The effect to generate the thumbnail for.
* @param {ImageData} imageData The image data to which the effect is applied.
* @return {Promise} A promise that resolves when the thumbnail
* is generated.
*/
generateThumbnail_(effect, imageData) {
const promise = this.spawnWorker_({
effect,
imageData,
});
promise.then((imageData) => {
const canvas = this.element.querySelector(
'#' + this.ref + effect + ' canvas'
);
canvas.getContext('2d').putImageData(imageData, 0, 0);
});
return promise;
}
/**
* Generates the complete set of thumbnails for the component effects.
*
* @param {ImageData} imageData The thumbnail image data (small version).
* @return {Promise} A promise that resolves when the thumbnails
* are generated.
*/
generateThumbnails_(imageData) {
return Promise.all(
this.effects.map((effect) =>
this.generateThumbnail_(effect, imageData)
)
);
}
/**
* Generates a resized version of the image data to generate the thumbnails
* more efficiently.
*
* @param {ImageData} imageData The original image data.
* @return {ImageData} The resized image data.
*/
generateThumbnailImageData_(imageData) {
const thumbnailSize = this.thumbnailSize;
const imageWidth = imageData.width;
const imageHeight = imageData.height;
const rawCanvas = document.createElement('canvas');
rawCanvas.width = imageWidth;
rawCanvas.height = imageHeight;
rawCanvas.getContext('2d').putImageData(imageData, 0, 0);
const commonSize = imageWidth > imageHeight ? imageHeight : imageWidth;
const canvas = document.createElement('canvas');
canvas.width = thumbnailSize;
canvas.height = thumbnailSize;
const context = canvas.getContext('2d');
context.drawImage(
rawCanvas,
imageWidth - commonSize,
imageHeight - commonSize,
commonSize,
commonSize,
0,
0,
thumbnailSize,
thumbnailSize
);
return context.getImageData(0, 0, thumbnailSize, thumbnailSize);
}
/**
* Prefetches all the effect results.
*
* @return {Promise} A promise that resolves when all the effects
* are prefetched.
*/
prefetchEffects_() {
return new Promise((resolve) => {
if (!this.isDisposed()) {
const missingEffects = this.effects.filter(
(effect) => !this.cache_[effect]
);
if (!missingEffects.length) {
resolve();
}
else {
this.getImageEditorImageData()
.then((imageData) =>
this.process(imageData, missingEffects[0])
)
.then(() => this.prefetchEffects_());
}
}
});
}
/**
* Applies the selected effect to the image.
*
* @param {ImageData} imageData The image data representation of the image.
* @return {Promise} A promise that resolves when the webworker
* finishes processing the image.
*/
preview(imageData) {
return this.process(imageData);
}
/**
* Notifies the editor that the component wants to generate a new preview of
* the current image.
*
* @param {MouseEvent} event The mouse event.
*/
previewEffect(event) {
this.currentEffect_ = event.delegateTarget.getAttribute('data-effect');
this.requestImageEditorPreview();
}
/**
* Applies the selected effect to the image.
*
* @param {ImageData} imageData The image data representation of the image.
* @param {String} effectName The effect to apply to the image.
* @return {Promise} A promise that resolves when the webworker
* finishes processing the image.
*/
process(imageData, effectName) {
const effect = effectName || this.currentEffect_;
let promise = this.cache_[effect];
if (!promise) {
promise = this.spawnWorker_({
effect,
imageData,
});
this.cache_[effect] = promise;
}
return promise;
}
/**
* Makes the carousel scroll left to reveal options of the visible area.
*
* @return void
*/
scrollLeft() {
const carousel = this.refs.carousel;
const itemWidth = this.refs.carouselFirstItem.offsetWidth || 0;
const marginLeft = parseInt(carousel.style.marginLeft || 0, 10);
if (marginLeft < 0) {
const newMarginValue = Math.min(marginLeft + itemWidth, 0);
this.carouselOffset = newMarginValue + 'px';
}
}
/**
* Makes the carousel scroll right to reveal options of the visible area.
*
* @return void
*/
scrollRight() {
if (this.canScrollForward_()) {
const carousel = this.refs.carousel;
const itemWidth = this.refs.carouselFirstItem.offsetWidth || 0;
const marginLeft = parseInt(carousel.style.marginLeft || 0, 10);
this.carouselOffset = marginLeft - itemWidth + 'px';
}
}
/**
* Spawns a webworker to process the image in a different thread.
*
* @param {String} workerURI The URI of the worker to spawn.
* @param {Object} message The image and effect preset.
* @return {Promise} A promise that resolves when the webworker
* finishes processing the image.
*/
spawnWorker_(message) {
return new Promise((resolve) => {
const processWorker = new Worker(
this.modulePath + '/EffectsWorker.js'
);
processWorker.onmessage = (event) => resolve(event.data);
processWorker.postMessage(message);
});
}
}
/**
* State definition.
*
* @static
* @type {!Object}
*/
EffectsComponent.STATE = {
/**
* Offset in pixels (<code>px</code> postfix) for the carousel item.
*
* @type {String}
*/
carouselOffset: {
validator: core.isString,
value: '0',
},
/**
* Array of available effects.
*
* @type {Object}
*/
effects: {
validator: core.isArray,
value: [
'none',
'ruby',
'absinthe',
'chroma',
'atari',
'tripel',
'ailis',
'flatfoot',
'pyrexia',
'umbra',
'rouge',
'idyll',
'glimmer',
'elysium',
'nucleus',
'amber',
'paella',
'aureus',
'expanse',
'orchid',
],
},
/**
* Injected helper that retrieves the editor image data.
*
* @type {Function}
*/
getImageEditorImageData: {
validator: core.isFunction,
},
/**
* Path of this module.
*
* @type {Function}
*/
modulePath: {
validator: core.isString,
},
/**
* Injected helper that retrieves the editor image data.
*
* @type {Function}
*/
requestImageEditorPreview: {
validator: core.isFunction,
},
/**
* Size of the thumbnails (size x size).
*
* @type {Number}
*/
thumbnailSize: {
validator: core.isNumber,
value: 55,
},
};
Soy.register(EffectsComponent, componentTemplates);
export default EffectsComponent;