c9-core/plugins/c9.ide.imgeditor/imgeditor.js

693 wiersze
28 KiB
JavaScript

define(function(require, exports, module) {
main.consumes = [
"Editor", "editors", "ui", "save", "vfs", "layout", "watcher",
"settings", "dialog.error", "c9"
];
main.provides = ["imgeditor"];
return main;
function main(options, imports, register) {
var ui = imports.ui;
var c9 = imports.c9;
var vfs = imports.vfs;
var save = imports.save;
var layout = imports.layout;
var watcher = imports.watcher;
var Editor = imports.Editor;
var editors = imports.editors;
var settings = imports.settings;
var showError = imports["dialog.error"].show;
var event = require("ace/lib/event");
var Pixastic = require("./lib_pixastic");
var loadedFiles = {};
/***** Initialization *****/
var extensions = ["bmp", "djv", "djvu", "jpg", "jpeg",
"pbm", "pgm", "png", "pnm", "ppm", "psd", "tiff",
"xbm", "xpm"];
var handle = editors.register("imgeditor", "Image Editor", ImageEditor, extensions);
var drawn;
handle.draw = function() {
if (drawn) return;
drawn = true;
// Insert CSS
ui.insertCss(require("text!./style.css"), null, handle);
};
// @todo revert to saved doesnt work (same for file watcher reload)
// @todo getState/setState
// @todo keep canvas reference on session and remove loadedFiles
// @Todo for later - add undo stack
function UndoItem(original, changed, apply) {
this.getState = function() { };
this.undo = function() {
apply(original);
};
this.redo = function() {
apply(changed);
};
}
// undoManager.on("itemFind", function(e) {
// return new Item(e.state[0], e.state[1]);
// });
function ImageEditor() {
var plugin = new Editor("Ajax.org", main.consumes, extensions);
var BGCOLOR = {
"flat-light": "#F1F1F1",
"flat-dark": "#3D3D3D",
"light": "#D3D3D3",
"light-gray": "#D3D3D3",
"dark": "#3D3D3D",
"dark-gray": "#3D3D3D"
};
var img, canvas, activeDocument, rect, crop, zoom, smooth, info, rectinfo;
var editor;
plugin.on("draw", function(e) {
handle.draw();
ui.insertMarkup(e.tab, require("text!./imgeditor.xml"), plugin);
var parent = plugin.getElement("parent");
var btn1 = plugin.getElement("btn1");
var btn3 = plugin.getElement("btn3");
var btn4 = plugin.getElement("btn4");
var btn5 = plugin.getElement("btn5");
var btn6 = plugin.getElement("btn6");
editor = plugin.getElement("imgEditor");
crop = plugin.getElement("btn2");
zoom = plugin.getElement("zoom");
smooth = plugin.getElement("smooth");
info = plugin.getElement("info");
rectinfo = plugin.getElement("rectinfo");
// Rectangle
rect = document.createElement("div");
editor.$ext.appendChild(rect);
rect.className = "imgeditorrect";
canvas = function() {
return editor.$ext.querySelector("canvas");
};
// Resize
var mnuResize = plugin.getElement("resize-menu");
btn1.setAttribute("submenu", mnuResize);
plugin.addElement(mnuResize);
var tbWidth = plugin.getElement("width");
var tbHeight = plugin.getElement("height");
var cbAspect = plugin.getElement("aspectratio");
var btnResize = plugin.getElement("resize-button");
btnResize.addEventListener("click", function() {
if (tbHeight.getValue() > 1 && tbWidth.getValue() > 1) {
exec("resize", {
width: tbWidth.getValue(),
height: tbHeight.getValue()
});
}
});
tbWidth.on("blur", function() {
if (cbAspect.checked) {
tbHeight.setAttribute("value", Math.round(canvas().offsetHeight
* (tbWidth.getValue() / canvas().offsetWidth)));
}
});
tbHeight.on("blur", function() {
if (cbAspect.checked) {
tbWidth.setAttribute("value", Math.round(canvas().offsetWidth
* (tbHeight.getValue() / canvas().offsetHeight)));
}
});
mnuResize.on("prop.visible", function(e) {
if (!e.value) return;
tbWidth.setValue(canvas().offsetWidth);
tbHeight.setValue(canvas().offsetHeight);
});
// smooth
smooth.on("afterchange", function(e) {
if (smooth.checked) {
ui.setStyleRule(".imgeditor canvas",
"-ms-interpolation-mode", "bicubic");
ui.setStyleRule(".imgeditor canvas",
"image-rendering", "auto");
}
else {
ui.setStyleRule(".imgeditor canvas",
"-ms-interpolation-mode", "nearest-neighbor");
["-moz-crisp-edges", "-o-crisp-edges",
"-webkit-optimize-contrast", "optimize-contrast",
"pixelated"].map(function(prop) {
ui.setStyleRule(".imgeditor canvas",
"image-rendering", prop);
});
}
var session = activeDocument.getSession();
session.smooth = smooth.checked;
settings.set("user/imgeditor/@smooth", smooth.checked);
clearRect();
});
// Zoom
zoom.on("afterchange", function(e) {
ui.setStyleRule(".imgeditor canvas",
apf.CSSPREFIX2 + "-transform",
"scale(" + (zoom.value / 100) + ")");
var session = activeDocument.getSession();
session.zoom = zoom.value;
if (e.value) // User Change
settings.set("user/imgeditor/@zoom", zoom.value);
clearRect();
});
// resize width/height
crop.on("click", function() { exec("crop"); });
btn3.on("click", function() { exec("rotate", { angle: -90 }); });
btn4.on("click", function() { exec("rotate", { angle: 90 }); });
btn5.on("click", function() { exec("fliph"); });
btn6.on("click", function() { exec("flipv"); });
editor.$ext.onmousemove = function(e) {
if (rect.style.display != "none")
return;
var cnvs = canvas();
var pos = cnvs.getBoundingClientRect();
var left = e.clientX - pos.left;
var top = e.clientY - pos.top;
var zoomLevel = zoom.value / 100;
if (left < 0 || top < 0
|| left > pos.width || top > pos.height)
left = top = 0;
rectinfo.setAttribute("caption",
"L: " + (left / zoomLevel) + "px, "
+ "T: " + (top / zoomLevel) + "px");
};
editor.$ext.onmousedown = function(e) {
startRect(e);
};
function saveCanvas(path, value, callback) {
var dataURL = loadedFiles[path];
var blob = dataUriToBlob(dataURL); // atob(dataURL.split(',')[1]);
// Alert watcher we are saving
watcher.ignore(path, 60000);
// Save
vfs.rest(path, {
method: "PUT",
body: blob
}, function(err, data, res) {
callback(err, data);
watcher.ignore(path);
});
}
function dataUriToBlob(dataURI) {
// serialize the base64/URLEncoded data
var byteString;
if (dataURI.split(',')[0].indexOf('base64') >= 0) {
byteString = atob(dataURI.split(',')[1]);
}
else {
byteString = unescape(dataURI.split(',')[1]);
}
// parse the mime type
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
// construct a Blob of the image data
var array = [];
for (var i = 0; i < byteString.length; i++) {
array.push(byteString.charCodeAt(i));
}
return new Blob(
[new Uint8Array(array)],
{ type: mimeString }
);
}
save.on("beforeSave", function(e) {
if (e.document.editor.type == "imgeditor") {
var path = e.document.tab.path;
// Prevent unchanged files from being saved
if (!e.document.changed && path == e.path)
return false;
if (e.document == activeDocument)
loadedFiles[e.path] = canvas().toDataURL();
return saveCanvas;
}
});
// Not sure what this is supposed to do
// save.on("afterSave", function(e) {
// console.log("afterfilesave");
// var path = e.document.tab.path;
// if (!path)
// return;
// var newPath = e.doc && e.doc.getNode && e.doc.getNode().getAttribute("path");
// if (editor.value == e.oldpath && newPath !== e.oldpath) {
// var dataURL = _canvas.toDataURL();
// saveCanvas(newPath,dataURL);
// return false;
// }
// });
if (!editor.focus)
editor.focus = function() { return false;};
editor.show();
});
/***** Method *****/
function setPath(path, doc, callback) {
if (!path) return;
// Caption is the filename
doc.title = path.substr(path.lastIndexOf("/") + 1);
// Tooltip is the full path
doc.tooltip = path;
var fullpath = path.match(/^\w+:\/\//)
? path
: vfs.url(path);
loadCanvas(doc.tab, fullpath, callback);
}
function loadCanvas(tab, path, callback) {
var idx = tab.path;
var cnvs = canvas();
var ctx = cnvs.getContext("2d");
var img = editor.$ext.querySelector("img");
if (img) {
var src = img.src.replace(/\?[^?]+/, "");
if (src == path)
return;
img.parentNode.removeChild(img);
}
// TODO remove this when vfs sets correct cache headers on changed images
path = path + "?" + Date.now();
img = document.createElement("img");
img.style.margin = "0 auto";
editor.$ext.appendChild(img);
// Enable CORS support
if (c9.hosted)
img.crossOrigin = "Anonymous";
if (path && !loadedFiles[idx]) {
tab.classList.add("connecting");
var timer = setTimeout(function() {
img._cleanup();
showError("Image loading timed out");
}, 120 * 1000);
img.onload = function onLoad(e) {
var sizeCaption = "W:" + img.width + "px, H:" + img.height + "px";
info.setAttribute("caption", sizeCaption);
cnvs.width = img.width;
cnvs.height = img.height;
cnvs.style.display = "inline-block";
img.style.display = "none";
ctx.drawImage(img, 0, 0);
loadedFiles[idx] = cnvs.toDataURL();
tab.classList.remove("connecting");
img._cleanup();
callback && callback(loadedFiles[idx]);
};
img.onerror = function() {
img._cleanup();
tab.classList.add("error");
img.src = options.staticPrefix + "/images/sorry.jpg";
showError("Invalid or Unsupported Image Format");
};
img._cleanup = function() {
clearTimeout(timer);
img.onerror = img.onload = null;
if (img.parentNode)
img.parentNode.removeChild(img);
tab.classList.remove("connecting");
};
img.src = path;
}
else {
var tempImg = new Image();
tempImg.onload = function() {
cnvs.width = tempImg.width;
cnvs.height = tempImg.height;
cnvs.style.display = "inline-block";
info.setAttribute("caption",
"W:" + tempImg.width + "px, " +
"H:" + tempImg.height + "px");
ctx.drawImage(tempImg, 0, 0);
};
tempImg.src = loadedFiles[idx];
callback && callback(loadedFiles[idx]);
}
}
function startRect(e) {
var container = rect.parentNode;
var pos = container.getBoundingClientRect();
var cnvs = canvas();
var htmlNode = editor.$ext;
var cnvsPos = cnvs.getBoundingClientRect();
var xMin = cnvsPos.left - pos.left + htmlNode.scrollLeft;
var yMin = cnvsPos.top - pos.top + htmlNode.scrollTop;
var xMax = xMin + cnvsPos.width;
var yMax = yMin + cnvsPos.height;
function clampX(x) { return Math.min(Math.max(xMin, x), xMax); }
function clampY(y) { return Math.min(Math.max(yMin, y), yMax); }
var startX = clampX(e.clientX - pos.left + htmlNode.scrollLeft);
var startY = clampY(e.clientY - pos.top + htmlNode.scrollTop);
var moved, scrolled;
event.capture(container, function onMove(e) {
var scrollLeft = htmlNode.scrollLeft;
var scrollTop = htmlNode.scrollTop;
var clientX = e.clientX - pos.left + scrollLeft;
var clientY = e.clientY - pos.top + scrollTop;
if (scrolled) {
clearTimeout(scrolled);
scrolled = null;
}
if (clientX > scrollLeft + pos.width) {
scrollLeft += clientX - scrollLeft - pos.width;
htmlNode.scrollLeft = scrollLeft;
if (scrollLeft < htmlNode.scrollWidth)
scrolled = true;
} else if (clientX < scrollLeft - 5) {
scrollLeft += clientX - scrollLeft;
htmlNode.scrollLeft = scrollLeft;
if (scrollLeft > 0)
scrolled = true;
}
if (clientY > scrollTop + pos.height) {
scrollTop += clientY - scrollTop - pos.height;
htmlNode.scrollTop = scrollTop;
if (scrollTop < htmlNode.scrollHeight)
scrolled = true;
} else if (clientY < scrollTop - 5) {
scrollTop += clientY - scrollTop;
htmlNode.scrollTop = scrollTop;
if (scrollTop > 0)
scrolled = true;
}
if (scrolled) {
scrolled = setTimeout(function() {
if (cnvs) onMove(e);
}, 20);
}
clientX = clampX(clientX);
clientY = clampY(clientY);
if (!moved) {
if (Math.abs(startX - e.clientX) + Math.abs(startY - e.clientY) > 5) {
moved = true;
rect.style.display = "block";
}
else return;
}
if (startX > clientX) {
rect.style.left = clientX + "px";
rect.style.width = (startX - clientX) + "px";
}
else {
rect.style.left = startX + "px";
rect.style.width = (clientX - startX) + "px";
}
if (startY > clientY) {
rect.style.top = clientY + "px";
rect.style.height = (startY - clientY) + "px";
}
else {
rect.style.top = startY + "px";
rect.style.height = (clientY - startY) + "px";
}
var zoomLevel = zoom.value / 100;
rectinfo.setAttribute("caption",
"L: " + ((rect.offsetLeft - cnvs.offsetLeft) / zoomLevel) + "px, "
+ "T: " + ((rect.offsetTop - cnvs.offsetTop) / zoomLevel) + "px, "
+ "W: " + (rect.offsetWidth / zoomLevel) + "px, "
+ "H: " + (rect.offsetHeight / zoomLevel) + "px");
}, function() {
cnvs = null;
if (moved && parseInt(rect.style.width, 10) && parseInt(rect.style.height, 10)) {
activeDocument.getSession().rect = {
left: rect.style.left,
top: rect.style.top,
width: rect.style.width,
height: rect.style.height,
};
crop.enable();
}
else {
clearRect();
}
});
event.stopEvent(e);
}
function exec(action, options) {
var cnvs = canvas();
var url = cnvs.toDataURL();
if (action == "crop") {
var zoomLevel = zoom.value / 100;
options = {
left: (rect.offsetLeft - cnvs.offsetLeft) / zoomLevel,
top: (rect.offsetTop - cnvs.offsetTop) / zoomLevel,
width: (rect.offsetWidth) / zoomLevel,
height: (rect.offsetHeight) / zoomLevel
};
}
Pixastic.process(cnvs, action, options);
clearRect();
cnvs = canvas();
info.setAttribute("caption",
"W:" + cnvs.offsetWidth + "px, " +
"H:" + cnvs.offsetHeight + "px");
//@todo
var doc = activeDocument;
doc.undoManager.add(new UndoItem(url, canvas().toDataURL(), function(url) {
loadedFiles[doc.tab.path] = url;
loadCanvas(doc.tab);
}));
}
function clearRect() {
rect.style.display = "none";
delete activeDocument.getSession().rect;
crop.disable();
}
plugin.on("documentLoad", function(e) {
var doc = e.doc;
var session = doc.getSession();
doc.tab.on("setPath", function(e) {
setPath(e.path, doc);
}, session);
// Value Retrieval
// doc.on("getValue", function get(e) {
// return session.session
// ? session.session.getValue()
// : e.value;
// }, session);
// Value setting
doc.on("setValue", function set(e) {
var path = doc.tab.path;
// The first value that is set should clear the undo stack
// additional times setting the value should keep it.
if (doc.hasValue()) {
var lastValue = loadedFiles[path];
delete loadedFiles[path];
// @todo this will go wrong and will be fixed when keeping canvas per session
setPath(path, doc, function(newValue) {
doc.undoManager.add(new UndoItem(lastValue, canvas().toDataURL(), function(url) {
loadedFiles[path] = url;
loadCanvas(doc.tab);
}));
});
}
else {
setPath(path, doc);
}
// doc.tab.classList.remove("loading");
}, session);
function setTheme(e) {
var tab = doc.tab;
var isDark = e.theme == "dark";
tab.backgroundColor = BGCOLOR[e.theme];
if (isDark) tab.classList.add("dark");
else tab.classList.remove("dark");
}
layout.on("themeChange", setTheme, doc);
setTheme({ theme: settings.get("user/general/@skin") });
canvas().style.display = "none";
session.zoom = settings.getNumber("user/imgeditor/@zoom") || 100;
});
plugin.on("documentActivate", function(e) {
var doc = e.doc;
var session = doc.getSession();
var path = doc.tab.path;
activeDocument = doc;
// Set Image
setPath(path, doc);
// Set Toolbar
zoom.setValue(session.zoom || 100);
zoom.dispatchEvent("afterchange");
smooth.setValue(session.smooth);
smooth.dispatchEvent("afterchange");
// Set Rect
if (session.rect) {
rect.style.display = "block";
rect.style.left = session.rect.left;
rect.style.top = session.rect.top;
rect.style.width = session.rect.width;
rect.style.height = session.rect.height;
}
else {
rect.style.display = "none";
}
});
plugin.on("documentUnload", function(e) {
delete loadedFiles[e.doc.tab.path];
});
/***** Register and define API *****/
/**
* The imgeditor handle, responsible for events that involve all
* ImageEditor instances. This is the object you get when you request
* the imgeditor service in your plugin.
*
* Example:
*
* define(function(require, exports, module) {
* main.consumes = ["imgeditor"];
* main.provides = ["myplugin"];
* return main;
*
* function main(options, imports, register) {
* var imgeditorHandle = imports.imgeditor;
* });
* });
*
*
* @class imgeditor
* @extends Plugin
* @singleton
*/
/**
* Read Only Image Viewer for Cloud9
*
* Example of instantiating a new terminal:
*
* tabManager.openFile("/test.png", true, function(err, tab) {
* if (err) throw err;
*
* var imgeditor = tab.editor;
* });
*
* @class imgeditor.ImageEditor
* @extends Editor
**/
/**
* The type of editor. Use this to create the terminal using
* {@link tabManager#openEditor} or {@link editors#createEditor}.
* @property {"imgeditor"} type
* @readonly
*/
plugin.freezePublicAPI({
autoload: false
});
plugin.load(null, "imgeditor");
return plugin;
}
ImageEditor.autoload = false;
register(null, {
imgeditor: handle
});
}
});