// ==UserScript== // @name glam // @namespace spaceotter // @author spaceotter@mastodon.art // @version 0.6 // @grant none // @include http://amer* // @include https://mastodon.social/* // @include https://mastodon.art/* // @include https://mstdn.io/* // ==/UserScript== //////////////////////////////////////////////////////////////////////////////////////////////////////// // // // CAMANJS // // // //////////////////////////////////////////////////////////////////////////////////////////////////////// // The CamanJS code is released under the following BSD flavored license /* Copyright (c) 2010-2016, Ryan LeFevre All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Ryan LeFevre nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // Generated by CoffeeScript 1.6.3 (function() { var $, Analyze, Blender, Calculate, Caman, CamanParser, Canvas, Convert, Event, Fiber, Filter, IO, Image, Layer, Log, Module, Pixel, Plugin, Renderer, Root, Store, Util, fs, http, moduleKeywords, slice, vignetteFilters, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __slice = [].slice, __hasProp = {}.hasOwnProperty, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; moduleKeywords = ['extended', 'included']; Module = (function() { function Module() {} Module["extends"] = function(obj) { var key, value, _ref; for (key in obj) { value = obj[key]; if (__indexOf.call(moduleKeywords, key) < 0) { this[key] = value; } } if ((_ref = obj.extended) != null) { _ref.apply(this); } return this; }; Module.includes = function(obj) { var key, value, _ref; for (key in obj) { value = obj[key]; if (__indexOf.call(moduleKeywords, key) < 0) { this.prototype[key] = value; } } if ((_ref = obj.included) != null) { _ref.apply(this); } return this; }; Module.delegate = function() { var args, source, target, _i, _len, _results; args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; target = args.pop(); _results = []; for (_i = 0, _len = args.length; _i < _len; _i++) { source = args[_i]; _results.push(this.prototype[source] = target.prototype[source]); } return _results; }; Module.aliasFunction = function(to, from) { var _this = this; return this.prototype[to] = function() { var args; args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; return _this.prototype[from].apply(_this, args); }; }; Module.aliasProperty = function(to, from) { return Object.defineProperty(this.prototype, to, { get: function() { return this[from]; }, set: function(val) { return this[from] = val; } }); }; Module.included = function(func) { return func.call(this, this.prototype); }; return Module; })(); slice = Array.prototype.slice; $ = function(sel, root) { if (root == null) { root = document; } if (typeof sel === "object" || (typeof exports !== "undefined" && exports !== null)) { return sel; } return root.querySelector(sel); }; Util = (function() { function Util() {} Util.uniqid = (function() { var id; id = 0; return { get: function() { return id++; } }; })(); Util.extend = function() { var copy, dest, obj, prop, src, _i, _len; obj = arguments[0], src = 2 <= arguments.length ? __slice.call(arguments, 1) : []; dest = obj; for (_i = 0, _len = src.length; _i < _len; _i++) { copy = src[_i]; for (prop in copy) { if (!__hasProp.call(copy, prop)) continue; dest[prop] = copy[prop]; } } return dest; }; Util.clampRGB = function(val) { if (val < 0) { return 0; } if (val > 255) { return 255; } return val; }; Util.copyAttributes = function(from, to, opts) { var attr, _i, _len, _ref, _ref1, _results; if (opts == null) { opts = {}; } _ref = from.attributes; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { attr = _ref[_i]; if ((opts.except != null) && (_ref1 = attr.nodeName, __indexOf.call(opts.except, _ref1) >= 0)) { continue; } _results.push(to.setAttribute(attr.nodeName, attr.nodeValue)); } return _results; }; Util.dataArray = function(length) { if (length == null) { length = 0; } if (Caman.NodeJS || (window.Uint8Array != null)) { return new Uint8Array(length); } return new Array(length); }; return Util; })(); if (typeof exports !== "undefined" && exports !== null) { Root = exports; Canvas = require('canvas'); Image = Canvas.Image; Fiber = require('fibers'); fs = require('fs'); http = require('http'); } else { Root = window; } Caman = (function(_super) { __extends(Caman, _super); Caman.version = { release: "4.1.2", date: "7/27/2013" }; Caman.DEBUG = false; Caman.allowRevert = true; Caman.crossOrigin = "anonymous"; Caman.remoteProxy = ""; Caman.proxyParam = "camanProxyUrl"; Caman.NodeJS = typeof exports !== "undefined" && exports !== null; Caman.autoload = !Caman.NodeJS; Caman.toString = function() { return "Version " + Caman.version.release + ", Released " + Caman.version.date; }; Caman.getAttrId = function(canvas) { if (Caman.NodeJS) { return true; } if (typeof canvas === "string") { canvas = $(canvas); } if (!((canvas != null) && (canvas.getAttribute != null))) { return null; } return canvas.getAttribute('data-caman-id'); }; function Caman() { this.nodeFileReady = __bind(this.nodeFileReady, this); var args, callback, id, _this = this; if (arguments.length === 0) { throw "Invalid arguments"; } if (this instanceof Caman) { this.finishInit = this.finishInit.bind(this); this.imageLoaded = this.imageLoaded.bind(this); args = arguments[0]; if (!Caman.NodeJS) { id = parseInt(Caman.getAttrId(args[0]), 10); callback = typeof args[1] === "function" ? args[1] : typeof args[2] === "function" ? args[2] : function() {}; if (!isNaN(id) && Store.has(id)) { return Store.execute(id, callback); } } this.id = Util.uniqid.get(); this.initializedPixelData = this.originalPixelData = null; this.cropCoordinates = { x: 0, y: 0 }; this.cropped = false; this.resized = false; this.pixelStack = []; this.layerStack = []; this.canvasQueue = []; this.currentLayer = null; this.scaled = false; this.analyze = new Analyze(this); this.renderer = new Renderer(this); this.domIsLoaded(function() { _this.parseArguments(args); return _this.setup(); }); return this; } else { return new Caman(arguments); } } Caman.prototype.domIsLoaded = function(cb) { var listener, _this = this; if (Caman.NodeJS) { return setTimeout(function() { return cb.call(_this); }, 0); } else { if (document.readyState === "complete") { Log.debug("DOM initialized"); return setTimeout(function() { return cb.call(_this); }, 0); } else { listener = function() { if (document.readyState === "complete") { Log.debug("DOM initialized"); return cb.call(_this); } }; return document.addEventListener("readystatechange", listener, false); } } }; Caman.prototype.parseArguments = function(args) { var key, val, _ref, _results; if (args.length === 0) { throw "Invalid arguments given"; } this.initObj = null; this.initType = null; this.imageUrl = null; this.callback = function() {}; this.setInitObject(args[0]); if (args.length === 1) { return; } switch (typeof args[1]) { case "string": this.imageUrl = args[1]; break; case "function": this.callback = args[1]; } if (args.length === 2) { return; } this.callback = args[2]; if (args.length === 4) { _ref = args[4]; _results = []; for (key in _ref) { if (!__hasProp.call(_ref, key)) continue; val = _ref[key]; _results.push(this.options[key] = val); } return _results; } }; Caman.prototype.setInitObject = function(obj) { if (Caman.NodeJS) { this.initObj = obj; this.initType = 'node'; return; } if (typeof obj === "object") { this.initObj = obj; } else { this.initObj = $(obj); } if (this.initObj == null) { throw "Could not find image or canvas for initialization."; } return this.initType = this.initObj.nodeName.toLowerCase(); }; Caman.prototype.setup = function() { switch (this.initType) { case "node": return this.initNode(); case "img": return this.initImage(); case "canvas": return this.initCanvas(); } }; Caman.prototype.initNode = function() { Log.debug("Initializing for NodeJS"); if (typeof this.initObj === "string" && this.initObj.match(/^https?:\/\//)) { return this.readFromHttp(this.initObj, this.nodeFileReady); } else if (typeof this.initObj === "string") { return fs.readFile(this.initObj, this.nodeFileReady); } else { return this.nodeFileReady(null, this.initObj); } }; Caman.prototype.readFromHttp = function(url, callback) { var req; Log.debug("Fetching image from " + url); req = http.get(url, function(res) { var buf; buf = ''; res.setEncoding('binary'); res.on('data', function(chunk) { return buf += chunk; }); return res.on('end', function() { return callback(null, new Buffer(buf, 'binary')); }); }); return req.on('error', callback); }; Caman.prototype.nodeFileReady = function(err, data) { if (err) { throw err; } this.image = new Image(); this.image.src = data; Log.debug("Image loaded. Width = " + (this.imageWidth()) + ", Height = " + (this.imageHeight())); this.canvas = new Canvas(this.imageWidth(), this.imageHeight()); return this.finishInit(); }; Caman.prototype.initImage = function() { this.image = this.initObj; this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); Util.copyAttributes(this.image, this.canvas, { except: ['src'] }); if (this.image.parentNode != null) { this.image.parentNode.replaceChild(this.canvas, this.image); } this.imageAdjustments(); return this.waitForImageLoaded(); }; Caman.prototype.initCanvas = function() { this.canvas = this.initObj; this.context = this.canvas.getContext('2d'); if (this.imageUrl != null) { this.image = document.createElement('img'); this.image.src = this.imageUrl; this.imageAdjustments(); return this.waitForImageLoaded(); } else { return this.finishInit(); } }; Caman.prototype.imageAdjustments = function() { if (this.needsHiDPISwap()) { Log.debug(this.image.src, "->", this.hiDPIReplacement()); this.swapped = true; this.image.src = this.hiDPIReplacement(); } if (IO.isRemote(this.image)) { this.image.src = IO.proxyUrl(this.image.src); return Log.debug("Remote image detected, using URL = " + this.image.src); } }; Caman.prototype.waitForImageLoaded = function() { if (this.isImageLoaded()) { return this.imageLoaded(); } else { return this.image.onload = this.imageLoaded; } }; Caman.prototype.isImageLoaded = function() { if (!this.image.complete) { return false; } if ((this.image.naturalWidth != null) && this.image.naturalWidth === 0) { return false; } return true; }; Caman.prototype.imageWidth = function() { return this.image.width || this.image.naturalWidth; }; Caman.prototype.imageHeight = function() { return this.image.height || this.image.naturalHeight; }; Caman.prototype.imageLoaded = function() { Log.debug("Image loaded. Width = " + (this.imageWidth()) + ", Height = " + (this.imageHeight())); if (this.swapped) { this.canvas.width = this.imageWidth() / this.hiDPIRatio(); this.canvas.height = this.imageHeight() / this.hiDPIRatio(); } else { this.canvas.width = this.imageWidth(); this.canvas.height = this.imageHeight(); } return this.finishInit(); }; Caman.prototype.finishInit = function() { var i, pixel, _i, _len, _ref; if (this.context == null) { this.context = this.canvas.getContext('2d'); } this.originalWidth = this.preScaledWidth = this.width = this.canvas.width; this.originalHeight = this.preScaledHeight = this.height = this.canvas.height; this.hiDPIAdjustments(); if (!this.hasId()) { this.assignId(); } if (this.image != null) { this.context.drawImage(this.image, 0, 0, this.imageWidth(), this.imageHeight(), 0, 0, this.preScaledWidth, this.preScaledHeight); } this.imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height); this.pixelData = this.imageData.data; if (Caman.allowRevert) { this.initializedPixelData = Util.dataArray(this.pixelData.length); this.originalPixelData = Util.dataArray(this.pixelData.length); _ref = this.pixelData; for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { pixel = _ref[i]; this.initializedPixelData[i] = pixel; this.originalPixelData[i] = pixel; } } this.dimensions = { width: this.canvas.width, height: this.canvas.height }; if (!Caman.NodeJS) { Store.put(this.id, this); } this.callback.call(this, this); return this.callback = function() {}; }; Caman.prototype.reloadCanvasData = function() { this.imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height); return this.pixelData = this.imageData.data; }; Caman.prototype.resetOriginalPixelData = function() { var i, pixel, _i, _len, _ref, _results; if (!Caman.allowRevert) { throw "Revert disabled"; } this.originalPixelData = Util.dataArray(this.pixelData.length); _ref = this.pixelData; _results = []; for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { pixel = _ref[i]; _results.push(this.originalPixelData[i] = pixel); } return _results; }; Caman.prototype.hasId = function() { return Caman.getAttrId(this.canvas) != null; }; Caman.prototype.assignId = function() { if (Caman.NodeJS || this.canvas.getAttribute('data-caman-id')) { return; } return this.canvas.setAttribute('data-caman-id', this.id); }; Caman.prototype.hiDPIDisabled = function() { return this.canvas.getAttribute('data-caman-hidpi-disabled') !== null; }; Caman.prototype.hiDPIAdjustments = function() { var ratio; if (Caman.NodeJS || !this.needsHiDPISwap()) { return; } ratio = this.hiDPIRatio(); if (ratio !== 1) { Log.debug("HiDPI ratio = " + ratio); this.scaled = true; this.preScaledWidth = this.canvas.width; this.preScaledHeight = this.canvas.height; this.canvas.width = this.preScaledWidth * ratio; this.canvas.height = this.preScaledHeight * ratio; this.canvas.style.width = "" + this.preScaledWidth + "px"; this.canvas.style.height = "" + this.preScaledHeight + "px"; this.context.scale(ratio, ratio); this.width = this.originalWidth = this.canvas.width; return this.height = this.originalHeight = this.canvas.height; } }; Caman.prototype.hiDPIRatio = function() { var backingStoreRatio, devicePixelRatio; devicePixelRatio = window.devicePixelRatio || 1; backingStoreRatio = this.context.webkitBackingStorePixelRatio || this.context.mozBackingStorePixelRatio || this.context.msBackingStorePixelRatio || this.context.oBackingStorePixelRatio || this.context.backingStorePixelRatio || 1; return devicePixelRatio / backingStoreRatio; }; Caman.prototype.hiDPICapable = function() { return (window.devicePixelRatio != null) && window.devicePixelRatio !== 1; }; Caman.prototype.needsHiDPISwap = function() { if (this.hiDPIDisabled() || !this.hiDPICapable()) { return false; } return this.hiDPIReplacement() !== null; }; Caman.prototype.hiDPIReplacement = function() { if (this.image == null) { return null; } return this.image.getAttribute('data-caman-hidpi'); }; Caman.prototype.replaceCanvas = function(newCanvas) { var oldCanvas; oldCanvas = this.canvas; this.canvas = newCanvas; this.context = this.canvas.getContext('2d'); if (!Caman.NodeJS) { oldCanvas.parentNode.replaceChild(this.canvas, oldCanvas); } this.width = this.canvas.width; this.height = this.canvas.height; this.reloadCanvasData(); return this.dimensions = { width: this.canvas.width, height: this.canvas.height }; }; Caman.prototype.render = function(callback) { var _this = this; if (callback == null) { callback = function() {}; } Event.trigger(this, "renderStart"); return this.renderer.execute(function() { _this.context.putImageData(_this.imageData, 0, 0); return callback.call(_this); }); }; Caman.prototype.revert = function(updateContext) { var i, pixel, _i, _len, _ref; if (updateContext == null) { updateContext = true; } if (!Caman.allowRevert) { throw "Revert disabled"; } _ref = this.originalVisiblePixels(); for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { pixel = _ref[i]; this.pixelData[i] = pixel; } if (updateContext) { return this.context.putImageData(this.imageData, 0, 0); } }; Caman.prototype.reset = function() { var canvas, ctx, i, imageData, pixel, pixelData, _i, _len, _ref; canvas = document.createElement('canvas'); Util.copyAttributes(this.canvas, canvas); canvas.width = this.originalWidth; canvas.height = this.originalHeight; ctx = canvas.getContext('2d'); imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); pixelData = imageData.data; _ref = this.initializedPixelData; for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { pixel = _ref[i]; pixelData[i] = pixel; } ctx.putImageData(imageData, 0, 0); this.cropCoordinates = { x: 0, y: 0 }; this.resized = false; return this.replaceCanvas(canvas); }; Caman.prototype.originalVisiblePixels = function() { var canvas, coord, ctx, endX, endY, i, imageData, pixel, pixelData, pixels, scaledCanvas, startX, startY, width, _i, _j, _len, _ref, _ref1, _ref2, _ref3; if (!Caman.allowRevert) { throw "Revert disabled"; } pixels = []; startX = this.cropCoordinates.x; endX = startX + this.width; startY = this.cropCoordinates.y; endY = startY + this.height; if (this.resized) { canvas = document.createElement('canvas'); canvas.width = this.originalWidth; canvas.height = this.originalHeight; ctx = canvas.getContext('2d'); imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); pixelData = imageData.data; _ref = this.originalPixelData; for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { pixel = _ref[i]; pixelData[i] = pixel; } ctx.putImageData(imageData, 0, 0); scaledCanvas = document.createElement('canvas'); scaledCanvas.width = this.width; scaledCanvas.height = this.height; ctx = scaledCanvas.getContext('2d'); ctx.drawImage(canvas, 0, 0, this.originalWidth, this.originalHeight, 0, 0, this.width, this.height); pixelData = ctx.getImageData(0, 0, this.width, this.height).data; width = this.width; } else { pixelData = this.originalPixelData; width = this.originalWidth; } for (i = _j = 0, _ref1 = pixelData.length; _j < _ref1; i = _j += 4) { coord = Pixel.locationToCoordinates(i, width); if (((startX <= (_ref2 = coord.x) && _ref2 < endX)) && ((startY <= (_ref3 = coord.y) && _ref3 < endY))) { pixels.push(pixelData[i], pixelData[i + 1], pixelData[i + 2], pixelData[i + 3]); } } return pixels; }; Caman.prototype.process = function(name, processFn) { this.renderer.add({ type: Filter.Type.Single, name: name, processFn: processFn }); return this; }; Caman.prototype.processKernel = function(name, adjust, divisor, bias) { var i, _i, _ref; if (divisor == null) { divisor = null; } if (bias == null) { bias = 0; } if (divisor == null) { divisor = 0; for (i = _i = 0, _ref = adjust.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { divisor += adjust[i]; } } this.renderer.add({ type: Filter.Type.Kernel, name: name, adjust: adjust, divisor: divisor, bias: bias }); return this; }; Caman.prototype.processPlugin = function(plugin, args) { this.renderer.add({ type: Filter.Type.Plugin, plugin: plugin, args: args }); return this; }; Caman.prototype.newLayer = function(callback) { var layer; layer = new Layer(this); this.canvasQueue.push(layer); this.renderer.add({ type: Filter.Type.LayerDequeue }); callback.call(layer); this.renderer.add({ type: Filter.Type.LayerFinished }); return this; }; Caman.prototype.executeLayer = function(layer) { return this.pushContext(layer); }; Caman.prototype.pushContext = function(layer) { this.layerStack.push(this.currentLayer); this.pixelStack.push(this.pixelData); this.currentLayer = layer; return this.pixelData = layer.pixelData; }; Caman.prototype.popContext = function() { this.pixelData = this.pixelStack.pop(); return this.currentLayer = this.layerStack.pop(); }; Caman.prototype.applyCurrentLayer = function() { return this.currentLayer.applyToParent(); }; return Caman; })(Module); Root.Caman = Caman; Caman.Analyze = (function() { function Analyze(c) { this.c = c; } Analyze.prototype.calculateLevels = function() { var i, levels, numPixels, _i, _j, _k, _ref; levels = { r: {}, g: {}, b: {} }; for (i = _i = 0; _i <= 255; i = ++_i) { levels.r[i] = 0; levels.g[i] = 0; levels.b[i] = 0; } for (i = _j = 0, _ref = this.c.pixelData.length; _j < _ref; i = _j += 4) { levels.r[this.c.pixelData[i]]++; levels.g[this.c.pixelData[i + 1]]++; levels.b[this.c.pixelData[i + 2]]++; } numPixels = this.c.pixelData.length / 4; for (i = _k = 0; _k <= 255; i = ++_k) { levels.r[i] /= numPixels; levels.g[i] /= numPixels; levels.b[i] /= numPixels; } return levels; }; return Analyze; })(); Analyze = Caman.Analyze; Caman.DOMUpdated = function() { var img, imgs, parser, _i, _len, _results; imgs = document.querySelectorAll("img[data-caman]"); if (!(imgs.length > 0)) { return; } _results = []; for (_i = 0, _len = imgs.length; _i < _len; _i++) { img = imgs[_i]; _results.push(parser = new CamanParser(img, function() { this.parse(); return this.execute(); })); } return _results; }; if (Caman.autoload) { (function() { if (document.readyState === "complete") { return Caman.DOMUpdated(); } else { return document.addEventListener("DOMContentLoaded", Caman.DOMUpdated, false); } })(); } CamanParser = (function() { var INST_REGEX; INST_REGEX = "(\\w+)\\((.*?)\\)"; function CamanParser(ele, ready) { this.dataStr = ele.getAttribute('data-caman'); this.caman = Caman(ele, ready.bind(this)); } CamanParser.prototype.parse = function() { var args, e, filter, func, inst, instFunc, m, r, unparsedInstructions, _i, _len, _ref, _results; this.ele = this.caman.canvas; r = new RegExp(INST_REGEX, 'g'); unparsedInstructions = this.dataStr.match(r); if (!(unparsedInstructions.length > 0)) { return; } r = new RegExp(INST_REGEX); _results = []; for (_i = 0, _len = unparsedInstructions.length; _i < _len; _i++) { inst = unparsedInstructions[_i]; _ref = inst.match(r), m = _ref[0], filter = _ref[1], args = _ref[2]; instFunc = new Function("return function() { this." + filter + "(" + args + "); };"); try { func = instFunc(); _results.push(func.call(this.caman)); } catch (_error) { e = _error; _results.push(Log.debug(e)); } } return _results; }; CamanParser.prototype.execute = function() { var ele; ele = this.ele; return this.caman.render(function() { return ele.parentNode.replaceChild(this.toImage(), ele); }); }; return CamanParser; })(); Caman.Blender = (function() { function Blender() {} Blender.blenders = {}; Blender.register = function(name, func) { return this.blenders[name] = func; }; Blender.execute = function(name, rgbaLayer, rgbaParent) { return this.blenders[name](rgbaLayer, rgbaParent); }; return Blender; })(); Blender = Caman.Blender; Caman.Calculate = (function() { function Calculate() {} Calculate.distance = function(x1, y1, x2, y2) { return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); }; Calculate.randomRange = function(min, max, getFloat) { var rand; if (getFloat == null) { getFloat = false; } rand = min + (Math.random() * (max - min)); if (getFloat) { return rand.toFixed(getFloat); } else { return Math.round(rand); } }; Calculate.luminance = function(rgba) { return (0.299 * rgba.r) + (0.587 * rgba.g) + (0.114 * rgba.b); }; Calculate.bezier = function(start, ctrl1, ctrl2, end, lowBound, highBound) { var bezier, clamp, controlPoints, endX, i, j, lerp, next, prev, t, _i, _j, _ref; if (lowBound == null) { lowBound = 0; } if (highBound == null) { highBound = 255; } if (start[0] instanceof Array) { controlPoints = start; lowBound = ctrl1; highBound = ctrl2; } else { controlPoints = [start, ctrl1, ctrl2, end]; } if (controlPoints.length < 2) { throw "Invalid number of arguments to bezier"; } bezier = {}; lerp = function(a, b, t) { return a * (1 - t) + b * t; }; clamp = function(a, min, max) { return Math.min(Math.max(a, min), max); }; for (i = _i = 0; _i < 1000; i = ++_i) { t = i / 1000; prev = controlPoints; while (prev.length > 1) { next = []; for (j = _j = 0, _ref = prev.length - 2; 0 <= _ref ? _j <= _ref : _j >= _ref; j = 0 <= _ref ? ++_j : --_j) { next.push([lerp(prev[j][0], prev[j + 1][0], t), lerp(prev[j][1], prev[j + 1][1], t)]); } prev = next; } bezier[Math.round(prev[0][0])] = Math.round(clamp(prev[0][1], lowBound, highBound)); } endX = controlPoints[controlPoints.length - 1][0]; bezier = Caman.Calculate.missingValues(bezier, endX); if (bezier[endX] == null) { bezier[endX] = bezier[endX - 1]; } return bezier; }; Calculate.hermite = function(controlPoints, lowBound, highBound) { var add, clamp, count, endX, fac0, fac1, fac2, fac3, i, j, lerp, m0, m1, mul, p, p0, p1, pointsPerSegment, pointsPerStep, pos, ret, sub, t, _i, _j, _ref, _this = this; if (controlPoints.length < 2) { throw "Invalid number of arguments to hermite"; } ret = {}; lerp = function(a, b, t) { return a * (1 - t) + b * t; }; add = function(a, b, c, d) { return [a[0] + b[0] + c[0] + d[0], a[1] + b[1] + c[1] + d[1]]; }; mul = function(a, b) { return [a[0] * b[0], a[1] * b[1]]; }; sub = function(a, b) { return [a[0] - b[0], a[1] - b[1]]; }; clamp = function(a, min, max) { return Math.min(Math.max(a, min), max); }; count = 0; for (i = _i = 0, _ref = controlPoints.length - 2; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) { p0 = controlPoints[i]; p1 = controlPoints[i + 1]; pointsPerSegment = p1[0] - p0[0]; pointsPerStep = 1 / pointsPerSegment; if (i === controlPoints.length - 2) { pointsPerStep = 1 / (pointsPerSegment - 1); } p = i > 0 ? controlPoints[i - 1] : p0; m0 = mul(sub(p1, p), [0.5, 0.5]); p = i < controlPoints.length - 2 ? controlPoints[i + 2] : p1; m1 = mul(sub(p, p0), [0.5, 0.5]); for (j = _j = 0; 0 <= pointsPerSegment ? _j <= pointsPerSegment : _j >= pointsPerSegment; j = 0 <= pointsPerSegment ? ++_j : --_j) { t = j * pointsPerStep; fac0 = 2.0 * t * t * t - 3.0 * t * t + 1.0; fac1 = t * t * t - 2.0 * t * t + t; fac2 = -2.0 * t * t * t + 3.0 * t * t; fac3 = t * t * t - t * t; pos = add(mul(p0, [fac0, fac0]), mul(m0, [fac1, fac1]), mul(p1, [fac2, fac2]), mul(m1, [fac3, fac3])); ret[Math.round(pos[0])] = Math.round(clamp(pos[1], lowBound, highBound)); count += 1; } } endX = controlPoints[controlPoints.length - 1][0]; ret = Caman.Calculate.missingValues(ret, endX); return ret; }; Calculate.missingValues = function(values, endX) { var i, j, leftCoord, ret, rightCoord, _i, _j; if (Object.keys(values).length < endX + 1) { ret = {}; for (i = _i = 0; 0 <= endX ? _i <= endX : _i >= endX; i = 0 <= endX ? ++_i : --_i) { if (values[i] != null) { ret[i] = values[i]; } else { leftCoord = [i - 1, ret[i - 1]]; for (j = _j = i; i <= endX ? _j <= endX : _j >= endX; j = i <= endX ? ++_j : --_j) { if (values[j] != null) { rightCoord = [j, values[j]]; break; } } ret[i] = leftCoord[1] + ((rightCoord[1] - leftCoord[1]) / (rightCoord[0] - leftCoord[0])) * (i - leftCoord[0]); } } return ret; } return values; }; return Calculate; })(); Calculate = Caman.Calculate; Caman.Convert = (function() { function Convert() {} Convert.hexToRGB = function(hex) { var b, g, r; if (hex.charAt(0) === "#") { hex = hex.substr(1); } r = parseInt(hex.substr(0, 2), 16); g = parseInt(hex.substr(2, 2), 16); b = parseInt(hex.substr(4, 2), 16); return { r: r, g: g, b: b }; }; Convert.rgbToHSL = function(r, g, b) { var d, h, l, max, min, s; if (typeof r === "object") { g = r.g; b = r.b; r = r.r; } r /= 255; g /= 255; b /= 255; max = Math.max(r, g, b); min = Math.min(r, g, b); l = (max + min) / 2; if (max === min) { h = s = 0; } else { d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); h = (function() { switch (max) { case r: return (g - b) / d + (g < b ? 6 : 0); case g: return (b - r) / d + 2; case b: return (r - g) / d + 4; } })(); h /= 6; } return { h: h, s: s, l: l }; }; Convert.hslToRGB = function(h, s, l) { var b, g, p, q, r; if (typeof h === "object") { s = h.s; l = h.l; h = h.h; } if (s === 0) { r = g = b = l; } else { q = l < 0.5 ? l * (1 + s) : l + s - l * s; p = 2 * l - q; r = this.hueToRGB(p, q, h + 1 / 3); g = this.hueToRGB(p, q, h); b = this.hueToRGB(p, q, h - 1 / 3); } return { r: r * 255, g: g * 255, b: b * 255 }; }; Convert.hueToRGB = function(p, q, t) { if (t < 0) { t += 1; } if (t > 1) { t -= 1; } if (t < 1 / 6) { return p + (q - p) * 6 * t; } if (t < 1 / 2) { return q; } if (t < 2 / 3) { return p + (q - p) * (2 / 3 - t) * 6; } return p; }; Convert.rgbToHSV = function(r, g, b) { var d, h, max, min, s, v; r /= 255; g /= 255; b /= 255; max = Math.max(r, g, b); min = Math.min(r, g, b); v = max; d = max - min; s = max === 0 ? 0 : d / max; if (max === min) { h = 0; } else { h = (function() { switch (max) { case r: return (g - b) / d + (g < b ? 6 : 0); case g: return (b - r) / d + 2; case b: return (r - g) / d + 4; } })(); h /= 6; } return { h: h, s: s, v: v }; }; Convert.hsvToRGB = function(h, s, v) { var b, f, g, i, p, q, r, t; i = Math.floor(h * 6); f = h * 6 - i; p = v * (1 - s); q = v * (1 - f * s); t = v * (1 - (1 - f) * s); switch (i % 6) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; case 5: r = v; g = p; b = q; } return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; }; Convert.rgbToXYZ = function(r, g, b) { var x, y, z; r /= 255; g /= 255; b /= 255; if (r > 0.04045) { r = Math.pow((r + 0.055) / 1.055, 2.4); } else { r /= 12.92; } if (g > 0.04045) { g = Math.pow((g + 0.055) / 1.055, 2.4); } else { g /= 12.92; } if (b > 0.04045) { b = Math.pow((b + 0.055) / 1.055, 2.4); } else { b /= 12.92; } x = r * 0.4124 + g * 0.3576 + b * 0.1805; y = r * 0.2126 + g * 0.7152 + b * 0.0722; z = r * 0.0193 + g * 0.1192 + b * 0.9505; return { x: x * 100, y: y * 100, z: z * 100 }; }; Convert.xyzToRGB = function(x, y, z) { var b, g, r; x /= 100; y /= 100; z /= 100; r = (3.2406 * x) + (-1.5372 * y) + (-0.4986 * z); g = (-0.9689 * x) + (1.8758 * y) + (0.0415 * z); b = (0.0557 * x) + (-0.2040 * y) + (1.0570 * z); if (r > 0.0031308) { r = (1.055 * Math.pow(r, 0.4166666667)) - 0.055; } else { r *= 12.92; } if (g > 0.0031308) { g = (1.055 * Math.pow(g, 0.4166666667)) - 0.055; } else { g *= 12.92; } if (b > 0.0031308) { b = (1.055 * Math.pow(b, 0.4166666667)) - 0.055; } else { b *= 12.92; } return { r: r * 255, g: g * 255, b: b * 255 }; }; Convert.xyzToLab = function(x, y, z) { var a, b, l, whiteX, whiteY, whiteZ; if (typeof x === "object") { y = x.y; z = x.z; x = x.x; } whiteX = 95.047; whiteY = 100.0; whiteZ = 108.883; x /= whiteX; y /= whiteY; z /= whiteZ; if (x > 0.008856451679) { x = Math.pow(x, 0.3333333333); } else { x = (7.787037037 * x) + 0.1379310345; } if (y > 0.008856451679) { y = Math.pow(y, 0.3333333333); } else { y = (7.787037037 * y) + 0.1379310345; } if (z > 0.008856451679) { z = Math.pow(z, 0.3333333333); } else { z = (7.787037037 * z) + 0.1379310345; } l = 116 * y - 16; a = 500 * (x - y); b = 200 * (y - z); return { l: l, a: a, b: b }; }; Convert.labToXYZ = function(l, a, b) { var x, y, z; if (typeof l === "object") { a = l.a; b = l.b; l = l.l; } y = (l + 16) / 116; x = y + (a / 500); z = y - (b / 200); if (x > 0.2068965517) { x = x * x * x; } else { x = 0.1284185493 * (x - 0.1379310345); } if (y > 0.2068965517) { y = y * y * y; } else { y = 0.1284185493 * (y - 0.1379310345); } if (z > 0.2068965517) { z = z * z * z; } else { z = 0.1284185493 * (z - 0.1379310345); } return { x: x * 95.047, y: y * 100.0, z: z * 108.883 }; }; Convert.rgbToLab = function(r, g, b) { var xyz; if (typeof r === "object") { g = r.g; b = r.b; r = r.r; } xyz = this.rgbToXYZ(r, g, b); return this.xyzToLab(xyz); }; Convert.labToRGB = function(l, a, b) {}; return Convert; })(); Convert = Caman.Convert; Caman.Event = (function() { function Event() {} Event.events = {}; Event.types = ["processStart", "processComplete", "renderStart", "renderFinished", "blockStarted", "blockFinished"]; Event.trigger = function(target, type, data) { var event, _i, _len, _ref, _results; if (data == null) { data = null; } if (this.events[type] && this.events[type].length) { _ref = this.events[type]; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { event = _ref[_i]; if (event.target === null || target.id === event.target.id) { _results.push(event.fn.call(target, data)); } else { _results.push(void 0); } } return _results; } }; Event.listen = function(target, type, fn) { var _fn, _type; if (typeof target === "string") { _type = target; _fn = type; target = null; type = _type; fn = _fn; } if (__indexOf.call(this.types, type) < 0) { return false; } if (!this.events[type]) { this.events[type] = []; } this.events[type].push({ target: target, fn: fn }); return true; }; return Event; })(); Event = Caman.Event; Caman.Filter = (function() { function Filter() {} Filter.Type = { Single: 1, Kernel: 2, LayerDequeue: 3, LayerFinished: 4, LoadOverlay: 5, Plugin: 6 }; Filter.register = function(name, filterFunc) { return Caman.prototype[name] = filterFunc; }; return Filter; })(); Filter = Caman.Filter; Caman.IO = (function() { function IO() {} IO.domainRegex = /(?:(?:http|https):\/\/)((?:\w+)\.(?:(?:\w|\.)+))/; IO.isRemote = function(img) { if (img == null) { return false; } if (this.corsEnabled(img)) { return false; } return this.isURLRemote(img.src); }; IO.corsEnabled = function(img) { var _ref; return (img.crossOrigin != null) && ((_ref = img.crossOrigin.toLowerCase()) === 'anonymous' || _ref === 'use-credentials'); }; IO.isURLRemote = function(url) { var matches; matches = url.match(this.domainRegex); if (matches) { return matches[1] !== document.domain; } else { return false; } }; IO.remoteCheck = function(src) { if (this.isURLRemote(src)) { if (!Caman.remoteProxy.length) { Log.info("Attempting to load a remote image without a configured proxy. URL: " + src); } else { if (Caman.isURLRemote(Caman.remoteProxy)) { Log.info("Cannot use a remote proxy for loading images."); return; } return this.proxyUrl(src); } } }; IO.proxyUrl = function(src) { return "" + Caman.remoteProxy + "?" + Caman.proxyParam + "=" + (encodeURIComponent(src)); }; IO.useProxy = function(lang) { var langToExt; langToExt = { ruby: 'rb', python: 'py', perl: 'pl', javascript: 'js' }; lang = lang.toLowerCase(); if (langToExt[lang] != null) { lang = langToExt[lang]; } return "proxies/caman_proxy." + lang; }; return IO; })(); Caman.prototype.save = function() { if (typeof exports !== "undefined" && exports !== null) { return this.nodeSave.apply(this, arguments); } else { return this.browserSave.apply(this, arguments); } }; Caman.prototype.browserSave = function(type) { var image; if (type == null) { type = "png"; } type = type.toLowerCase(); image = this.toBase64(type).replace("image/" + type, "image/octet-stream"); return document.location.href = image; }; Caman.prototype.nodeSave = function(file, overwrite, callback) { var e, stats; if (overwrite == null) { overwrite = true; } if (callback == null) { callback = null; } try { stats = fs.statSync(file); if (stats.isFile() && !overwrite) { return false; } } catch (_error) { e = _error; Log.debug("Creating output file " + file); } return fs.writeFile(file, this.canvas.toBuffer(), function(err) { Log.debug("Finished writing to " + file); if (callback) { return callback.call(this, err); } }); }; Caman.prototype.toImage = function(type) { var img; img = new Image(); img.src = this.toBase64(type); img.width = this.dimensions.width; img.height = this.dimensions.height; if (window.devicePixelRatio) { img.width /= window.devicePixelRatio; img.height /= window.devicePixelRatio; } return img; }; Caman.prototype.toBase64 = function(type) { if (type == null) { type = "png"; } type = type.toLowerCase(); return this.canvas.toDataURL("image/" + type); }; IO = Caman.IO; Caman.Layer = (function() { function Layer(c) { this.c = c; this.filter = this.c; this.options = { blendingMode: 'normal', opacity: 1.0 }; this.layerID = Util.uniqid.get(); this.canvas = typeof exports !== "undefined" && exports !== null ? new Canvas() : document.createElement('canvas'); this.canvas.width = this.c.dimensions.width; this.canvas.height = this.c.dimensions.height; this.context = this.canvas.getContext('2d'); this.context.createImageData(this.canvas.width, this.canvas.height); this.imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height); this.pixelData = this.imageData.data; } Layer.prototype.newLayer = function(cb) { return this.c.newLayer.call(this.c, cb); }; Layer.prototype.setBlendingMode = function(mode) { this.options.blendingMode = mode; return this; }; Layer.prototype.opacity = function(opacity) { this.options.opacity = opacity / 100; return this; }; Layer.prototype.copyParent = function() { var i, parentData, _i, _ref; parentData = this.c.pixelData; for (i = _i = 0, _ref = this.c.pixelData.length; _i < _ref; i = _i += 4) { this.pixelData[i] = parentData[i]; this.pixelData[i + 1] = parentData[i + 1]; this.pixelData[i + 2] = parentData[i + 2]; this.pixelData[i + 3] = parentData[i + 3]; } return this; }; Layer.prototype.fillColor = function() { return this.c.fillColor.apply(this.c, arguments); }; Layer.prototype.overlayImage = function(image) { if (typeof image === "object") { image = image.src; } else if (typeof image === "string" && image[0] === "#") { image = $(image).src; } if (!image) { return this; } this.c.renderer.renderQueue.push({ type: Filter.Type.LoadOverlay, src: image, layer: this }); return this; }; Layer.prototype.applyToParent = function() { var i, layerData, parentData, result, rgbaLayer, rgbaParent, _i, _ref, _results; parentData = this.c.pixelStack[this.c.pixelStack.length - 1]; layerData = this.c.pixelData; _results = []; for (i = _i = 0, _ref = layerData.length; _i < _ref; i = _i += 4) { rgbaParent = { r: parentData[i], g: parentData[i + 1], b: parentData[i + 2], a: parentData[i + 3] }; rgbaLayer = { r: layerData[i], g: layerData[i + 1], b: layerData[i + 2], a: layerData[i + 3] }; result = Blender.execute(this.options.blendingMode, rgbaLayer, rgbaParent); result.r = Util.clampRGB(result.r); result.g = Util.clampRGB(result.g); result.b = Util.clampRGB(result.b); if (result.a == null) { result.a = rgbaLayer.a; } parentData[i] = rgbaParent.r - ((rgbaParent.r - result.r) * (this.options.opacity * (result.a / 255))); parentData[i + 1] = rgbaParent.g - ((rgbaParent.g - result.g) * (this.options.opacity * (result.a / 255))); _results.push(parentData[i + 2] = rgbaParent.b - ((rgbaParent.b - result.b) * (this.options.opacity * (result.a / 255)))); } return _results; }; return Layer; })(); Layer = Caman.Layer; Caman.Logger = (function() { function Logger() { var name, _i, _len, _ref; _ref = ['log', 'info', 'warn', 'error']; for (_i = 0, _len = _ref.length; _i < _len; _i++) { name = _ref[_i]; this[name] = (function(name) { return function() { var args, e; args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; if (!Caman.DEBUG) { return; } try { return console[name].apply(console, args); } catch (_error) { e = _error; return console[name](args); } }; })(name); } this.debug = this.log; } return Logger; })(); Log = new Caman.Logger(); Caman.Pixel = (function() { Pixel.coordinatesToLocation = function(x, y, width) { return (y * width + x) * 4; }; Pixel.locationToCoordinates = function(loc, width) { var x, y; y = Math.floor(loc / (width * 4)); x = (loc % (width * 4)) / 4; return { x: x, y: y }; }; function Pixel(r, g, b, a, c) { this.r = r != null ? r : 0; this.g = g != null ? g : 0; this.b = b != null ? b : 0; this.a = a != null ? a : 255; this.c = c != null ? c : null; this.loc = 0; } Pixel.prototype.setContext = function(c) { return this.c = c; }; Pixel.prototype.locationXY = function() { var x, y; if (this.c == null) { throw "Requires a CamanJS context"; } y = this.c.dimensions.height - Math.floor(this.loc / (this.c.dimensions.width * 4)); x = (this.loc % (this.c.dimensions.width * 4)) / 4; return { x: x, y: y }; }; Pixel.prototype.pixelAtLocation = function(loc) { if (this.c == null) { throw "Requires a CamanJS context"; } return new Pixel(this.c.pixelData[loc], this.c.pixelData[loc + 1], this.c.pixelData[loc + 2], this.c.pixelData[loc + 3], this.c); }; Pixel.prototype.getPixelRelative = function(horiz, vert) { var newLoc; if (this.c == null) { throw "Requires a CamanJS context"; } newLoc = this.loc + (this.c.dimensions.width * 4 * (vert * -1)) + (4 * horiz); if (newLoc > this.c.pixelData.length || newLoc < 0) { return new Pixel(0, 0, 0, 255, this.c); } return this.pixelAtLocation(newLoc); }; Pixel.prototype.putPixelRelative = function(horiz, vert, rgba) { var nowLoc; if (this.c == null) { throw "Requires a CamanJS context"; } nowLoc = this.loc + (this.c.dimensions.width * 4 * (vert * -1)) + (4 * horiz); if (newLoc > this.c.pixelData.length || newLoc < 0) { return; } this.c.pixelData[newLoc] = rgba.r; this.c.pixelData[newLoc + 1] = rgba.g; this.c.pixelData[newLoc + 2] = rgba.b; this.c.pixelData[newLoc + 3] = rgba.a; return true; }; Pixel.prototype.getPixel = function(x, y) { var loc; if (this.c == null) { throw "Requires a CamanJS context"; } loc = this.coordinatesToLocation(x, y, this.width); return this.pixelAtLocation(loc); }; Pixel.prototype.putPixel = function(x, y, rgba) { var loc; if (this.c == null) { throw "Requires a CamanJS context"; } loc = this.coordinatesToLocation(x, y, this.width); this.c.pixelData[loc] = rgba.r; this.c.pixelData[loc + 1] = rgba.g; this.c.pixelData[loc + 2] = rgba.b; return this.c.pixelData[loc + 3] = rgba.a; }; Pixel.prototype.toString = function() { return this.toKey(); }; Pixel.prototype.toHex = function(includeAlpha) { var hex; if (includeAlpha == null) { includeAlpha = false; } hex = '#' + this.r.toString(16) + this.g.toString(16) + this.b.toString(16); if (includeAlpha) { return hex + this.a.toString(16); } else { return hex; } }; return Pixel; })(); Pixel = Caman.Pixel; Caman.Plugin = (function() { function Plugin() {} Plugin.plugins = {}; Plugin.register = function(name, plugin) { return this.plugins[name] = plugin; }; Plugin.execute = function(context, name, args) { return this.plugins[name].apply(context, args); }; return Plugin; })(); Plugin = Caman.Plugin; Caman.Renderer = (function() { Renderer.Blocks = Caman.NodeJS ? require('os').cpus().length : 4; function Renderer(c) { this.c = c; this.processNext = __bind(this.processNext, this); this.renderQueue = []; this.modPixelData = null; } Renderer.prototype.add = function(job) { if (job == null) { return; } return this.renderQueue.push(job); }; Renderer.prototype.processNext = function() { var layer; if (this.renderQueue.length === 0) { Event.trigger(this, "renderFinished"); if (this.finishedFn != null) { this.finishedFn.call(this.c); } return this; } this.currentJob = this.renderQueue.shift(); switch (this.currentJob.type) { case Filter.Type.LayerDequeue: layer = this.c.canvasQueue.shift(); this.c.executeLayer(layer); return this.processNext(); case Filter.Type.LayerFinished: this.c.applyCurrentLayer(); this.c.popContext(); return this.processNext(); case Filter.Type.LoadOverlay: return this.loadOverlay(this.currentJob.layer, this.currentJob.src); case Filter.Type.Plugin: return this.executePlugin(); default: return this.executeFilter(); } }; Renderer.prototype.execute = function(callback) { this.finishedFn = callback; this.modPixelData = Util.dataArray(this.c.pixelData.length); return this.processNext(); }; Renderer.prototype.eachBlock = function(fn) { var blockN, blockPixelLength, bnum, end, f, i, lastBlockN, n, start, _i, _ref, _results, _this = this; this.blocksDone = 0; n = this.c.pixelData.length; blockPixelLength = Math.floor((n / 4) / Renderer.Blocks); blockN = blockPixelLength * 4; lastBlockN = blockN + ((n / 4) % Renderer.Blocks) * 4; _results = []; for (i = _i = 0, _ref = Renderer.Blocks; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { start = i * blockN; end = start + (i === Renderer.Blocks - 1 ? lastBlockN : blockN); if (Caman.NodeJS) { f = Fiber(function() { return fn.call(_this, i, start, end); }); bnum = f.run(); _results.push(this.blockFinished(bnum)); } else { _results.push(setTimeout((function(i, start, end) { return function() { return fn.call(_this, i, start, end); }; })(i, start, end), 0)); } } return _results; }; Renderer.prototype.executeFilter = function() { Event.trigger(this.c, "processStart", this.currentJob); if (this.currentJob.type === Filter.Type.Single) { return this.eachBlock(this.renderBlock); } else { return this.eachBlock(this.renderKernel); } }; Renderer.prototype.executePlugin = function() { Log.debug("Executing plugin " + this.currentJob.plugin); Plugin.execute(this.c, this.currentJob.plugin, this.currentJob.args); Log.debug("Plugin " + this.currentJob.plugin + " finished!"); return this.processNext(); }; Renderer.prototype.renderBlock = function(bnum, start, end) { var i, pixel, _i; Log.debug("Block #" + bnum + " - Filter: " + this.currentJob.name + ", Start: " + start + ", End: " + end); Event.trigger(this.c, "blockStarted", { blockNum: bnum, totalBlocks: Renderer.Blocks, startPixel: start, endPixel: end }); pixel = new Pixel(); pixel.setContext(this.c); for (i = _i = start; _i < end; i = _i += 4) { pixel.loc = i; pixel.r = this.c.pixelData[i]; pixel.g = this.c.pixelData[i + 1]; pixel.b = this.c.pixelData[i + 2]; pixel.a = this.c.pixelData[i + 3]; this.currentJob.processFn(pixel); this.c.pixelData[i] = Util.clampRGB(pixel.r); this.c.pixelData[i + 1] = Util.clampRGB(pixel.g); this.c.pixelData[i + 2] = Util.clampRGB(pixel.b); this.c.pixelData[i + 3] = Util.clampRGB(pixel.a); } if (Caman.NodeJS) { return Fiber["yield"](bnum); } else { return this.blockFinished(bnum); } }; Renderer.prototype.renderKernel = function(bnum, start, end) { var adjust, adjustSize, bias, builder, builderIndex, divisor, i, j, k, kernel, n, name, p, pixel, res, _i, _j, _k; name = this.currentJob.name; bias = this.currentJob.bias; divisor = this.currentJob.divisor; n = this.c.pixelData.length; adjust = this.currentJob.adjust; adjustSize = Math.sqrt(adjust.length); kernel = []; Log.debug("Rendering kernel - Filter: " + this.currentJob.name); start = Math.max(start, this.c.dimensions.width * 4 * ((adjustSize - 1) / 2)); end = Math.min(end, n - (this.c.dimensions.width * 4 * ((adjustSize - 1) / 2))); builder = (adjustSize - 1) / 2; pixel = new Pixel(); pixel.setContext(this.c); for (i = _i = start; _i < end; i = _i += 4) { pixel.loc = i; builderIndex = 0; for (j = _j = -builder; -builder <= builder ? _j <= builder : _j >= builder; j = -builder <= builder ? ++_j : --_j) { for (k = _k = builder; builder <= -builder ? _k <= -builder : _k >= -builder; k = builder <= -builder ? ++_k : --_k) { p = pixel.getPixelRelative(j, k); kernel[builderIndex * 3] = p.r; kernel[builderIndex * 3 + 1] = p.g; kernel[builderIndex * 3 + 2] = p.b; builderIndex++; } } res = this.processKernel(adjust, kernel, divisor, bias); this.modPixelData[i] = Util.clampRGB(res.r); this.modPixelData[i + 1] = Util.clampRGB(res.g); this.modPixelData[i + 2] = Util.clampRGB(res.b); this.modPixelData[i + 3] = this.c.pixelData[i + 3]; } if (Caman.NodeJS) { return Fiber["yield"](bnum); } else { return this.blockFinished(bnum); } }; Renderer.prototype.blockFinished = function(bnum) { var i, _i, _ref; if (bnum >= 0) { Log.debug("Block #" + bnum + " finished! Filter: " + this.currentJob.name); } this.blocksDone++; Event.trigger(this.c, "blockFinished", { blockNum: bnum, blocksFinished: this.blocksDone, totalBlocks: Renderer.Blocks }); if (this.blocksDone === Renderer.Blocks) { if (this.currentJob.type === Filter.Type.Kernel) { for (i = _i = 0, _ref = this.c.pixelData.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { this.c.pixelData[i] = this.modPixelData[i]; } } if (bnum >= 0) { Log.debug("Filter " + this.currentJob.name + " finished!"); } Event.trigger(this.c, "processComplete", this.currentJob); return this.processNext(); } }; Renderer.prototype.processKernel = function(adjust, kernel, divisor, bias) { var i, val, _i, _ref; val = { r: 0, g: 0, b: 0 }; for (i = _i = 0, _ref = adjust.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { val.r += adjust[i] * kernel[i * 3]; val.g += adjust[i] * kernel[i * 3 + 1]; val.b += adjust[i] * kernel[i * 3 + 2]; } val.r = (val.r / divisor) + bias; val.g = (val.g / divisor) + bias; val.b = (val.b / divisor) + bias; return val; }; Renderer.prototype.loadOverlay = function(layer, src) { var img, proxyUrl, _this = this; img = new Image(); img.onload = function() { layer.context.drawImage(img, 0, 0, _this.c.dimensions.width, _this.c.dimensions.height); layer.imageData = layer.context.getImageData(0, 0, _this.c.dimensions.width, _this.c.dimensions.height); layer.pixelData = layer.imageData.data; _this.c.pixelData = layer.pixelData; return _this.processNext(); }; proxyUrl = IO.remoteCheck(src); return img.src = proxyUrl != null ? proxyUrl : src; }; return Renderer; })(); Renderer = Caman.Renderer; Caman.Store = (function() { function Store() {} Store.items = {}; Store.has = function(search) { return this.items[search] != null; }; Store.get = function(search) { return this.items[search]; }; Store.put = function(name, obj) { return this.items[name] = obj; }; Store.execute = function(search, callback) { var _this = this; setTimeout(function() { return callback.call(_this.get(search), _this.get(search)); }, 0); return this.get(search); }; Store.flush = function(name) { if (name == null) { name = false; } if (name) { return delete this.items[name]; } else { return this.items = {}; } }; return Store; })(); Store = Caman.Store; Blender.register("normal", function(rgbaLayer, rgbaParent) { return { r: rgbaLayer.r, g: rgbaLayer.g, b: rgbaLayer.b }; }); Blender.register("multiply", function(rgbaLayer, rgbaParent) { return { r: (rgbaLayer.r * rgbaParent.r) / 255, g: (rgbaLayer.g * rgbaParent.g) / 255, b: (rgbaLayer.b * rgbaParent.b) / 255 }; }); Blender.register("screen", function(rgbaLayer, rgbaParent) { return { r: 255 - (((255 - rgbaLayer.r) * (255 - rgbaParent.r)) / 255), g: 255 - (((255 - rgbaLayer.g) * (255 - rgbaParent.g)) / 255), b: 255 - (((255 - rgbaLayer.b) * (255 - rgbaParent.b)) / 255) }; }); Blender.register("overlay", function(rgbaLayer, rgbaParent) { var result; result = {}; result.r = rgbaParent.r > 128 ? 255 - 2 * (255 - rgbaLayer.r) * (255 - rgbaParent.r) / 255 : (rgbaParent.r * rgbaLayer.r * 2) / 255; result.g = rgbaParent.g > 128 ? 255 - 2 * (255 - rgbaLayer.g) * (255 - rgbaParent.g) / 255 : (rgbaParent.g * rgbaLayer.g * 2) / 255; result.b = rgbaParent.b > 128 ? 255 - 2 * (255 - rgbaLayer.b) * (255 - rgbaParent.b) / 255 : (rgbaParent.b * rgbaLayer.b * 2) / 255; return result; }); Blender.register("difference", function(rgbaLayer, rgbaParent) { return { r: rgbaLayer.r - rgbaParent.r, g: rgbaLayer.g - rgbaParent.g, b: rgbaLayer.b - rgbaParent.b }; }); Blender.register("addition", function(rgbaLayer, rgbaParent) { return { r: rgbaParent.r + rgbaLayer.r, g: rgbaParent.g + rgbaLayer.g, b: rgbaParent.b + rgbaLayer.b }; }); Blender.register("exclusion", function(rgbaLayer, rgbaParent) { return { r: 128 - 2 * (rgbaParent.r - 128) * (rgbaLayer.r - 128) / 255, g: 128 - 2 * (rgbaParent.g - 128) * (rgbaLayer.g - 128) / 255, b: 128 - 2 * (rgbaParent.b - 128) * (rgbaLayer.b - 128) / 255 }; }); Blender.register("softLight", function(rgbaLayer, rgbaParent) { var result; result = {}; result.r = rgbaParent.r > 128 ? 255 - ((255 - rgbaParent.r) * (255 - (rgbaLayer.r - 128))) / 255 : (rgbaParent.r * (rgbaLayer.r + 128)) / 255; result.g = rgbaParent.g > 128 ? 255 - ((255 - rgbaParent.g) * (255 - (rgbaLayer.g - 128))) / 255 : (rgbaParent.g * (rgbaLayer.g + 128)) / 255; result.b = rgbaParent.b > 128 ? 255 - ((255 - rgbaParent.b) * (255 - (rgbaLayer.b - 128))) / 255 : (rgbaParent.b * (rgbaLayer.b + 128)) / 255; return result; }); Blender.register("lighten", function(rgbaLayer, rgbaParent) { return { r: rgbaParent.r > rgbaLayer.r ? rgbaParent.r : rgbaLayer.r, g: rgbaParent.g > rgbaLayer.g ? rgbaParent.g : rgbaLayer.g, b: rgbaParent.b > rgbaLayer.b ? rgbaParent.b : rgbaLayer.b }; }); Blender.register("darken", function(rgbaLayer, rgbaParent) { return { r: rgbaParent.r > rgbaLayer.r ? rgbaLayer.r : rgbaParent.r, g: rgbaParent.g > rgbaLayer.g ? rgbaLayer.g : rgbaParent.g, b: rgbaParent.b > rgbaLayer.b ? rgbaLayer.b : rgbaParent.b }; }); Filter.register("fillColor", function() { var color; if (arguments.length === 1) { color = Convert.hexToRGB(arguments[0]); } else { color = { r: arguments[0], g: arguments[1], b: arguments[2] }; } return this.process("fillColor", function(rgba) { rgba.r = color.r; rgba.g = color.g; rgba.b = color.b; rgba.a = 255; return rgba; }); }); Filter.register("brightness", function(adjust) { adjust = Math.floor(255 * (adjust / 100)); return this.process("brightness", function(rgba) { rgba.r += adjust; rgba.g += adjust; rgba.b += adjust; return rgba; }); }); Filter.register("saturation", function(adjust) { adjust *= -0.01; return this.process("saturation", function(rgba) { var max; max = Math.max(rgba.r, rgba.g, rgba.b); if (rgba.r !== max) { rgba.r += (max - rgba.r) * adjust; } if (rgba.g !== max) { rgba.g += (max - rgba.g) * adjust; } if (rgba.b !== max) { rgba.b += (max - rgba.b) * adjust; } return rgba; }); }); Filter.register("vibrance", function(adjust) { adjust *= -1; return this.process("vibrance", function(rgba) { var amt, avg, max; max = Math.max(rgba.r, rgba.g, rgba.b); avg = (rgba.r + rgba.g + rgba.b) / 3; amt = ((Math.abs(max - avg) * 2 / 255) * adjust) / 100; if (rgba.r !== max) { rgba.r += (max - rgba.r) * amt; } if (rgba.g !== max) { rgba.g += (max - rgba.g) * amt; } if (rgba.b !== max) { rgba.b += (max - rgba.b) * amt; } return rgba; }); }); Filter.register("greyscale", function(adjust) { return this.process("greyscale", function(rgba) { var avg; avg = Calculate.luminance(rgba); rgba.r = avg; rgba.g = avg; rgba.b = avg; return rgba; }); }); Filter.register("contrast", function(adjust) { adjust = Math.pow((adjust + 100) / 100, 2); return this.process("contrast", function(rgba) { rgba.r /= 255; rgba.r -= 0.5; rgba.r *= adjust; rgba.r += 0.5; rgba.r *= 255; rgba.g /= 255; rgba.g -= 0.5; rgba.g *= adjust; rgba.g += 0.5; rgba.g *= 255; rgba.b /= 255; rgba.b -= 0.5; rgba.b *= adjust; rgba.b += 0.5; rgba.b *= 255; return rgba; }); }); Filter.register("hue", function(adjust) { return this.process("hue", function(rgba) { var b, g, h, hsv, r, _ref; hsv = Convert.rgbToHSV(rgba.r, rgba.g, rgba.b); h = hsv.h * 100; h += Math.abs(adjust); h = h % 100; h /= 100; hsv.h = h; _ref = Convert.hsvToRGB(hsv.h, hsv.s, hsv.v), r = _ref.r, g = _ref.g, b = _ref.b; rgba.r = r; rgba.g = g; rgba.b = b; return rgba; }); }); Filter.register("colorize", function() { var level, rgb; if (arguments.length === 2) { rgb = Convert.hexToRGB(arguments[0]); level = arguments[1]; } else if (arguments.length === 4) { rgb = { r: arguments[0], g: arguments[1], b: arguments[2] }; level = arguments[3]; } return this.process("colorize", function(rgba) { rgba.r -= (rgba.r - rgb.r) * (level / 100); rgba.g -= (rgba.g - rgb.g) * (level / 100); rgba.b -= (rgba.b - rgb.b) * (level / 100); return rgba; }); }); Filter.register("invert", function() { return this.process("invert", function(rgba) { rgba.r = 255 - rgba.r; rgba.g = 255 - rgba.g; rgba.b = 255 - rgba.b; return rgba; }); }); Filter.register("sepia", function(adjust) { if (adjust == null) { adjust = 100; } adjust /= 100; return this.process("sepia", function(rgba) { rgba.r = Math.min(255, (rgba.r * (1 - (0.607 * adjust))) + (rgba.g * (0.769 * adjust)) + (rgba.b * (0.189 * adjust))); rgba.g = Math.min(255, (rgba.r * (0.349 * adjust)) + (rgba.g * (1 - (0.314 * adjust))) + (rgba.b * (0.168 * adjust))); rgba.b = Math.min(255, (rgba.r * (0.272 * adjust)) + (rgba.g * (0.534 * adjust)) + (rgba.b * (1 - (0.869 * adjust)))); return rgba; }); }); Filter.register("gamma", function(adjust) { return this.process("gamma", function(rgba) { rgba.r = Math.pow(rgba.r / 255, adjust) * 255; rgba.g = Math.pow(rgba.g / 255, adjust) * 255; rgba.b = Math.pow(rgba.b / 255, adjust) * 255; return rgba; }); }); Filter.register("noise", function(adjust) { adjust = Math.abs(adjust) * 2.55; return this.process("noise", function(rgba) { var rand; rand = Calculate.randomRange(adjust * -1, adjust); rgba.r += rand; rgba.g += rand; rgba.b += rand; return rgba; }); }); Filter.register("clip", function(adjust) { adjust = Math.abs(adjust) * 2.55; return this.process("clip", function(rgba) { if (rgba.r > 255 - adjust) { rgba.r = 255; } else if (rgba.r < adjust) { rgba.r = 0; } if (rgba.g > 255 - adjust) { rgba.g = 255; } else if (rgba.g < adjust) { rgba.g = 0; } if (rgba.b > 255 - adjust) { rgba.b = 255; } else if (rgba.b < adjust) { rgba.b = 0; } return rgba; }); }); Filter.register("channels", function(options) { var chan, value; if (typeof options !== "object") { return this; } for (chan in options) { if (!__hasProp.call(options, chan)) continue; value = options[chan]; if (value === 0) { delete options[chan]; continue; } options[chan] /= 100; } if (options.length === 0) { return this; } return this.process("channels", function(rgba) { if (options.red != null) { if (options.red > 0) { rgba.r += (255 - rgba.r) * options.red; } else { rgba.r -= rgba.r * Math.abs(options.red); } } if (options.green != null) { if (options.green > 0) { rgba.g += (255 - rgba.g) * options.green; } else { rgba.g -= rgba.g * Math.abs(options.green); } } if (options.blue != null) { if (options.blue > 0) { rgba.b += (255 - rgba.b) * options.blue; } else { rgba.b -= rgba.b * Math.abs(options.blue); } } return rgba; }); }); Filter.register("curves", function() { var algo, bezier, chans, cps, end, i, last, start, _i, _j, _ref, _ref1; chans = arguments[0], cps = 2 <= arguments.length ? __slice.call(arguments, 1) : []; last = cps[cps.length - 1]; if (typeof last === "function") { algo = last; cps.pop(); } else if (typeof last === "string") { algo = Calculate[last]; cps.pop(); } else { algo = Calculate.bezier; } if (typeof chans === "string") { chans = chans.split(""); } if (chans[0] === "v") { chans = ['r', 'g', 'b']; } if (cps.length < 2) { throw "Invalid number of arguments to curves filter"; } bezier = algo(cps, 0, 255); start = cps[0]; if (start[0] > 0) { for (i = _i = 0, _ref = start[0]; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { bezier[i] = start[1]; } } end = cps[cps.length - 1]; if (end[0] < 255) { for (i = _j = _ref1 = end[0]; _ref1 <= 255 ? _j <= 255 : _j >= 255; i = _ref1 <= 255 ? ++_j : --_j) { bezier[i] = end[1]; } } return this.process("curves", function(rgba) { var _k, _ref2; for (i = _k = 0, _ref2 = chans.length; 0 <= _ref2 ? _k < _ref2 : _k > _ref2; i = 0 <= _ref2 ? ++_k : --_k) { rgba[chans[i]] = bezier[rgba[chans[i]]]; } return rgba; }); }); Filter.register("exposure", function(adjust) { var ctrl1, ctrl2, p; p = Math.abs(adjust) / 100; ctrl1 = [0, 255 * p]; ctrl2 = [255 - (255 * p), 255]; if (adjust < 0) { ctrl1 = ctrl1.reverse(); ctrl2 = ctrl2.reverse(); } return this.curves('rgb', [0, 0], ctrl1, ctrl2, [255, 255]); }); Caman.Plugin.register("crop", function(width, height, x, y) { var canvas, ctx; if (x == null) { x = 0; } if (y == null) { y = 0; } if (typeof exports !== "undefined" && exports !== null) { canvas = new Canvas(width, height); } else { canvas = document.createElement('canvas'); Util.copyAttributes(this.canvas, canvas); canvas.width = width; canvas.height = height; } ctx = canvas.getContext('2d'); ctx.drawImage(this.canvas, x, y, width, height, 0, 0, width, height); this.cropCoordinates = { x: x, y: y }; this.cropped = true; return this.replaceCanvas(canvas); }); Caman.Plugin.register("resize", function(newDims) { var canvas, ctx; if (newDims == null) { newDims = null; } if (newDims === null || ((newDims.width == null) && (newDims.height == null))) { Log.error("Invalid or missing dimensions given for resize"); return; } if (newDims.width == null) { newDims.width = this.canvas.width * newDims.height / this.canvas.height; } else if (newDims.height == null) { newDims.height = this.canvas.height * newDims.width / this.canvas.width; } if (typeof exports !== "undefined" && exports !== null) { canvas = new Canvas(newDims.width, newDims.height); } else { canvas = document.createElement('canvas'); Util.copyAttributes(this.canvas, canvas); canvas.width = newDims.width; canvas.height = newDims.height; } ctx = canvas.getContext('2d'); ctx.drawImage(this.canvas, 0, 0, this.canvas.width, this.canvas.height, 0, 0, newDims.width, newDims.height); this.resized = true; return this.replaceCanvas(canvas); }); Caman.Filter.register("crop", function() { return this.processPlugin("crop", Array.prototype.slice.call(arguments, 0)); }); Caman.Filter.register("resize", function() { return this.processPlugin("resize", Array.prototype.slice.call(arguments, 0)); }); Caman.Filter.register("boxBlur", function() { return this.processKernel("Box Blur", [1, 1, 1, 1, 1, 1, 1, 1, 1]); }); Caman.Filter.register("heavyRadialBlur", function() { return this.processKernel("Heavy Radial Blur", [0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0]); }); Caman.Filter.register("gaussianBlur", function() { return this.processKernel("Gaussian Blur", [1, 4, 6, 4, 1, 4, 16, 24, 16, 4, 6, 24, 36, 24, 6, 4, 16, 24, 16, 4, 1, 4, 6, 4, 1]); }); Caman.Filter.register("motionBlur", function(degrees) { var kernel; if (degrees === 0 || degrees === 180) { kernel = [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0]; } else if ((degrees > 0 && degrees < 90) || (degrees > 180 && degrees < 270)) { kernel = [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]; } else if (degrees === 90 || degrees === 270) { kernel = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; } else { kernel = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]; } return this.processKernel("Motion Blur", kernel); }); Caman.Filter.register("sharpen", function(amt) { if (amt == null) { amt = 100; } amt /= 100; return this.processKernel("Sharpen", [0, -amt, 0, -amt, 4 * amt + 1, -amt, 0, -amt, 0]); }); vignetteFilters = { brightness: function(rgba, amt, opts) { rgba.r = rgba.r - (rgba.r * amt * opts.strength); rgba.g = rgba.g - (rgba.g * amt * opts.strength); rgba.b = rgba.b - (rgba.b * amt * opts.strength); return rgba; }, gamma: function(rgba, amt, opts) { rgba.r = Math.pow(rgba.r / 255, Math.max(10 * amt * opts.strength, 1)) * 255; rgba.g = Math.pow(rgba.g / 255, Math.max(10 * amt * opts.strength, 1)) * 255; rgba.b = Math.pow(rgba.b / 255, Math.max(10 * amt * opts.strength, 1)) * 255; return rgba; }, colorize: function(rgba, amt, opts) { rgba.r -= (rgba.r - opts.color.r) * amt; rgba.g -= (rgba.g - opts.color.g) * amt; rgba.b -= (rgba.b - opts.color.b) * amt; return rgba; } }; Filter.register("vignette", function(size, strength) { var bezier, center, end, start; if (strength == null) { strength = 60; } if (typeof size === "string" && size.substr(-1) === "%") { if (this.dimensions.height > this.dimensions.width) { size = this.dimensions.width * (parseInt(size.substr(0, size.length - 1), 10) / 100); } else { size = this.dimensions.height * (parseInt(size.substr(0, size.length - 1), 10) / 100); } } strength /= 100; center = [this.dimensions.width / 2, this.dimensions.height / 2]; start = Math.sqrt(Math.pow(center[0], 2) + Math.pow(center[1], 2)); end = start - size; bezier = Calculate.bezier([0, 1], [30, 30], [70, 60], [100, 80]); return this.process("vignette", function(rgba) { var dist, div, loc; loc = rgba.locationXY(); dist = Calculate.distance(loc.x, loc.y, center[0], center[1]); if (dist > end) { div = Math.max(1, (bezier[Math.round(((dist - end) / size) * 100)] / 10) * strength); rgba.r = Math.pow(rgba.r / 255, div) * 255; rgba.g = Math.pow(rgba.g / 255, div) * 255; rgba.b = Math.pow(rgba.b / 255, div) * 255; } return rgba; }); }); Filter.register("rectangularVignette", function(opts) { var defaults, dim, percent, size, _i, _len, _ref; defaults = { strength: 50, cornerRadius: 0, method: 'brightness', color: { r: 0, g: 0, b: 0 } }; opts = Util.extend(defaults, opts); if (!opts.size) { return this; } else if (typeof opts.size === "string") { percent = parseInt(opts.size, 10) / 100; opts.size = { width: this.dimensions.width * percent, height: this.dimensions.height * percent }; } else if (typeof opts.size === "object") { _ref = ["width", "height"]; for (_i = 0, _len = _ref.length; _i < _len; _i++) { dim = _ref[_i]; if (typeof opts.size[dim] === "string") { opts.size[dim] = this.dimensions[dim] * (parseInt(opts.size[dim], 10) / 100); } } } else if (opts.size === "number") { size = opts.size; opts.size = { width: size, height: size }; } if (typeof opts.cornerRadius === "string") { opts.cornerRadius = (opts.size.width / 2) * (parseInt(opts.cornerRadius, 10) / 100); } opts.strength /= 100; opts.size.width = Math.floor(opts.size.width); opts.size.height = Math.floor(opts.size.height); opts.image = { width: this.dimensions.width, height: this.dimensions.height }; if (opts.method === "colorize" && typeof opts.color === "string") { opts.color = Convert.hexToRGB(opts.color); } opts.coords = { left: (this.dimensions.width - opts.size.width) / 2, right: this.dimensions.width - opts.coords.left, bottom: (this.dimensions.height - opts.size.height) / 2, top: this.dimensions.height - opts.coords.bottom }; opts.corners = [ { x: opts.coords.left + opts.cornerRadius, y: opts.coords.top - opts.cornerRadius }, { x: opts.coords.right - opts.cornerRadius, y: opts.coords.top - opts.cornerRadius }, { x: opts.coords.right - opts.cornerRadius, y: opts.coords.bottom + opts.cornerRadius }, { x: opts.coords.left + opts.cornerRadius, y: opts.coords.bottom + opts.cornerRadius } ]; opts.maxDist = Calculate.distance(0, 0, opts.corners[3].x, opts.corners[3].y) - opts.cornerRadius; return this.process("rectangularVignette", function(rgba) { var amt, loc, radialDist; loc = rgba.locationXY(); if ((loc.x > opts.corners[0].x && loc.x < opts.corners[1].x) && (loc.y > opts.coords.bottom && loc.y < opts.coords.top)) { return rgba; } if ((loc.x > opts.coords.left && loc.x < opts.coords.right) && (loc.y > opts.corners[3].y && loc.y < opts.corners[2].y)) { return rgba; } if (loc.x > opts.corners[0].x && loc.x < opts.corners[1].x && loc.y > opts.coords.top) { amt = (loc.y - opts.coords.top) / opts.maxDist; } else if (loc.y > opts.corners[2].y && loc.y < opts.corners[1].y && loc.x > opts.coords.right) { amt = (loc.x - opts.coords.right) / opts.maxDist; } else if (loc.x > opts.corners[0].x && loc.x < opts.corners[1].x && loc.y < opts.coords.bottom) { amt = (opts.coords.bottom - loc.y) / opts.maxDist; } else if (loc.y > opts.corners[2].y && loc.y < opts.corners[1].y && loc.x < opts.coords.left) { amt = (opts.coords.left - loc.x) / opts.maxDist; } else if (loc.x <= opts.corners[0].x && loc.y >= opts.corners[0].y) { radialDist = Caman.distance(loc.x, loc.y, opts.corners[0].x, opts.corners[0].y); amt = (radialDist - opts.cornerRadius) / opts.maxDist; } else if (loc.x >= opts.corners[1].x && loc.y >= opts.corners[1].y) { radialDist = Caman.distance(loc.x, loc.y, opts.corners[1].x, opts.corners[1].y); amt = (radialDist - opts.cornerRadius) / opts.maxDist; } else if (loc.x >= opts.corners[2].x && loc.y <= opts.corners[2].y) { radialDist = Caman.distance(loc.x, loc.y, opts.corners[2].x, opts.corners[2].y); amt = (radialDist - opts.cornerRadius) / opts.maxDist; } else if (loc.x <= opts.corners[3].x && loc.y <= opts.corners[3].y) { radialDist = Caman.distance(loc.x, loc.y, opts.corners[3].x, opts.corners[3].y); amt = (radialDist - opts.cornerRadius) / opts.maxDist; } if (amt < 0) { return rgba; } return vignetteFilters[opts.method](rgba, amt, opts); }); }); /* CompoundBlur - Blurring with varying radii for Canvas Version: 0.1 Author: Mario Klingemann Contact: mario@quasimondo.com Website: http://www.quasimondo.com/StackBlurForCanvas Twitter: @quasimondo Modified By: Ryan LeFevre (@meltingice) In case you find this class useful - especially in commercial projects - I am not totally unhappy for a small donation to my PayPal account mario@quasimondo.de Copyright (c) 2011 Mario Klingemann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function() { var BlurStack, getLinearGradientMap, getRadialGradientMap, mul_table, shg_table; mul_table = [512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259]; shg_table = [9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24]; getLinearGradientMap = function(width, height, centerX, centerY, angle, length, mirrored) { var cnv, context, gradient, x1, x2, y1, y2; cnv = typeof exports !== "undefined" && exports !== null ? new Canvas() : document.createElement('canvas'); cnv.width = width; cnv.height = height; x1 = centerX + Math.cos(angle) * length * 0.5; y1 = centerY + Math.sin(angle) * length * 0.5; x2 = centerX - Math.cos(angle) * length * 0.5; y2 = centerY - Math.sin(angle) * length * 0.5; context = cnv.getContext("2d"); gradient = context.createLinearGradient(x1, y1, x2, y2); if (!mirrored) { gradient.addColorStop(0, "white"); gradient.addColorStop(1, "black"); } else { gradient.addColorStop(0, "white"); gradient.addColorStop(0.5, "black"); gradient.addColorStop(1, "white"); } context.fillStyle = gradient; context.fillRect(0, 0, width, height); return context.getImageData(0, 0, width, height); }; getRadialGradientMap = function(width, height, centerX, centerY, radius1, radius2) { var cnv, context, gradient; cnv = typeof exports !== "undefined" && exports !== null ? new Canvas() : document.createElement('canvas'); cnv.width = width; cnv.height = height; context = cnv.getContext("2d"); gradient = context.createRadialGradient(centerX, centerY, radius1, centerX, centerY, radius2); gradient.addColorStop(1, "white"); gradient.addColorStop(0, "black"); context.fillStyle = gradient; context.fillRect(0, 0, width, height); return context.getImageData(0, 0, width, height); }; BlurStack = function() { this.r = 0; this.g = 0; this.b = 0; this.a = 0; return this.next = null; }; Caman.Plugin.register("compoundBlur", function(radiusData, radius, increaseFactor, blurLevels) { var b_in_sum, b_out_sum, b_sum, blend, currentIndex, div, g_in_sum, g_out_sum, g_sum, height, heightMinus1, i, iblend, idx, imagePixels, index, iradius, lookupValue, mul_sum, p, pb, pg, pixels, pr, r_in_sum, r_out_sum, r_sum, radiusPixels, radiusPlus1, rbs, shg_sum, stack, stackEnd, stackIn, stackOut, stackStart, steps, sumFactor, w4, wh, wh4, width, widthMinus1, x, y, yi, yp, yw, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r; width = this.dimensions.width; height = this.dimensions.height; imagePixels = this.pixelData; radiusPixels = radiusData.data; wh = width * height; wh4 = wh << 2; pixels = []; for (i = _i = 0; 0 <= wh4 ? _i < wh4 : _i > wh4; i = 0 <= wh4 ? ++_i : --_i) { pixels[i] = imagePixels[i]; } currentIndex = 0; steps = blurLevels; blurLevels -= 1; while (steps-- >= 0) { iradius = (radius + 0.5) | 0; if (iradius === 0) { continue; } if (iradius > 256) { iradius = 256; } div = iradius + iradius + 1; w4 = width << 2; widthMinus1 = width - 1; heightMinus1 = height - 1; radiusPlus1 = iradius + 1; sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2; stackStart = new BlurStack(); stackEnd = void 0; stack = stackStart; for (i = _j = 1; 1 <= div ? _j < div : _j > div; i = 1 <= div ? ++_j : --_j) { stack = stack.next = new BlurStack(); if (i === radiusPlus1) { stackEnd = stack; } } stack.next = stackStart; stackIn = null; stackOut = null; yw = yi = 0; mul_sum = mul_table[iradius]; shg_sum = shg_table[iradius]; for (y = _k = 0; 0 <= height ? _k < height : _k > height; y = 0 <= height ? ++_k : --_k) { r_in_sum = g_in_sum = b_in_sum = r_sum = g_sum = b_sum = 0; r_out_sum = radiusPlus1 * (pr = pixels[yi]); g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]); b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]); r_sum += sumFactor * pr; g_sum += sumFactor * pg; b_sum += sumFactor * pb; stack = stackStart; for (i = _l = 0; 0 <= radiusPlus1 ? _l < radiusPlus1 : _l > radiusPlus1; i = 0 <= radiusPlus1 ? ++_l : --_l) { stack.r = pr; stack.g = pg; stack.b = pb; stack = stack.next; } for (i = _m = 1; 1 <= radiusPlus1 ? _m < radiusPlus1 : _m > radiusPlus1; i = 1 <= radiusPlus1 ? ++_m : --_m) { p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); r_sum += (stack.r = (pr = pixels[p])) * (rbs = radiusPlus1 - i); g_sum += (stack.g = (pg = pixels[p + 1])) * rbs; b_sum += (stack.b = (pb = pixels[p + 2])) * rbs; r_in_sum += pr; g_in_sum += pg; b_in_sum += pb; stack = stack.next; } stackIn = stackStart; stackOut = stackEnd; for (x = _n = 0; 0 <= width ? _n < width : _n > width; x = 0 <= width ? ++_n : --_n) { pixels[yi] = (r_sum * mul_sum) >> shg_sum; pixels[yi + 1] = (g_sum * mul_sum) >> shg_sum; pixels[yi + 2] = (b_sum * mul_sum) >> shg_sum; r_sum -= r_out_sum; g_sum -= g_out_sum; b_sum -= b_out_sum; r_out_sum -= stackIn.r; g_out_sum -= stackIn.g; b_out_sum -= stackIn.b; p = (yw + ((p = x + radiusPlus1) < widthMinus1 ? p : widthMinus1)) << 2; r_in_sum += (stackIn.r = pixels[p]); g_in_sum += (stackIn.g = pixels[p + 1]); b_in_sum += (stackIn.b = pixels[p + 2]); r_sum += r_in_sum; g_sum += g_in_sum; b_sum += b_in_sum; stackIn = stackIn.next; r_out_sum += (pr = stackOut.r); g_out_sum += (pg = stackOut.g); b_out_sum += (pb = stackOut.b); r_in_sum -= pr; g_in_sum -= pg; b_in_sum -= pb; stackOut = stackOut.next; yi += 4; } yw += width; } for (x = _o = 0; 0 <= width ? _o < width : _o > width; x = 0 <= width ? ++_o : --_o) { g_in_sum = b_in_sum = r_in_sum = g_sum = b_sum = r_sum = 0; yi = x << 2; r_out_sum = radiusPlus1 * (pr = pixels[yi]); g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]); b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]); r_sum += sumFactor * pr; g_sum += sumFactor * pg; b_sum += sumFactor * pb; stack = stackStart; for (i = _p = 0; 0 <= radiusPlus1 ? _p < radiusPlus1 : _p > radiusPlus1; i = 0 <= radiusPlus1 ? ++_p : --_p) { stack.r = pr; stack.g = pg; stack.b = pb; stack = stack.next; } yp = width; for (i = _q = 1; 1 <= radiusPlus1 ? _q < radiusPlus1 : _q > radiusPlus1; i = 1 <= radiusPlus1 ? ++_q : --_q) { yi = (yp + x) << 2; r_sum += (stack.r = (pr = pixels[yi])) * (rbs = radiusPlus1 - i); g_sum += (stack.g = (pg = pixels[yi + 1])) * rbs; b_sum += (stack.b = (pb = pixels[yi + 2])) * rbs; r_in_sum += pr; g_in_sum += pg; b_in_sum += pb; stack = stack.next; if (i < heightMinus1) { yp += width; } } yi = x; stackIn = stackStart; stackOut = stackEnd; for (y = _r = 0; 0 <= height ? _r < height : _r > height; y = 0 <= height ? ++_r : --_r) { p = yi << 2; pixels[p] = (r_sum * mul_sum) >> shg_sum; pixels[p + 1] = (g_sum * mul_sum) >> shg_sum; pixels[p + 2] = (b_sum * mul_sum) >> shg_sum; r_sum -= r_out_sum; g_sum -= g_out_sum; b_sum -= b_out_sum; r_out_sum -= stackIn.r; g_out_sum -= stackIn.g; b_out_sum -= stackIn.b; p = (x + (((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width)) << 2; r_sum += (r_in_sum += (stackIn.r = pixels[p])); g_sum += (g_in_sum += (stackIn.g = pixels[p + 1])); b_sum += (b_in_sum += (stackIn.b = pixels[p + 2])); stackIn = stackIn.next; r_out_sum += (pr = stackOut.r); g_out_sum += (pg = stackOut.g); b_out_sum += (pb = stackOut.b); r_in_sum -= pr; g_in_sum -= pg; b_in_sum -= pb; stackOut = stackOut.next; yi += width; } } radius *= increaseFactor; i = wh; while (--i > -1) { idx = i << 2; lookupValue = (radiusPixels[idx + 2] & 0xff) / 255.0 * blurLevels; index = lookupValue | 0; if (index === currentIndex) { blend = 256.0 * (lookupValue - (lookupValue | 0)); iblend = 256 - blend; imagePixels[idx] = (imagePixels[idx] * iblend + pixels[idx] * blend) >> 8; imagePixels[idx + 1] = (imagePixels[idx + 1] * iblend + pixels[idx + 1] * blend) >> 8; imagePixels[idx + 2] = (imagePixels[idx + 2] * iblend + pixels[idx + 2] * blend) >> 8; } else if (index === currentIndex + 1) { imagePixels[idx] = pixels[idx]; imagePixels[idx + 1] = pixels[idx + 1]; imagePixels[idx + 2] = pixels[idx + 2]; } } currentIndex++; } return this; }); Caman.Filter.register("tiltShift", function(opts) { var defaults, gradient; defaults = { center: { x: this.dimensions.width / 2, y: this.dimensions.height / 2 }, angle: 45, focusWidth: 200, startRadius: 3, radiusFactor: 1.5, steps: 3 }; opts = Util.extend(defaults, opts); opts.angle *= Math.PI / 180; gradient = getLinearGradientMap(this.dimensions.width, this.dimensions.height, opts.center.x, opts.center.y, opts.angle, opts.focusWidth, true); return this.processPlugin("compoundBlur", [gradient, opts.startRadius, opts.radiusFactor, opts.steps]); }); return Caman.Filter.register("radialBlur", function(opts) { var defaults, gradient, radius1, radius2; defaults = { size: 50, center: { x: this.dimensions.width / 2, y: this.dimensions.height / 2 }, startRadius: 3, radiusFactor: 1.5, steps: 3, radius: null }; opts = Util.extend(defaults, opts); if (!opts.radius) { opts.radius = this.dimensions.width < this.dimensions.height ? this.dimensions.height : this.dimensions.width; } radius1 = (opts.radius / 2) - opts.size; radius2 = opts.radius / 2; gradient = getRadialGradientMap(this.dimensions.width, this.dimensions.height, opts.center.x, opts.center.y, radius1, radius2); return this.processPlugin("compoundBlur", [gradient, opts.startRadius, opts.radiusFactor, opts.steps]); }); })(); Caman.Filter.register("edgeEnhance", function() { return this.processKernel("Edge Enhance", [0, 0, 0, -1, 1, 0, 0, 0, 0]); }); Caman.Filter.register("edgeDetect", function() { return this.processKernel("Edge Detect", [-1, -1, -1, -1, 8, -1, -1, -1, -1]); }); Caman.Filter.register("emboss", function() { return this.processKernel("Emboss", [-2, -1, 0, -1, 1, 1, 0, 1, 2]); }); Caman.Filter.register("posterize", function(adjust) { var numOfAreas, numOfValues; numOfAreas = 256 / adjust; numOfValues = 255 / (adjust - 1); return this.process("posterize", function(rgba) { rgba.r = Math.floor(Math.floor(rgba.r / numOfAreas) * numOfValues); rgba.g = Math.floor(Math.floor(rgba.g / numOfAreas) * numOfValues); rgba.b = Math.floor(Math.floor(rgba.b / numOfAreas) * numOfValues); return rgba; }); }); Caman.Filter.register("vintage", function(vignette) { if (vignette == null) { vignette = true; } this.greyscale(); this.contrast(5); this.noise(3); this.sepia(100); this.channels({ red: 8, blue: 2, green: 4 }); this.gamma(0.87); if (vignette) { return this.vignette("40%", 30); } }); Caman.Filter.register("lomo", function(vignette) { if (vignette == null) { vignette = true; } this.brightness(15); this.exposure(15); this.curves('rgb', [0, 0], [200, 0], [155, 255], [255, 255]); this.saturation(-20); this.gamma(1.8); if (vignette) { this.vignette("50%", 60); } return this.brightness(5); }); Caman.Filter.register("clarity", function(grey) { if (grey == null) { grey = false; } this.vibrance(20); this.curves('rgb', [5, 0], [130, 150], [190, 220], [250, 255]); this.sharpen(15); this.vignette("45%", 20); if (grey) { this.greyscale(); this.contrast(4); } return this; }); Caman.Filter.register("sinCity", function() { this.contrast(100); this.brightness(15); this.exposure(10); this.posterize(80); this.clip(30); return this.greyscale(); }); Caman.Filter.register("sunrise", function() { this.exposure(3.5); this.saturation(-5); this.vibrance(50); this.sepia(60); this.colorize("#e87b22", 10); this.channels({ red: 8, blue: 8 }); this.contrast(5); this.gamma(1.2); return this.vignette("55%", 25); }); Caman.Filter.register("crossProcess", function() { this.exposure(5); this.colorize("#e87b22", 4); this.sepia(20); this.channels({ blue: 8, red: 3 }); this.curves('b', [0, 0], [100, 150], [180, 180], [255, 255]); this.contrast(15); this.vibrance(75); return this.gamma(1.6); }); Caman.Filter.register("orangePeel", function() { this.curves('rgb', [0, 0], [100, 50], [140, 200], [255, 255]); this.vibrance(-30); this.saturation(-30); this.colorize('#ff9000', 30); this.contrast(-5); return this.gamma(1.4); }); Caman.Filter.register("love", function() { this.brightness(5); this.exposure(8); this.contrast(4); this.colorize('#c42007', 30); this.vibrance(50); return this.gamma(1.3); }); Caman.Filter.register("grungy", function() { this.gamma(1.5); this.clip(25); this.saturation(-60); this.contrast(5); this.noise(5); return this.vignette("50%", 30); }); Caman.Filter.register("jarques", function() { this.saturation(-35); this.curves('b', [20, 0], [90, 120], [186, 144], [255, 230]); this.curves('r', [0, 0], [144, 90], [138, 120], [255, 255]); this.curves('g', [10, 0], [115, 105], [148, 100], [255, 248]); this.curves('rgb', [0, 0], [120, 100], [128, 140], [255, 255]); return this.sharpen(20); }); Caman.Filter.register("pinhole", function() { this.greyscale(); this.sepia(10); this.exposure(10); this.contrast(15); return this.vignette("60%", 35); }); Caman.Filter.register("oldBoot", function() { this.saturation(-20); this.vibrance(-50); this.gamma(1.1); this.sepia(30); this.channels({ red: -10, blue: 5 }); this.curves('rgb', [0, 0], [80, 50], [128, 230], [255, 255]); return this.vignette("60%", 30); }); Caman.Filter.register("glowingSun", function(vignette) { if (vignette == null) { vignette = true; } this.brightness(10); this.newLayer(function() { this.setBlendingMode("multiply"); this.opacity(80); this.copyParent(); this.filter.gamma(0.8); this.filter.contrast(50); return this.filter.exposure(10); }); this.newLayer(function() { this.setBlendingMode("softLight"); this.opacity(80); return this.fillColor("#f49600"); }); this.exposure(20); this.gamma(0.8); if (vignette) { return this.vignette("45%", 20); } }); Caman.Filter.register("hazyDays", function() { this.gamma(1.2); this.newLayer(function() { this.setBlendingMode("overlay"); this.opacity(60); this.copyParent(); this.filter.channels({ red: 5 }); return this.filter.stackBlur(15); }); this.newLayer(function() { this.setBlendingMode("addition"); this.opacity(40); return this.fillColor("#6899ba"); }); this.newLayer(function() { this.setBlendingMode("multiply"); this.opacity(35); this.copyParent(); this.filter.brightness(40); this.filter.vibrance(40); this.filter.exposure(30); this.filter.contrast(15); this.filter.curves('r', [0, 40], [128, 128], [128, 128], [255, 215]); this.filter.curves('g', [0, 40], [128, 128], [128, 128], [255, 215]); this.filter.curves('b', [0, 40], [128, 128], [128, 128], [255, 215]); return this.filter.stackBlur(5); }); this.curves('r', [20, 0], [128, 158], [128, 128], [235, 255]); this.curves('g', [20, 0], [128, 128], [128, 128], [235, 255]); this.curves('b', [20, 0], [128, 108], [128, 128], [235, 255]); return this.vignette("45%", 20); }); Caman.Filter.register("herMajesty", function() { this.brightness(40); this.colorize("#ea1c5d", 10); this.curves('b', [0, 10], [128, 180], [190, 190], [255, 255]); this.newLayer(function() { this.setBlendingMode('overlay'); this.opacity(50); this.copyParent(); this.filter.gamma(0.7); return this.newLayer(function() { this.setBlendingMode('normal'); this.opacity(60); return this.fillColor('#ea1c5d'); }); }); this.newLayer(function() { this.setBlendingMode('multiply'); this.opacity(60); this.copyParent(); this.filter.saturation(50); this.filter.hue(90); return this.filter.contrast(10); }); this.gamma(1.4); this.vibrance(-30); this.newLayer(function() { this.opacity(10); return this.fillColor('#e5f0ff'); }); return this; }); Caman.Filter.register("nostalgia", function() { this.saturation(20); this.gamma(1.4); this.greyscale(); this.contrast(5); this.sepia(100); this.channels({ red: 8, blue: 2, green: 4 }); this.gamma(0.8); this.contrast(5); this.exposure(10); this.newLayer(function() { this.setBlendingMode('overlay'); this.copyParent(); this.opacity(55); return this.filter.stackBlur(10); }); return this.vignette("50%", 30); }); Caman.Filter.register("hemingway", function() { this.greyscale(); this.contrast(10); this.gamma(0.9); this.newLayer(function() { this.setBlendingMode("multiply"); this.opacity(40); this.copyParent(); this.filter.exposure(15); this.filter.contrast(15); return this.filter.channels({ green: 10, red: 5 }); }); this.sepia(30); this.curves('rgb', [0, 10], [120, 90], [180, 200], [235, 255]); this.channels({ red: 5, green: -2 }); return this.exposure(15); }); Caman.Filter.register("concentrate", function() { this.sharpen(40); this.saturation(-50); this.channels({ red: 3 }); this.newLayer(function() { this.setBlendingMode("multiply"); this.opacity(80); this.copyParent(); this.filter.sharpen(5); this.filter.contrast(50); this.filter.exposure(10); return this.filter.channels({ blue: 5 }); }); return this.brightness(10); }); Caman.Plugin.register("rotate", function(degrees) { var angle, canvas, ctx, height, to_radians, width, x, y; angle = degrees % 360; if (angle === 0) { return this.dimensions = { width: this.canvas.width, height: this.canvas.height }; } to_radians = Math.PI / 180; if (typeof exports !== "undefined" && exports !== null) { canvas = new Canvas(); } else { canvas = document.createElement('canvas'); Util.copyAttributes(this.canvas, canvas); } if (angle === 90 || angle === -270 || angle === 270 || angle === -90) { width = this.canvas.height; height = this.canvas.width; x = width / 2; y = height / 2; } else if (angle === 180) { width = this.canvas.width; height = this.canvas.height; x = width / 2; y = height / 2; } else { width = Math.sqrt(Math.pow(this.originalWidth, 2) + Math.pow(this.originalHeight, 2)); height = width; x = this.canvas.height / 2; y = this.canvas.width / 2; } canvas.width = width; canvas.height = height; ctx = canvas.getContext('2d'); ctx.save(); ctx.translate(x, y); ctx.rotate(angle * to_radians); ctx.drawImage(this.canvas, -this.canvas.width / 2, -this.canvas.height / 2, this.canvas.width, this.canvas.height); ctx.restore(); return this.replaceCanvas(canvas); }); Caman.Filter.register("rotate", function() { return this.processPlugin("rotate", Array.prototype.slice.call(arguments, 0)); }); /* StackBlur - a fast almost Gaussian Blur For Canvas v0.31 modified for CamanJS Version: 0.31 Author: Mario Klingemann Contact: mario@quasimondo.com Website: http://www.quasimondo.com/StackBlurForCanvas Twitter: @quasimondo Modified By: Ryan LeFevre (@meltingice) In case you find this class useful - especially in commercial projects - I am not totally unhappy for a small donation to my PayPal account mario@quasimondo.de Or support me on flattr: https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript Copyright (c) 2010 Mario Klingemann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function() { var BlurStack, mul_table, shg_table; mul_table = [512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259]; shg_table = [9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24]; BlurStack = function() { this.r = 0; this.g = 0; this.b = 0; this.a = 0; return this.next = null; }; Caman.Plugin.register("stackBlur", function(radius) { var b_in_sum, b_out_sum, b_sum, div, g_in_sum, g_out_sum, g_sum, height, heightMinus1, i, mul_sum, p, pb, pg, pixels, pr, r_in_sum, r_out_sum, r_sum, radiusPlus1, rbs, shg_sum, stack, stackEnd, stackIn, stackOut, stackStart, sumFactor, w4, width, widthMinus1, x, y, yi, yp, yw, _i, _j, _k, _l, _m, _n, _o, _p, _q; if (isNaN(radius) || radius < 1) { return; } radius |= 0; pixels = this.pixelData; width = this.dimensions.width; height = this.dimensions.height; div = radius + radius + 1; w4 = width << 2; widthMinus1 = width - 1; heightMinus1 = height - 1; radiusPlus1 = radius + 1; sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2; stackStart = new BlurStack(); stack = stackStart; for (i = _i = 1; 1 <= div ? _i < div : _i > div; i = 1 <= div ? ++_i : --_i) { stack = stack.next = new BlurStack(); if (i === radiusPlus1) { stackEnd = stack; } } stack.next = stackStart; stackIn = null; stackOut = null; yw = yi = 0; mul_sum = mul_table[radius]; shg_sum = shg_table[radius]; for (y = _j = 0; 0 <= height ? _j < height : _j > height; y = 0 <= height ? ++_j : --_j) { r_in_sum = g_in_sum = b_in_sum = r_sum = g_sum = b_sum = 0; r_out_sum = radiusPlus1 * (pr = pixels[yi]); g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]); b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]); r_sum += sumFactor * pr; g_sum += sumFactor * pg; b_sum += sumFactor * pb; stack = stackStart; for (i = _k = 0; 0 <= radiusPlus1 ? _k < radiusPlus1 : _k > radiusPlus1; i = 0 <= radiusPlus1 ? ++_k : --_k) { stack.r = pr; stack.g = pg; stack.b = pb; stack = stack.next; } for (i = _l = 1; 1 <= radiusPlus1 ? _l < radiusPlus1 : _l > radiusPlus1; i = 1 <= radiusPlus1 ? ++_l : --_l) { p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); r_sum += (stack.r = (pr = pixels[p])) * (rbs = radiusPlus1 - i); g_sum += (stack.g = (pg = pixels[p + 1])) * rbs; b_sum += (stack.b = (pb = pixels[p + 2])) * rbs; r_in_sum += pr; g_in_sum += pg; b_in_sum += pb; stack = stack.next; } stackIn = stackStart; stackOut = stackEnd; for (x = _m = 0; 0 <= width ? _m < width : _m > width; x = 0 <= width ? ++_m : --_m) { pixels[yi] = (r_sum * mul_sum) >> shg_sum; pixels[yi + 1] = (g_sum * mul_sum) >> shg_sum; pixels[yi + 2] = (b_sum * mul_sum) >> shg_sum; r_sum -= r_out_sum; g_sum -= g_out_sum; b_sum -= b_out_sum; r_out_sum -= stackIn.r; g_out_sum -= stackIn.g; b_out_sum -= stackIn.b; p = (yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1)) << 2; r_in_sum += (stackIn.r = pixels[p]); g_in_sum += (stackIn.g = pixels[p + 1]); b_in_sum += (stackIn.b = pixels[p + 2]); r_sum += r_in_sum; g_sum += g_in_sum; b_sum += b_in_sum; stackIn = stackIn.next; r_out_sum += (pr = stackOut.r); g_out_sum += (pg = stackOut.g); b_out_sum += (pb = stackOut.b); r_in_sum -= pr; g_in_sum -= pg; b_in_sum -= pb; stackOut = stackOut.next; yi += 4; } yw += width; } for (x = _n = 0; 0 <= width ? _n < width : _n > width; x = 0 <= width ? ++_n : --_n) { g_in_sum = b_in_sum = r_in_sum = g_sum = b_sum = r_sum = 0; yi = x << 2; r_out_sum = radiusPlus1 * (pr = pixels[yi]); g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]); b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]); r_sum += sumFactor * pr; g_sum += sumFactor * pg; b_sum += sumFactor * pb; stack = stackStart; for (i = _o = 0; 0 <= radiusPlus1 ? _o < radiusPlus1 : _o > radiusPlus1; i = 0 <= radiusPlus1 ? ++_o : --_o) { stack.r = pr; stack.g = pg; stack.b = pb; stack = stack.next; } yp = width; for (i = _p = 1; 1 <= radius ? _p <= radius : _p >= radius; i = 1 <= radius ? ++_p : --_p) { yi = (yp + x) << 2; r_sum += (stack.r = (pr = pixels[yi])) * (rbs = radiusPlus1 - i); g_sum += (stack.g = (pg = pixels[yi + 1])) * rbs; b_sum += (stack.b = (pb = pixels[yi + 2])) * rbs; r_in_sum += pr; g_in_sum += pg; b_in_sum += pb; stack = stack.next; if (i < heightMinus1) { yp += width; } } yi = x; stackIn = stackStart; stackOut = stackEnd; for (y = _q = 0; 0 <= height ? _q < height : _q > height; y = 0 <= height ? ++_q : --_q) { p = yi << 2; pixels[p] = (r_sum * mul_sum) >> shg_sum; pixels[p + 1] = (g_sum * mul_sum) >> shg_sum; pixels[p + 2] = (b_sum * mul_sum) >> shg_sum; r_sum -= r_out_sum; g_sum -= g_out_sum; b_sum -= b_out_sum; r_out_sum -= stackIn.r; g_out_sum -= stackIn.g; b_out_sum -= stackIn.b; p = (x + (((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width)) << 2; r_sum += (r_in_sum += (stackIn.r = pixels[p])); g_sum += (g_in_sum += (stackIn.g = pixels[p + 1])); b_sum += (b_in_sum += (stackIn.b = pixels[p + 2])); stackIn = stackIn.next; r_out_sum += (pr = stackOut.r); g_out_sum += (pg = stackOut.g); b_out_sum += (pb = stackOut.b); r_in_sum -= pr; g_in_sum -= pg; b_in_sum -= pb; stackOut = stackOut.next; yi += width; } } return this; }); return Caman.Filter.register("stackBlur", function(radius) { return this.processPlugin("stackBlur", [radius]); }); })(); Caman.Filter.register("threshold", function(adjust) { return this.process("threshold", function(rgba) { var luminance; luminance = (0.2126 * rgba.r) + (0.7152 * rgba.g) + (0.0722 * rgba.b); if (luminance < adjust) { rgba.r = 0; rgba.g = 0; rgba.b = 0; } else { rgba.r = 255; rgba.g = 255; rgba.b = 255; } return rgba; }); }); }).call(this); //////////////////////////////////////////////////////////////////////////////////////////////////////// // // // GLAM // // // //////////////////////////////////////////////////////////////////////////////////////////////////////// var DEBUG = false; function print(text) { console.log(text);} print('loading canvas'); function createLightbox(parent) { print('creating lightbox'); // TODO consider making these values more accessible? ie properties var canvas_size = '640'; var filter_size = '128'; var sticker_size = '128'; ////////////////////////////// // // // Main Preview // // // ////////////////////////////// var lightbox = document.createElement("div"); lightbox.setAttribute('id', 'lightbox'); lightbox.setAttribute('style', 'position:fixed; top:0; left:0; width:100%; height:100%; text-align:center; background:rgba(0,0,0,.7); overflow-y: auto'); lightbox.style.display = 'none'; lightbox.addEventListener('click', function () { //lightbox.style.display = 'none'; // unfortunately its easier to destroy a lightbox than to // reset a canvas shared with caman lightbox.remove(); }, false); var lbclose = document.createElement("p"); lbclose.textContent = 'Click to close'; lbclose.setAttribute('style', 'text-align:right; color:#fff; margin-right:20px; font-size:12px; font-weight:500;'); var lbcontent = document.createElement("div"); var lbtoppanel = document.createElement("div"); lbtoppanel.setAttribute('style', 'display: flex; flex-flow: row nowrap; justify-content: center; margin-top: 10px; margin-bottom: 10px;'); ////////////////////////////// // // // CANVAS // // // ////////////////////////////// var canvasdiv = document.createElement("div"); // aids in positioning canvasdiv.setAttribute('style', 'position: relative;'); var lbcanvas = document.createElement("canvas"); lbcanvas.textContent = 'No canvas for you'; lbcanvas.setAttribute('id', 'canvas'); lbcanvas.setAttribute('width', canvas_size); lbcanvas.setAttribute('height', canvas_size); // stack is used to repr canvas items that (virtually) sit above the canvas image // these items are collectively called overlays // // canvasstack is an array of objects: // // { // 'id':int, // a unique id number // // 'image': obj, // HTMLImageElement // // 'width':int, // current width & height // 'height':int, // // 'x':int, // the center of the overlay // 'y':int, // the center of the overlay // // 'click_offset':{'x':int, 'y':int}, // (reserved) to keep the dragged item from snapping its center to cursor // // 'srcwidth':int, // the original width & height // 'srcheight':int // } // lbcanvas.canvasstack = []; lbcanvas.next_stack_id = 1; lbcanvas.mousebegan = false; lbcanvas.follower = null; lbcanvas.bgpixels = null; // a copy of the unfiltered image w/ no overlays lbcanvas.procpixels = null; // a copy of the filtered image w/ no overlays lbcanvas.currfilter = null; // the last filter function called. because there can only be one, also the repr current filter lbcanvas.tweakstack = []; // determines the order that tweaks are applied. atm only one tweak per type lbcanvas.file = null; lbcanvas.imagedata = null; lbcanvas.current_rotation = 0; // degrees lbcanvas.xflipped = false; lbcanvas.yflipped = false; lbcanvas.focalpoint = {'visible':false, 'x':lbcanvas.width / 2, 'y':lbcanvas.height / 2, 'width':80, 'height':80, 'held':false}; // convenience var var _ctx = lbcanvas.getContext('2d'); ////////////////////// // throbber // ////////////////////// var throbber = document.createElement("div"); throbber.setAttribute('id', 'throbber'); throbber.setAttribute('style', 'display:flex; justify-content: center; width: 100%; height: 70px; position: absolute; top:' + lbcanvas.height / 2 + 'px;'); throbber.style.display = 'none'; var dot01 = document.createElement("div"); dot01.setAttribute('style', 'width:30px; height:30px; margin: 4px; border-radius:100%; background:#fff; animation: 1.4s ease-in-out 0s infinite normal both running animkeys; animation-delay: -0.32s;'); var dot02 = document.createElement("div"); dot02.setAttribute('style', 'width:30px; height:30px; margin: 4px; border-radius:100%; background:#fff; animation: 1.4s ease-in-out 0s infinite normal both running animkeys; animation-delay: -0.16s;'); var dot03 = document.createElement("div"); dot03.setAttribute('style', 'width:30px; height:30px; margin: 4px; border-radius:100%; background:#fff; animation: 1.4s ease-in-out 0s infinite normal both running animkeys;'); throbber.appendChild(dot01); throbber.appendChild(dot02); throbber.appendChild(dot03); canvasdiv.appendChild(throbber); ////////////////////// // overlay controls // ////////////////////// var controls_width = 150; var ovbtnwidth = 32; // widgets that associated with overlays divided into two groups: buttons and slider var ovlaycontrols = document.createElement("div"); ovlaycontrols.setAttribute('id', 'overlay_controls'); ovlaycontrols.setAttribute('style', 'position: absolute; display: block;'); ovlaycontrols.style.display = 'none'; ovlaycontrols.overlay_id = 0; ovlaycontrols.addEventListener('mouseenter', function () { ovlaycontrols.style.display = 'block'; }, false); ovlaycontrols.addEventListener('mouseleave', function () { ovlaycontrols.style.display = 'none'; }, false); var ovlaybuttons = document.createElement("div"); ovlaybuttons.setAttribute('style', 'display:flex; flex-basis: ' + controls_width + 'px; justify-content: flex-end;'); ovlaycontrols.appendChild(ovlaybuttons); // scale overlay var ovlayscale = document.createElement("button"); ovlayscale.setAttribute('style', 'width: ' + ovbtnwidth +'px; height: ' + ovbtnwidth + 'px; border-radius:' + ovbtnwidth + 'px; border: 0; font-size: 18px; font-weight:500; color: #fff; background: rgba(0.8,0.8,0.8,.5); margin-right: 10px;'); ovlayscale.textContent = ' + '; ovlayscale.addEventListener('click', function (e) { e.stopPropagation(); print('show slider'); var slider = document.querySelector('#overlay_sliders :first-child'); print(slider); if (slider) { slider.style.display = 'block'; } }, false); ovlaybuttons.appendChild(ovlayscale); // remove overlay var ovlayremove = document.createElement("button"); ovlayremove.setAttribute('style', 'width: ' + ovbtnwidth +'px; height: ' + ovbtnwidth + 'px; border-radius:' + ovbtnwidth + 'px; border: 0; font-size: 18px; font-weight:500; color: #fff; background: rgba(0.8,0.8,0.8,.5);'); ovlayremove.textContent = ' x '; ovlayremove.addEventListener('click', function (e) { e.stopPropagation(); print('removing overlay item'); lightboxRemoveOverlay(ovlaycontrols.overlay_id); ovlaycontrols.overlay_id = 0; ovlaycontrols.style.display = 'none'; }, false); ovlaybuttons.appendChild(ovlayremove); var ovlaysliders = document.createElement("div"); ovlaysliders.setAttribute('id', 'overlay_sliders'); ovlaysliders.setAttribute('style', 'width: ' + controls_width + 'px; justify-content: flex-end; height:32px; background: rgba(0, 0, 0, 0.0);'); ovlaysliders.addEventListener('click', function (e) {e.stopPropagation()}, false); ovlaycontrols.appendChild(ovlaysliders); var scaleslider = document.createElement("input"); scaleslider.style.display = 'none'; scaleslider.setAttribute('type', 'range'); scaleslider.setAttribute('min', '0.2'); scaleslider.setAttribute('max', '2.0'); scaleslider.setAttribute('step', '0.1'); scaleslider.addEventListener('input', function (e) { e.stopPropagation(); print(scaleslider.value); lightboxScaleOverlay(ovlaycontrols.overlay_id, Number(scaleslider.value), lbcanvas, _ctx); }, false); scaleslider.addEventListener('mouseleave', function (e) { e.stopPropagation(); print('hide slider'); var slider = document.querySelector('#overlay_sliders :first-child'); if (slider) { slider.style.display = 'none'; } }, false); ovlaysliders.appendChild(scaleslider); canvasdiv.appendChild(ovlaycontrols); // events for manipulating elements on the canvas stack: e.g. stickers and UI indicators //////////// // click // //////////// lbcanvas.addEventListener('click', function (e) { e.stopPropagation(); }, false); ////////////////// // mousedown // ////////////////// lbcanvas.addEventListener('mousedown', function (e) { e.stopPropagation(); lbcanvas.mousebegan = true; lbcanvas.follower = null; print('(' + e.offsetX + ', ' + e.offsetY + ')'); // if the focal point is enabled, that takes precedence if (lbcanvas.focalpoint.visible) { let xtopleft = lbcanvas.focalpoint.x - lbcanvas.focalpoint.width / 2; let ytopleft = lbcanvas.focalpoint.y - lbcanvas.focalpoint.height / 2; if (e.offsetX > xtopleft && e.offsetX < xtopleft + lbcanvas.focalpoint.width && e.offsetY > ytopleft && e.offsetY < ytopleft + lbcanvas.focalpoint.height) { lbcanvas.focalpoint.held = true; } } // find the first matching item in LIFO order for (let i = lbcanvas.canvasstack.length - 1; i >= 0 ; i--) { let ovlayitem = lbcanvas.canvasstack[i]; let xtopleft = ovlayitem.x - ovlayitem.width / 2; let ytopleft = ovlayitem.y - ovlayitem.height / 2; if (e.offsetX > xtopleft && e.offsetX < xtopleft + ovlayitem.width && e.offsetY > ytopleft && e.offsetY < ytopleft + ovlayitem.height) { // print('found a match'); lbcanvas.follower = ovlayitem; break; } } // reset controls, if any let controls = document.querySelector('#overlay_controls'); if (controls) { controls.style.display = 'none'; } }, false); ////////////////// // mousemove // ////////////////// lbcanvas.addEventListener('mousemove', function (e) { e.stopPropagation(); if (lbcanvas.mousebegan) { // dragging if (lbcanvas.focalpoint.held) { // clear the canvas _ctx.putImageData(lbcanvas.procpixels, 0, 0); lbcanvas.focalpoint.x = e.offsetX; lbcanvas.focalpoint.y = e.offsetY; lightboxDrawOverlays(); } else if (lbcanvas.follower) { // clear the canvas _ctx.putImageData(lbcanvas.procpixels, 0, 0); lbcanvas.follower.x = e.offsetX; lbcanvas.follower.y = e.offsetY; lightboxDrawOverlays(); } } else { // hovering // find the first object under mouse let target = null; for (let i = lbcanvas.canvasstack.length - 1; i >= 0 ; i--) { let ovlayitem = lbcanvas.canvasstack[i]; let xtopleft = ovlayitem.x - ovlayitem.width / 2; let ytopleft = ovlayitem.y - ovlayitem.height / 2; if (e.offsetX > xtopleft && e.offsetX < xtopleft + ovlayitem.width && e.offsetY > ytopleft && e.offsetY < ytopleft + ovlayitem.height) { //print('found a match'); target = ovlayitem; break; } } // next // if we found an overlay... if (target) { // update the overlay controls let controls = document.querySelector('#overlay_controls'); if (controls) { let xoffset = (target.x - target.width / 2) + (target.width - controls_width); let yoffset = (target.y - target.height / 2); controls.style.left = '' + xoffset + 'px'; controls.style.top = '' + yoffset + 'px'; // store the stack id of the current item controls.overlay_id = target.id; // don't show if control is too far to the right of canvas or too far above if (xoffset > canvas_size - ovbtnwidth / 2 || yoffset < -ovbtnwidth / 2) controls.style.display = 'none'; else controls.style.display = 'block'; } } else { // reset controls, if any let controls = document.querySelector('#overlay_controls'); if (controls) { controls.style.display = 'none'; } } } }, false); ////////////////// // mouseup // ////////////////// lbcanvas.addEventListener('mouseup', function (e) { e.stopPropagation(); lbcanvas.mousebegan = false; lbcanvas.focalpoint.held = false; lbcanvas.follower = null; }, false); lbcanvas.addEventListener('mouseleave', function (e) { e.stopPropagation(); lbcanvas.mousebegan = false; lbcanvas.focalpoint.held = false; lbcanvas.follower = null; // reset controls, if any let controls = document.querySelector('#overlay_controls'); if (controls) { controls.style.display = 'none'; } var slider = document.querySelector('#overlay_sliders :first-child'); if (slider) slider.style.display = 'none'; }, false); canvasdiv.appendChild(lbcanvas); lbtoppanel.appendChild(canvasdiv); ////////////////////////////// // // // Right Panel // // // ////////////////////////////// // TOOT BUTTON // once images are integrated back into the compose box, this button becomes a Done button var lbcontrols = document.createElement("div"); lbcontrols.setAttribute('style', 'display: flex; flex-flow: column nowrap; position:absolute; right: 100px; top: 100px; width: 150px;'); var postbtn = document.createElement("button"); postbtn.setAttribute('style', 'display: block; width: 100%; border-radius:4px; border: 0; font-size: 18px; font-weight:500; color: #fff; background: #2b90d9; padding: 10px; margin-bottom: 10px; margin-right: 10px; height: auto;'); postbtn.textContent = 'Toot'; postbtn.addEventListener('click', function (e) { e.stopPropagation(); var url = window.location.origin; var access_token = get_access_token(); if (!access_token) { print('Failed to retrieve access token'); return false; } // disable while uploading var throbber = document.querySelector('#throbber'); if (throbber) throbber.style.display = 'flex'; postbtn.disabled = true; postbtn.style.background = '#808080'; var visibility = getVisibilityFromUI(); visibility = visibility || 'public'; print(visibility); var message = getTextFromUI(); message = message || '.'; print(message); var spoiler_text = getSpoilerTextFromUI(); var sensitive = 'false'; var postonload = function () { if (throbber) throbber.style.display = 'none'; resetUI(); lightbox.click(); }; var onload = function (url, access_token, media_id) { postmedia(url, access_token, media_id, message, visibility, spoiler_text, sensitive, postonload); }; var onerror = function (code, errmsg) { if (throbber) throbber.style.display = 'none'; print('ERROR ' + code + ': ' + errmsg); alert('ERROR ' + code + ': ' + errmsg); resetUI(); lightbox.click(); }; var onabort = function () { if (throbber) throbber.style.display = 'none'; alert('Upload aborted.'); resetUI(); lightbox.click(); }; var filetype = lbcanvas.file.type; print(filetype); // grab the canvas pixel // Note - must grab from the caman canvas not our lbcanvas function blobloaded(blobdata) { let focalpoint = lightboxGetFocalPoint(); uploadmedia(url, access_token, blobdata, focalpoint, onload, onerror, onabort); print('uploading ' + lbcanvas.file.name + ' ...'); } // export canvas as a blob for upload Caman(lbcanvas, function () { this.canvas.toBlob(blobloaded, lbcanvas.file.type); }); }, false); lbcontrols.appendChild(postbtn); // FILTERS BUTTON var filterbtn = document.createElement("button"); filterbtn.setAttribute('style', 'display: block; width: 100%; border-radius:4px; border: 0; font-size: 18px; font-weight:500; color: #fff; background: #2b90d9; padding: 10px; margin-bottom: 10px; margin-right: 10px; height: auto;'); filterbtn.textContent = 'Filters'; filterbtn.addEventListener('click', function (e) { e.stopPropagation(); lightboxShowFilters(); }, false); lbcontrols.appendChild(filterbtn); // STICKERS BUTTON var MAX_STICKERS = 32; // max amount of pngs allowed in a single tar // atm sticker files are tar files containing two or more png files // png files can be up to 255x255, may have alpha, may be of different sizes // no more than MAX_STICKERS pngs allowed. this may be lifted in future release. // recommended: filenames < 24 chars, ascii // to keep script small and single file, compressed tar files are not supported // import_sticker_btn depends on import_sticker_btn // these buttons may be relocated in the near future, // appended as the final item in the sticker list // this is the button the import_sticker_btn (below) will trigger // because input buttons are ugly var import_sticker = document.createElement("input"); import_sticker.setAttribute('type', 'file'); import_sticker.setAttribute('accept', '.tar,application/tar'); import_sticker.setAttribute('id', 'sticker_input'); import_sticker.setAttribute('name', 'stickers'); import_sticker.setAttribute('style', 'display: none;'); // process selected tar file(s) import_sticker.addEventListener('change', function (e) { var files = e.target.files; for (let i = 0; i < files.length; i++) { let file = files[i]; // only uncompressed tar files supported. if (file.type.startsWith('application/tar') || file.type.startsWith('application/x-tar')) { // were good } else continue; let filename = file.name; let filesize = file.size; let filetype = file.type; if (DEBUG) print(filename); print(filesize); print(filetype); var reader = new FileReader(); // process tar file when done loading reader.addEventListener('loadend', function () { if (DEBUG) print('done loading tar file'); var data = reader.result; // array of obj: {buffer:ArrayBuffer, 'width':int, 'height':int} var pngbuffers = []; var offset = 0; // every item in a tar file has a 512-byte header, // plus a final header at the end with all bytes == '\0' for (let j = 0; j < MAX_STICKERS; j++) { let bytes = data.slice(offset, offset+512); let header = new Uint8Array(bytes); // header is always ascii let header_ascii = bytesToAscii(header); //print(header_ascii); let headerobj = asciiToTarHeader(header_ascii); if (!headerobj) break; let add_sticker = true; // don't add folders if (headerobj.type != 'FILE') add_sticker = false; // don't add dot files if (headerobj.name[0] == '.') add_sticker = false; if (add_sticker) { let pngbytes = data.slice(offset + 512, offset + 512 + headerobj.size); //print(pngbytes); // returns null if not a png let pngheader = bytesToPngHeader(pngbytes); if (pngheader && pngheader.width <= 255 && pngheader.height <= 255) { print('Adding ' + headerobj.name); print(headerobj.size); pngbuffers.push({'buffer':pngbytes, 'width':pngheader.width, 'height':pngheader.height}); } } let nextblock = roundToNearestBlock(headerobj.size); //print(nextblock); // nextblock is the number of bytes to the next header beginning where the current header ends // thus, newoffset = header.length + nextblock offset += 512 + nextblock; print('---------------------------------'); } // XXX rename pngbuffers. should these args be properties of the canvas or esily accessible otherwise lightboxAddStickers(pngbuffers, canvas_size, sticker_size); }, false); reader.readAsArrayBuffer(file); } }, false); var import_sticker_btn = document.createElement("button"); import_sticker_btn.setAttribute('style', 'display: block; width: 100%; border-radius:4px; border: 0; font-size: 18px; font-weight:500; color: #fff; background: #2b90d9; padding: 10px; margin-bottom: 10px; margin-right: 10px; height: auto;'); import_sticker_btn.textContent = 'Add Stickers'; import_sticker_btn.addEventListener('click', function (e) { e.stopPropagation(); import_sticker.click(); }, false); lbcontrols.appendChild(import_sticker_btn); var stickerbtn = document.createElement("button"); stickerbtn.setAttribute('style', 'display: block; width: 100%; border-radius:4px; border: 0; font-size: 18px; font-weight:500; color: #fff; background: #2b90d9; padding: 10px; margin-bottom: 10px; margin-right: 10px; height: auto;'); stickerbtn.textContent = 'Stickers'; stickerbtn.addEventListener('click', function (e) { e.stopPropagation(); lightboxShowStickers(); }, false); lbcontrols.appendChild(stickerbtn); // TWEAKS BUTTON (e.g. rotate CW, focal point, brightness+contrast, exposure, crop, text) var editbtn = document.createElement("button"); editbtn.setAttribute('style', 'display: block; width: 100%; border-radius:4px; border: 0; font-size: 18px; font-weight:500; color: #fff; background: #2b90d9; padding: 10px; margin-bottom: 10px; margin-right: 10px; height: auto;'); editbtn.textContent = 'Tweaks'; editbtn.addEventListener('click', function (e) { print('tweak tweak'); e.stopPropagation(); lightboxShowTweaks(); }, false); lbcontrols.appendChild(editbtn); // TEST BUTTON if (DEBUG) { var testlink = document.createElement("a"); testlink.download = 'export.png'; testlink.style.display = 'none'; var testbtn = document.createElement("button"); testbtn.setAttribute('id', 'testbutton'); testbtn.setAttribute('style', 'display: block; width: 100%; border-radius:4px; border: 0; font-size: 18px; font-weight:500; color: #fff; background: #808080; padding: 10px; margin-bottom: 10px; margin-right: 10px; height: auto;'); testbtn.textContent = 'Test'; testbtn.addEventListener('click', function (e) { print('testing 1 2 3'); e.stopPropagation(); var dataurl = lbcanvas.toDataURL('image/png'); testlink.href = dataurl; testlink.click(); }, false); lbcontrols.appendChild(testlink); lbcontrols.appendChild(testbtn); } lbtoppanel.appendChild(lbcontrols); ////////////////////////////// // // // Filters Container // // // ////////////////////////////// var filters = document.createElement("div"); filters.setAttribute('id', 'filter_list'); filters.setAttribute('style', 'display: flex; flex-flow: row nowrap; width: 100%; overflow-y: auto;'); filters.style.display = 'none'; filters.addEventListener('click', function (e) { e.stopPropagation();}, false); // filters get added once an image is loaded ////////////////////////////// // // // Stickers Container // // // ////////////////////////////// var stickers = document.createElement("div"); stickers.setAttribute('id', 'sticker_list'); stickers.setAttribute('style', 'display: flex; flex-flow: row nowrap; width: 100%; overflow-y: auto;'); stickers.style.display = 'none'; stickers.addEventListener('click', function (e) { e.stopPropagation();}, false); ////////////////////////////// // // // Tweaks Container // // // ////////////////////////////// var tweaks = document.createElement("div"); tweaks.setAttribute('id', 'tweak_list'); tweaks.setAttribute('style', 'display: flex; flex-flow: row nowrap; width: 100%; overflow-y: auto;'); tweaks.style.display = 'none'; tweaks.addEventListener('click', function (e) { e.stopPropagation();}, false); // tweak container also holds the current values of all tweak operations tweaks.settings = { brightness: {default_value:0, current_value:0}, contrast: {default_value:0, current_value:0}, saturation: {default_value:0, current_value:0}, exposure: {default_value:0, current_value:0}, gamma: {default_value:1, current_value:1}, hue: {default_value:0, current_value:0}, sepia: {default_value:0, current_value:0}, vibrance: {default_value:0, current_value:0} }; ////////////////////////////// // // // Tweak Options Container // // // ////////////////////////////// var tweakopts = document.createElement("div"); tweakopts.setAttribute('id', 'tweak_options'); tweakopts.setAttribute('style', 'display: flex; flex-flow: row nowrap; width: 100%; overflow-y: auto;'); tweakopts.style.display = 'none'; tweakopts.addEventListener('click', function (e) { e.stopPropagation();}, false); lightbox.appendChild(lbclose); lightbox.appendChild(lbcontent); lbcontent.appendChild(lbtoppanel); lbcontent.appendChild(filters); lbcontent.appendChild(stickers); lbcontent.appendChild(tweaks); lbcontent.appendChild(tweakopts); parent.appendChild(lightbox); if (DEBUG) print('lightbox complete'); return lightbox; } // populates the lightbox function loadLightbox(file, image) { var canvas = document.getElementById('canvas'); if (canvas.getContext) { var ctx = canvas.getContext('2d'); var width = canvas.width; var height = canvas.height; var factor = 1.0; // scale factor // center the image var xoffset = 0; var yoffset = 0; // scale the images down to fit inside the canvas // NOTE this will scale images up if they are smaller too // TODO address the aforementioned if (image.height > image.width) { // if we let image height = canvas.height // then we must scale image width factor = canvas.height / image.height; width = Math.floor( factor * image.width); // center src horiz xoffset = Math.floor(canvas.width / 2 - width / 2); } else if (image.width > image.height) { // if we let image width = canvas.width // then we must scale image height factor = canvas.width / image.width; height = Math.floor( factor * image.height); // center src vert yoffset = Math.floor(canvas.height / 2 - height / 2); } if (DEBUG) { // show alpha portion of the canvas ctx.fillStyle = 'green'; ctx.fillRect(0, 0, canvas.width, canvas.height); } else ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, image.width, image.height, 0+xoffset, 0+yoffset, width, height); canvas.file = file; canvas.bgpixels = ctx.getImageData(0, 0, canvas.width, canvas.height); canvas.procpixels = canvas.bgpixels; canvas.imagedata = {'image':image, 'width':image.width, 'height':image.height, 'xoffset':xoffset, 'yoffset':yoffset, 'dest_width':width, 'dest_height':height}; // add filters // only add those considered 'enabled' var nfilters = getFilterCount(); var filters = []; for (let i = 0; i < nfilters; i++) { let filter = getFilter(i); filters.push(filter); } var thumbnail_size = 128; lightboxAddFilters(image, filters, thumbnail_size); lightboxAddTweaks(); lightboxAddTweakOptions(); } } function processTweaksAndFilter(canvas) { var throbber = document.querySelector('#throbber'); Caman(canvas, function () { if (throbber) throbber.style.display = 'flex'; // pop all caman operations this.revert(); // restore canvas pixels var ctx = canvas.getContext('2d'); ctx.putImageData(canvas.bgpixels, 0, 0); // inform caman of changes to canvas this.reloadCanvasData(); // apply all tweaks on the tweak stack for (let i = 0; i < canvas.tweakstack.length; i++) { let currtweak = canvas.tweakstack[i]; print('applying' + currtweak.type + ': ' + currtweak.value); if (currtweak.type == 'brightness') { this.brightness(currtweak.value); } else if (currtweak.type == 'contrast') { this.contrast(currtweak.value); } else if (currtweak.type == 'exposure') { this.exposure(currtweak.value); } else if (currtweak.type == 'gamma') { this.gamma(currtweak.value); } else if (currtweak.type == 'hue') { this.hue(currtweak.value); } else if (currtweak.type == 'saturation') { this.saturation(currtweak.value); } else if (currtweak.type == 'sepia') { this.sepia(currtweak.value); } else if (currtweak.type == 'vibrance') { this.vibrance(currtweak.value); } else if (currtweak.type == 'hflip') { } } // re-apply last filter, if any if (canvas.currfilter) canvas.currfilter(this); this.render( function () { // save rendered pixels; to be used when we need to clear canvas canvas.procpixels = ctx.getImageData(0, 0, canvas.width, canvas.height); lightboxDrawOverlays(); if (throbber) throbber.style.display = 'none'; }); }); } function lightboxAddFilters(image, filters, thumbnail_size) { print('adding specified filters'); var filter_list = document.querySelector('#filter_list'); if (!filter_list) return; var canvas = document.querySelector('#canvas'); if (!canvas) return; filter_list.style.display = 'flex'; for (let i = 0; i < filters.length; i++) { let filter_group = document.createElement("div"); filter_group.setAttribute('style', 'margin-left: 10px; margin-right: 10px; margin-top: 10px'); let preview = document.createElement("canvas"); preview.textContent = 'No canvas for you'; preview.setAttribute('class', 'filter_preview'); preview.setAttribute('width', thumbnail_size); preview.setAttribute('height', thumbnail_size); // resize image (ie find best fit) and draw thumbnail let ctx = preview.getContext('2d'); let width = thumbnail_size; let height = thumbnail_size; let factor = 1.0; let xoffset = 0; let yoffset = 0; if (image.height > image.width) { factor = preview.height / image.height; width = Math.floor( factor * image.width); // center horiz xoffset = Math.floor(preview.width / 2 - width / 2); } else if (image.width > image.height) { factor = preview.width / image.width; height = Math.floor( factor * image.height); // center vert yoffset = Math.floor(preview.height / 2 - height / 2); } //print('loading filter thumbnail'); ctx.fillStyle = 'black'; ctx.fillRect(0, 0, preview.width, preview.height); ctx.drawImage(image, 0, 0, image.width, image.height, 0+xoffset, 0+yoffset, width, height); // render thumbnail w/ filter applied let filter = filters[i]; let filter_display_name = filter.display_name; let filter_function = filter['function']; //Caman(preview, thisfilter['function']); Caman(preview, function () { filter_function(this); this.render( ); }); filter_group.appendChild(preview); // each thumbnail needs a label indicating the filter let label = document.createElement("p"); label.textContent = filter_display_name; label.setAttribute('style', 'color:#fff; font-size:12px; font-weight:500;'); filter_group.appendChild(label); // add thumnail group to our filter list print('adding filter thumbnail'); filter_list.appendChild(filter_group); // add any event handlers here preview.addEventListener('click', function (e) { e.stopPropagation(); canvas.currfilter = filter_function; processTweaksAndFilter(canvas); }, false); // next filter } } // returns null or a string with two float values: e.g. '0.75,0.5' function lightboxGetFocalPoint() { var focalstr = ''; var canvas = document.querySelector('#canvas'); if (!canvas) return null; // normalize focalpoint var xorigin = canvas.width / 2; var yorigin = canvas.height / 2; var x = canvas.focalpoint.x - xorigin; var y = canvas.focalpoint.y - yorigin; x = x / (canvas.width / 2); y = y / (canvas.height / 2); y = y * -1.0; print('focal ' + x + ', ' + y); focalstr += x + ',' + y; return focalstr; } function lightboxShowFilters() { // hide the sticker list // hide the tweaks list // hide the tweak options // show the filter list var filterlist = document.querySelector('#filter_list'); var stickerlist = document.querySelector('#sticker_list'); var tweaklist = document.querySelector('#tweak_list'); var tweakopts = document.querySelector('#tweak_options'); if (!filterlist) return; if (!stickerlist) return; if (!tweaklist) return; if (!tweakopts) return; filterlist.style.display = 'flex'; stickerlist.style.display = 'none'; tweaklist.style.display = 'none'; tweakopts.style.display = 'none'; } function lightboxAddStickers(stickers, canvas_size, sticker_size) { // did we find any stickers? if (stickers.length < 1) { return; } // hide the filter list // hide the tweaks list var filterlist = document.querySelector('#filter_list'); var tweaklist = document.querySelector('#tweak_list'); var tweakopts = document.querySelector('#tweak_options'); if (!filterlist) return; if (!tweaklist) return; if (!tweakopts) return; filterlist.style.display = 'none'; tweaklist.style.display = 'none'; tweakopts.style.display = 'none'; // populate the sticker list var stickerlist = document.querySelector('#sticker_list'); if (!stickerlist) return; stickerlist.style.display = 'flex'; for (let j = 0; j < stickers.length; j++) { let sticker_group = document.createElement("div"); sticker_group.setAttribute('style', 'margin-left: 10px; margin-right: 10px; margin-top: 10px'); let sticker_preview = document.createElement("img"); sticker_preview.setAttribute('class', 'sticker_preview'); sticker_preview.setAttribute('width', sticker_size); sticker_preview.setAttribute('height', sticker_size); sticker_preview.style.background = '#808080'; // ~ encodeURIComponent let binary = ''; let bytes = new Uint8Array( stickers[j].buffer ); let len = bytes.byteLength; for (let k = 0; k < len; k++) { binary += String.fromCharCode( bytes[ k ] ); } let dataurl = 'data:image/png;base64,' + window.btoa( binary ); sticker_preview.src = dataurl; sticker_preview.addEventListener('click', function (e) { var canvas = document.querySelector('#canvas'); if (!canvas) return; var ctx = canvas.getContext('2d'); // center stickers on canvas var canvas_center = Math.round(canvas_size / 2); // drawImage() uses images top left corner. adjust values ctx.drawImage(sticker_preview, canvas_center - stickers[j].width / 2, canvas_center - stickers[j].height / 2); // add the sticker to the canvas stack // TODO encapsulate the creation of new overlays let overlay_id = canvas.next_stack_id++; canvas.canvasstack.push({'id':overlay_id, 'image':sticker_preview, 'x':canvas_center, 'y':canvas_center, 'width':stickers[j].width, 'height':stickers[j].height, 'srcwidth':stickers[j].width, 'srcheight':stickers[j].height}); }, false); // append children sticker_group.appendChild(sticker_preview); stickerlist.appendChild(sticker_group); } } function lightboxShowStickers() { // hide the filter list // hide the tweaks list // hide the tweak options // show the sticker list var filterlist = document.querySelector('#filter_list'); var stickerlist = document.querySelector('#sticker_list'); var tweaklist = document.querySelector('#tweak_list'); var tweakopts = document.querySelector('#tweak_options'); if (!filterlist) return; if (!stickerlist) return; if (!tweaklist) return; if (!tweakopts) return; filterlist.style.display = 'none'; tweaklist.style.display = 'none'; tweakopts.style.display = 'none'; stickerlist.style.display = 'flex'; } function lightboxDrawOverlays() { var canvas = document.querySelector('#canvas'); if (!canvas) return; var ctx = canvas.getContext('2d'); print('revealing overlay'); for (let i = 0; i < canvas.canvasstack.length; i++) { let ovlayitem = canvas.canvasstack[i]; ctx.drawImage(ovlayitem.image, ovlayitem.x - ovlayitem.width / 2, ovlayitem.y - ovlayitem.height / 2, ovlayitem.width, ovlayitem.height) } // draw the focal point, if any, last if (canvas.focalpoint.visible) { print('drawing focal point'); ctx.save(); ctx.fillStyle = 'rgba(255, 165, 0, 0.5)'; ctx.beginPath(); var radius = canvas.focalpoint.width / 2; ctx.arc(canvas.focalpoint.x, canvas.focalpoint.y, radius, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); ctx.restore(); } } function lightboxRemoveOverlay(overlay_id) { if (overlay_id < 1) { print('Bad overlay ID'); return false; } var canvas = document.querySelector('#canvas'); if (!canvas) return false; var index = -1; for (let i = 0; i < canvas.canvasstack.length; i++) { let ovlayitem = canvas.canvasstack[i]; if (ovlayitem.id == overlay_id) { index = i; break; } } if (index >= 0) { var removed = canvas.canvasstack.splice(index, 1); if (DEBUG) print(canvas.canvasstack); var ctx = canvas.getContext('2d'); ctx.putImageData(canvas.procpixels, 0, 0); lightboxDrawOverlays(); return true; } print('item not found'); return false; } // TODO optimize by req caller to supply overlay item in lieu of id function lightboxScaleOverlay(overlay_id, factor, canvas, ctx) { if (overlay_id < 1) { print('Bad overlay ID'); return false; } for (let i = 0; i < canvas.canvasstack.length; i++) { let ovlayitem = canvas.canvasstack[i]; if (ovlayitem.id == overlay_id) { ovlayitem.width = Math.round(ovlayitem.srcwidth * factor); ovlayitem.height = Math.round(ovlayitem.srcheight * factor); ctx.putImageData(canvas.procpixels, 0, 0); lightboxDrawOverlays(); return true; } } print('item not found'); return false; } function lightboxAddTweaks() { var tweak_size = 128; var tweaklist = document.querySelector('#tweak_list'); if (!tweaklist) return; function loadFocalPoint() { // hide tweak list tweaklist.style.display = 'none'; lightboxShowTweakOptions('FOCAL'); } function loadRotate() { // hide tweak list tweaklist.style.display = 'none'; lightboxShowTweakOptions('ROTATE'); } // callback used when slider changes // 1st arg is the slider itself, 2nd is any args you assigned to the callback function applytweak(slider, args) { var canvas = document.querySelector('#canvas'); if (!canvas) return; optype = args[0]; // add this tweak to the tweak stack // if tweak already in stack, replace it, otherwise append var addtweak = true; for (let i = 0; i < canvas.tweakstack.length; i++) { let currtweak = canvas.tweakstack[i]; if (currtweak.type == optype) { print('replacing tweak'); addtweak = false; currtweak.value = Number(slider.value); break; } } if (addtweak) { print('adding a new tweak'); canvas.tweakstack.push({type:optype, value: Number(slider.value)}); } processTweaksAndFilter(canvas); tweaklist.settings[optype].current_value = slider.value; }; // this is called whenever the user decides to remove the tweak function removetweak(args) { var canvas = document.querySelector('#canvas'); if (!canvas) return; optype = args[0]; var index = -1; for (let i = 0; i < canvas.tweakstack.length; i++) { let currtweak = canvas.tweakstack[i]; if (currtweak.type == optype) { index = i; break; } } if (index >= 0) { // remove tweak from stack canvas.tweakstack.splice(index, 1); // reset current value var tweaklist = document.querySelector('#tweak_list'); tweaklist.settings[optype].current_value = tweaklist.settings[optype].default_value; // redraw processTweaksAndFilter(canvas); } } // dont forget to add the caman filter op in processTweaksAndFilter( ) and the entries to tweaks.settings function loadBrightness() { // hide tweak list tweaklist.style.display = 'none'; // fetch current value var currvalue = tweaklist.settings.brightness.current_value; lightboxShowTweakOptions('SLIDER', {min:-100, max:100, step:1, value:currvalue}, applytweak, ['brightness'], removetweak, ['brightness']); } function loadContrast() { // hide tweak list tweaklist.style.display = 'none'; // fetch current value var currvalue = tweaklist.settings.contrast.current_value; lightboxShowTweakOptions('SLIDER', {min:-20, max:20, step:1, value:currvalue}, applytweak, ['contrast'], removetweak, ['contrast']); } function loadExposure() { // hide tweak list tweaklist.style.display = 'none'; // fetch current value var currvalue = tweaklist.settings.exposure.current_value; lightboxShowTweakOptions('SLIDER', {min:-100, max:100, step:1, value:currvalue}, applytweak, ['exposure'], removetweak, ['exposure']); } function loadGamma() { // hide tweak list tweaklist.style.display = 'none'; // fetch current value var currvalue = tweaklist.settings.gamma.current_value; lightboxShowTweakOptions('SLIDER', {min:1, max:4, step:0.1, value:currvalue}, applytweak, ['gamma'], removetweak, ['gamma']); } function loadHue() { // hide tweak list tweaklist.style.display = 'none'; // fetch current value var currvalue = tweaklist.settings.hue.current_value; lightboxShowTweakOptions('SLIDER', {min:0, max:100, step:1, value:currvalue}, applytweak, ['hue'], removetweak, ['hue']); } function loadSaturation() { // hide tweak list tweaklist.style.display = 'none'; // fetch current value var currvalue = tweaklist.settings.saturation.current_value; lightboxShowTweakOptions('SLIDER', {min:-100, max:100, step:1, value:currvalue}, applytweak, ['saturation'], removetweak, ['saturation']); } function loadSepia() { // hide tweak list tweaklist.style.display = 'none'; // fetch current value var currvalue = tweaklist.settings.sepia.current_value; lightboxShowTweakOptions('SLIDER', {min:0, max:100, step:1, value:currvalue}, applytweak, ['sepia'], removetweak, ['sepia']); } function loadVibrance() { // hide tweak list tweaklist.style.display = 'none'; // fetch current value var currvalue = tweaklist.settings.vibrance.current_value; lightboxShowTweakOptions('SLIDER', {min:-100, max:100, step:1, value:currvalue}, applytweak, ['vibrance'], removetweak, ['vibrance']); } var tweaks = [ {name:'focalpoint', displayname:'Focal Point', icon:'fa-crosshairs', enabled:true, setup:loadFocalPoint}, {name:'rotate', displayname:'Rotate', icon:'fa-undo', enabled:true, setup:loadRotate}, {name:'crop', displayname:'Crop', icon:'fa-crop', enabled:false, setup:null}, {name:'brightness', displayname:'Brightness', icon:'fa-bolt', enabled:true, setup:loadBrightness}, {name:'channel', displayname:'Channel', icon:'fa-cog', enabled:false, setup:null}, {name:'colorize', displayname:'Colorize', icon:'fa-cog', enabled:false, setup:null}, {name:'contrast', displayname:'Contrast', icon:'fa-adjust', enabled:true, setup:loadContrast}, {name:'exposure', displayname:'Exposure', icon:'fa-camera', enabled:true, setup:loadExposure}, {name:'gamma', displayname:'Gamma', icon:'fa-signal', enabled:true, setup:loadGamma}, {name:'hue', displayname:'Hue', icon:'fa-tint', enabled:true, setup:loadHue}, {name:'saturation', displayname:'Saturation', icon:'fa-edit', enabled:true, setup:loadSaturation}, {name:'sepia', displayname:'Sepia', icon:'fa-image', enabled:true, setup:loadSepia}, {name:'vibrance', displayname:'Vibrance', icon:'fa-plus-circle', enabled:true, setup:loadVibrance}, {name:'blur', displayname:'Blur', icon:'fa-beer', enabled:false, setup:null}, {name:'sharpen', displayname:'Sharpen', icon:'fa-binoculars', enabled:false, setup:null}, {name:'text', displayname:'Text', icon:'fa-font', enabled:false, setup:null} ]; // populate the tweak list for (let i = 0; i < tweaks.length; i++) { let tweak = tweaks[i]; print('Adding ' + tweak.displayname); let tweak_group = document.createElement("div"); tweak_group.setAttribute('style', 'margin-left: 10px; margin-right: 10px; margin-top: 10px'); let tweak_button = document.createElement("button"); tweak_button.setAttribute('class', 'icon-button'); tweak_button.setAttribute('style', 'font-size: 80px; border: 0; border-radius:24px; width:128px; line-height: 128px; color: #fff;'); tweak_button.setAttribute('width', tweak_size); tweak_button.setAttribute('height', tweak_size); var tweak_color = '#808080'; if (tweak.enabled) tweak_color = '#2b90d9'; tweak_button.style.background = tweak_color; if (DEBUG && window.location.origin.startsWith('http:')) tweak_button.textContent = '+'; let tweak_button_icon = document.createElement("i"); tweak_button_icon.setAttribute('class', 'fa fa-fw ' + tweak.icon); tweak_button.appendChild(tweak_button_icon); // each button needs a label indicating the tweak let label = document.createElement("p"); label.textContent = tweak.displayname; label.setAttribute('style', 'color:#fff; font-size:12px; font-weight:500;'); tweak_group.appendChild(tweak_button); tweak_group.appendChild(label); // add button group to our tweak list print('adding tweak button'); tweaklist.appendChild(tweak_group); // add any event handlers here tweak_button.addEventListener('click', function (e) { e.stopPropagation(); if (tweak.setup) tweak.setup(); }, false); } } function lightboxShowTweaks() { // hide the filter list // hide the sticker list // hide the tweak options // show the tweak list var filterlist = document.querySelector('#filter_list'); var stickerlist = document.querySelector('#sticker_list'); var tweaklist = document.querySelector('#tweak_list'); var tweakopts = document.querySelector('#tweak_options'); if (!filterlist) return; if (!stickerlist) return; if (!tweaklist) return; if (!tweakopts) return; print('showing tweaks'); filterlist.style.display = 'none'; stickerlist.style.display = 'none'; tweakopts.style.display = 'none'; tweaklist.style.display = 'flex'; } function lightboxAddTweakOptions() { var tweakopts = document.querySelector('#tweak_options'); if (!tweakopts) return; ////////////////////////// // SCALAR SLIDER // ////////////////////////// // create a slider group var slider_group = document.createElement("div"); slider_group.setAttribute('id', 'tweak_slider_group'); slider_group.setAttribute('style', 'display:flex; justify-content: center; width: 100%;'); slider_group.style.display = 'none'; // all tweak opt controls are hidden by default if (DEBUG) slider_group.style.background = '#808080' slider_group.addEventListener('change', function (e) { e.stopPropagation(); }, false); var tweakslider = document.createElement("input"); tweakslider.setAttribute('id', 'tweak_slider'); tweakslider.setAttribute('type', 'range'); tweakslider.setAttribute('min', '0.0'); tweakslider.setAttribute('max', '1.0'); tweakslider.setAttribute('step', '0.1'); if (DEBUG) tweakslider.style.background = '#808080' tweakslider.setAttribute('style', 'appearance: none; height:50px; width: 300px;'); // will be overriden tweakslider.onchange = function (e) { e.stopPropagation(); print(tweakslider.value); }; // remove var remove_tweak = document.createElement("button"); remove_tweak.setAttribute('id', 'tweak_slider_remove'); remove_tweak.setAttribute('style', 'width: 32px; height: 32px; border-radius: 32px; border: 0; margin-top: 8px; margin-right: 10px; font-size: 18px; font-weight:500; color: #fff; background: #2b90d9;'); remove_tweak.textContent = 'x'; // will be overidden remove_tweak.onclick = function (e) { e.stopPropagation(); slider_group.style.display = 'none'; lightboxShowTweaks(); }; // close var closebutton = document.createElement("button"); closebutton.setAttribute('id', 'tweak_slider_close'); closebutton.setAttribute('style', 'height: 32px; border-radius: 32px; border: 0; margin-top: 8px; font-size: 18px; font-weight:500; color: #fff; background: #2b90d9;'); closebutton.textContent = 'close'; closebutton.onclick = function (e) { e.stopPropagation(); // hide and unhide necessary controls slider_group.style.display = 'none'; lightboxShowTweaks(); }; slider_group.appendChild(tweakslider); slider_group.appendChild(remove_tweak); slider_group.appendChild(closebutton); tweakopts.appendChild(slider_group); ////////////////////////// // COLOR SLIDER // ////////////////////////// ////////////////////////// // ROTATE GROUP // ////////////////////////// var rotbtnsize = 128; let rotate_group = document.createElement("div"); rotate_group.setAttribute('id', 'rotate_group'); rotate_group.setAttribute('style', 'display:flex; flex-direction: row; justify-content: center; width: 100%;'); rotate_group.style.display = 'none'; // add button group to our tweak opts tweakopts.appendChild(rotate_group); ///// CW Rotate Button let cw_group = document.createElement("div"); //cw_group.setAttribute('style', 'display:flex; flex-direction: column;'); let cwrotate_button = document.createElement("button"); cwrotate_button.setAttribute('class', 'icon-button'); cwrotate_button.setAttribute('style', 'font-size: 80px; border: 0; border-radius:24px; width:128px; line-height: 128px; color: #fff; margin-right: 10px;'); //cwrotate_button.setAttribute('width', rotbtnsize); //cwrotate_button.setAttribute('height', rotbtnsize); cwrotate_button.style.background = '#2b90d9'; if (DEBUG && window.location.origin.startsWith('http:')) cwrotate_button.textContent = 'C'; let cwrotate_button_icon = document.createElement("i"); cwrotate_button_icon.setAttribute('class', 'fa fa-fw fa-undo'); cwrotate_button_icon.setAttribute('style', 'transform: scale(-1, 1);'); cwrotate_button.appendChild(cwrotate_button_icon); // each button needs a label indicating the rotate/flip let cwlabel = document.createElement("p"); cwlabel.textContent = 'Rotate CW'; cwlabel.setAttribute('style', 'color:#fff; font-size:12px; font-weight:500;'); cw_group.appendChild(cwrotate_button); cw_group.appendChild(cwlabel); rotate_group.appendChild(cw_group); ///// CCW Rotate Button let ccw_group = document.createElement("div"); //ccw_group.setAttribute('style', 'display:flex; flex-direction: column;'); let ccwrotate_button = document.createElement("button"); ccwrotate_button.setAttribute('class', 'icon-button'); ccwrotate_button.setAttribute('style', 'font-size: 80px; border: 0; border-radius:24px; width:128px; line-height: 128px; color: #fff; margin-right: 10px;'); ccwrotate_button.setAttribute('width', rotbtnsize); ccwrotate_button.setAttribute('height', rotbtnsize); ccwrotate_button.style.background = '#2b90d9'; if (DEBUG && window.location.origin.startsWith('http:')) ccwrotate_button.textContent = 'U'; let ccwrotate_button_icon = document.createElement("i"); ccwrotate_button_icon.setAttribute('class', 'fa fa-fw fa-undo'); ccwrotate_button.appendChild(ccwrotate_button_icon); // each button needs a label indicating the rotate/flip let ccwlabel = document.createElement("p"); ccwlabel.textContent = 'Rotate CCW'; ccwlabel.setAttribute('style', 'color:#fff; font-size:12px; font-weight:500;'); ccw_group.appendChild(ccwrotate_button); ccw_group.appendChild(ccwlabel); rotate_group.appendChild(ccw_group); ///// Flip Vertical Button let vflip_group = document.createElement("div"); let vflip_button = document.createElement("button"); vflip_button.setAttribute('class', 'icon-button'); vflip_button.setAttribute('style', 'font-size: 80px; border: 0; border-radius:24px; width:128px; line-height: 128px; color: #fff; margin-right: 10px;'); vflip_button.setAttribute('width', rotbtnsize); vflip_button.setAttribute('height', rotbtnsize); vflip_button.style.background = '#2b90d9'; if (DEBUG && window.location.origin.startsWith('http:')) vflip_button.textContent = 'V'; let vflip_button_icon = document.createElement("i"); vflip_button_icon.setAttribute('class', 'fa fa-fw fa-image'); vflip_button_icon.setAttribute('style', 'transform: scale(1, -1);'); vflip_button.appendChild(vflip_button_icon); // each button needs a label indicating the rotate/flip let vfliplabel = document.createElement("p"); vfliplabel.textContent = 'Flip Vertical'; vfliplabel.setAttribute('style', 'color:#fff; font-size:12px; font-weight:500;'); vflip_group.appendChild(vflip_button); vflip_group.appendChild(vfliplabel); rotate_group.appendChild(vflip_group); ///// Flip Horizontal Button let hflip_group = document.createElement("div"); let hflip_button = document.createElement("button"); hflip_button.setAttribute('class', 'icon-button'); hflip_button.setAttribute('style', 'font-size: 80px; border: 0; border-radius:24px; width:128px; line-height: 128px; color: #fff; margin-right: 10px;'); hflip_button.setAttribute('width', rotbtnsize); hflip_button.setAttribute('height', rotbtnsize); hflip_button.style.background = '#2b90d9'; if (DEBUG && window.location.origin.startsWith('http:')) hflip_button.textContent = 'H'; let hflip_button_icon = document.createElement("i"); hflip_button_icon.setAttribute('class', 'fa fa-fw fa-image'); hflip_button.appendChild(hflip_button_icon); // each button needs a label indicating the rotate/flip let hfliplabel = document.createElement("p"); hfliplabel.textContent = 'Flip Horizontal'; hfliplabel.setAttribute('style', 'color:#fff; font-size:12px; font-weight:500;'); hflip_group.appendChild(hflip_button); hflip_group.appendChild(hfliplabel); rotate_group.appendChild(hflip_group); // add any event handlers here cwrotate_button.addEventListener('click', function (e) { e.stopPropagation(); var canvas = document.querySelector('#canvas'); if (canvas) rotateCanvas(canvas, 'CW'); }, false); ccwrotate_button.addEventListener('click', function (e) { e.stopPropagation(); var canvas = document.querySelector('#canvas'); if (canvas) rotateCanvas(canvas, 'CCW'); }, false); vflip_button.addEventListener('click', function (e) { e.stopPropagation(); var canvas = document.querySelector('#canvas'); if (canvas) flipCanvas(canvas, 'VERTICAL'); }, false); hflip_button.addEventListener('click', function (e) { e.stopPropagation(); var canvas = document.querySelector('#canvas'); if (canvas) flipCanvas(canvas, 'HORIZONTAL'); }, false); ////////////////////////// // FOCAL GROUP // ////////////////////////// var focalbtnsize = 128; let focal_group = document.createElement("div"); focal_group.setAttribute('id', 'focal_group'); focal_group.setAttribute('style', 'display:flex; flex-direction: row; justify-content: center; width: 100%;'); focal_group.style.display = 'none'; // add button group to our tweak opts tweakopts.appendChild(focal_group); ///// Show Button let show_focal = document.createElement("button"); show_focal.setAttribute('class', 'icon-button'); show_focal.setAttribute('style', 'font-size: 80px; border: 0; border-radius:24px; width:128px; line-height: 128px; color: #fff; margin-right: 10px;'); show_focal.setAttribute('width', focalbtnsize); show_focal.setAttribute('height', focalbtnsize); show_focal.style.background = '#2b90d9'; if (DEBUG && window.location.origin.startsWith('http:')) show_focal.textContent = 'S'; let show_focal_icon = document.createElement("i"); show_focal_icon.setAttribute('class', 'fa fa-fw fa-eye'); show_focal.appendChild(show_focal_icon); focal_group.appendChild(show_focal); ///// Hide Button let hide_focal = document.createElement("button"); hide_focal.setAttribute('class', 'icon-button'); hide_focal.setAttribute('style', 'font-size: 80px; border: 0; border-radius:24px; width:128px; line-height: 128px; color: #fff; margin-right: 10px;'); hide_focal.setAttribute('width', focalbtnsize); hide_focal.setAttribute('height', focalbtnsize); hide_focal.style.background = '#2b90d9'; if (DEBUG && window.location.origin.startsWith('http:')) hide_focal.textContent = 'H'; let hide_focal_icon = document.createElement("i"); hide_focal_icon.setAttribute('class', 'fa fa-fw fa-eye-slash'); hide_focal.appendChild(hide_focal_icon); focal_group.appendChild(hide_focal); show_focal.addEventListener('click', function (e) { e.stopPropagation(); print('show focal point'); var canvas = document.querySelector('#canvas'); if (canvas) { canvas.focalpoint.visible = true; var ctx = canvas.getContext('2d'); ctx.putImageData(canvas.procpixels, 0, 0); lightboxDrawOverlays(); } }, false); hide_focal.addEventListener('click', function (e) { e.stopPropagation(); print('hide focal point'); var canvas = document.querySelector('#canvas'); if (canvas) { canvas.focalpoint.visible = false; var ctx = canvas.getContext('2d'); ctx.putImageData(canvas.procpixels, 0, 0); lightboxDrawOverlays(); } }, false); } // settings (obj) // For sliders: // min // max // step // value // // onchange (function). recieves 2 args: the slider and the array you supplied // // onchange_args is an array that is passed as the 2nd arg of the callback // // onremove (function) gets called when user click the remove button (x) // function lightboxShowTweakOptions(opttype, settings, onchange, onchange_args, onremove, remove_args) { // hide the filter list // hide the sticker list // hide the tweak options // show the tweak list var filterlist = document.querySelector('#filter_list'); var stickerlist = document.querySelector('#sticker_list'); var tweaklist = document.querySelector('#tweak_list'); var tweakopts = document.querySelector('#tweak_options'); if (!filterlist) return; if (!stickerlist) return; if (!tweaklist) return; if (!tweakopts) return; print('showing tweak opts'); filterlist.style.display = 'none'; stickerlist.style.display = 'none'; tweaklist.style.display = 'none'; tweakopts.style.display = 'flex'; // hide all tools by default var slider_group = document.querySelector('#tweak_slider_group'); if (!slider_group) return; var rotate_group = document.querySelector('#rotate_group'); if (!rotate_group) return; var focal_group = document.querySelector('#focal_group'); if (!focal_group) return; // start everything w/ a clean slate slider_group.style.display = 'none'; rotate_group.style.display = 'none'; focal_group.style.display = 'none'; // there are many tweak opts. which one? if (opttype == 'SLIDER') { print('this tweak requested a slider'); slider_group.style.display = 'flex'; // init/restore slider var slider = document.querySelector('#tweak_slider'); if (!slider) return; slider.min = settings.min; slider.max = settings.max; slider.step = settings.step; slider.value = settings.value; slider.onchange = function (e) { e.stopPropagation(); if (onchange) onchange(slider, onchange_args); }; var remove = document.querySelector('#tweak_slider_remove'); // call this when the remove button is clicked if (remove) { remove.onclick = function (e) { e.stopPropagation(); if (onremove) onremove(remove_args); slider_group.style.display = 'none'; lightboxShowTweaks(); }; } } else if (opttype == 'ROTATE') { print('this tweak requested rotate opts'); rotate_group.style.display = 'flex'; } else if (opttype == 'FOCAL') { print('this tweak requested focal point opts'); focal_group.style.display = 'flex'; } } // TODO use the caman rotate filer // mode: 'CW', 'CCW' function rotateCanvas(canvas, mode) { print('rotating'); var rotation = 90; if (mode == 'CCW') rotation = -90; canvas.current_rotation = (canvas.current_rotation + rotation) % 360; var ctx = canvas.getContext('2d'); ctx.save(); ctx.translate(canvas.width/2, canvas.height/2); ctx.rotate((Math.PI / 180) * canvas.current_rotation); ctx.drawImage(canvas.imagedata.image, 0, 0, canvas.imagedata.image.width, canvas.imagedata.image.height, -canvas.width/2 + canvas.imagedata.xoffset, -canvas.height/2 + canvas.imagedata.yoffset, canvas.imagedata.dest_width, canvas.imagedata.dest_height); ctx.restore(); canvas.bgpixels = ctx.getImageData(0, 0, canvas.width, canvas.height); processTweaksAndFilter(canvas); print('done'); } // TODO flips must compound. either bake output: image -> dataurl -> new image // or add op to tweak stack // mode: VERTICAL, HORIZONTAL function flipCanvas(canvas, mode) { print('flip'); var ctx = canvas.getContext('2d'); ctx.save(); if (mode == 'VERTICAL') { if (!canvas.yflipped) { ctx.translate(0,canvas.height); ctx.scale(1,-1); canvas.yflipped = true; } else { canvas.yflipped = false; } } else if (mode == 'HORIZONTAL') { if (!canvas.xflipped) { ctx.translate(canvas.width, 0); ctx.scale(-1,1); canvas.xflipped = true; } else { canvas.xflipped = false; } } else { // bad mode return; } //ctx.drawImage(canvas.imagedata.image, 0, 0, canvas.imagedata.image.width, canvas.imagedata.image.height, -canvas.width/2 + canvas.imagedata.xoffset, -canvas.height/2 + canvas.imagedata.yoffset, canvas.imagedata.dest_width, canvas.imagedata.dest_height); ctx.drawImage(canvas.imagedata.image, 0, 0, canvas.imagedata.image.width, canvas.imagedata.image.height, 0 + canvas.imagedata.xoffset, 0 + canvas.imagedata.yoffset, canvas.imagedata.dest_width, canvas.imagedata.dest_height); ctx.restore(); canvas.bgpixels = ctx.getImageData(0, 0, canvas.width, canvas.height); processTweaksAndFilter(canvas); print('done'); } // atm glam toots your pic from inside the lightbox // in the future the lightbox will hopefully be able to add image in compose box // and toot proceeds as usual function getVisibilityFromUI() { var itag = document.querySelector('.privacy-dropdown__value-icon i'); if (!itag) return null; if (itag.classList.contains('fa-globe')) return 'public'; if (itag.classList.contains('fa-unlock-alt')) return 'unlisted'; if (itag.classList.contains('fa-lock')) return 'private'; if (itag.classList.contains('fa-envelope')) return 'direct'; return null; } function getTextFromUI() { var textarea = document.querySelector('.compose-form textarea'); if (!textarea) return '.'; return textarea.value; } function getSpoilerTextFromUI() { var cwinput = document.querySelector('#cw-spoiler-input'); if (!cwinput) return null; var value = cwinput.value; if (!value) return null; return value; } function resetUI() { // clear text area var textarea = document.querySelector('.compose-form textarea'); if (textarea) textarea.value = ''; // clear spoiler text var cwinput = document.querySelector('#cw-spoiler-input'); if (cwinput) cwinput.value = ''; // clear cw // reset visibility var visbut = document.querySelector('div .privacy-dropdown__value button'); if (visbut) visbut.click(); var pubopt = document.querySelector('div[data-index="public"]'); if (pubopt) pubopt.click(); } // returns the number of enabled filters // the user will be able to set which filters in the future function getFilterCount() { var filters = getFilters(); return filters.length; } // The filters that ship with glam: // Note - CamanJS allows users to define new filters // TODO document how var _filters = { normal: {'function': function (caman) { }, 'display_name':'Normal'}, vintage: {'function': function (caman) { caman.vintage(); }, 'display_name':'Vintage'}, lomo: {'function': function (caman) { caman.lomo(); }, 'display_name':'Lomo'}, clarity: {'function': function (caman) { caman.clarity(); }, 'display_name':'Clarity'}, sinCity: {'function': function (caman) { caman.sinCity(); }, 'display_name':'Sin City'}, sunrise: {'function': function (caman) { caman.sunrise(); }, 'display_name':'Sunrise'}, crossProcess: {'function': function (caman) { caman.crossProcess(); }, 'display_name':'Cross Process'}, orangePeel: {'function': function (caman) { caman.orangePeel(); }, 'display_name':'Orange Peel'}, love: {'function': function (caman) { caman.love(); }, 'display_name':'Love'}, grungy: {'function': function (caman) { caman.grungy(); }, 'display_name':'Grungy'}, jarques: {'function': function (caman) { caman.jarques(); }, 'display_name':'Jarques'}, pinhole: {'function': function (caman) { caman.pinhole(); }, 'display_name':'Pinhole'}, oldBoot: {'function': function (caman) { caman.oldBoot(); }, 'display_name':'Old Boot'}, glowingSun: {'function': function (caman) { caman.glowingSun(); }, 'display_name':'Glowing Sun'}, hazyDays: {'function': function (caman) { caman.hazyDays(); }, 'display_name':'Hazy Days'}, herMajesty: {'function': function (caman) { caman.herMajesty(); }, 'display_name':'Her Majesty'}, nostalgia: {'function': function (caman) { caman.nostalgia(); }, 'display_name':'Nostalgia'}, hemingway: {'function': function (caman) { caman.hemingway(); }, 'display_name':'Hemmingway'}, concentrate: {'function': function (caman) { caman.concentrate(); }, 'display_name':'Concentrate'}, }; function getFilters() { var filters = []; // enabled and in this order // TODO fetch from local storage var enabled = ['normal', 'vintage', 'sinCity', 'sunrise', 'grungy', 'pinhole', 'glowingSun', 'hazyDays', 'herMajesty', 'nostalgia', 'concentrate']; for (let i = 0; i < enabled.length; i++) { var keyword = enabled[i]; let filter = _filters[keyword]; filters.push(filter); } return filters; } function getFilter(index) { var filters = getFilters(); if (index >= filters.length) { return null; } return filters[index]; } // add a button to the compose box that lets user choose // an image to apply a filter or sticker function insertChooseAFile() { var target = document.querySelector('.compose-form__buttons'); if (!target) target = document.querySelector('.logos'); // DEBUG if (target) { // this is the hutton the iconbutton (below) will trigger var button = document.createElement("input"); button.setAttribute('type', 'file'); button.setAttribute('accept', '.jpg,.jpeg,.png,image/jpeg,image/png'); button.setAttribute('id', 'file_input'); button.setAttribute('name', 'files'); button.setAttribute('style', 'display: none;'); button.addEventListener('change', function (e) { var files = e.target.files; for (let i = 0; i < files.length; i++) { let file = files[i]; // only jpegs and pngs are supported atm if (file.type.startsWith('image/jpeg') || file.type.startsWith('image/png')) { // were good } else continue; let filename = file.name; let filesize = file.size; let filetype = file.type; if (DEBUG) print(filename); print(filesize); print(filetype); var img = document.createElement("img"); var reader = new FileReader(); // add the image to our light after we're done loading it reader.addEventListener('load', function () { print('done reading file'); print('we need a lightbox'); var lightbox = document.querySelector('#lightbox'); if (lightbox == null) { print('requesting a new one'); var body = document.getElementsByTagName("body"); lightbox = createLightbox(body[0]); } else { // atm not re-using lightox. a new one must be created print('found one'); } // when the image tag is done loading img.addEventListener('load', function() { print('request image load in lightbox'); loadLightbox(file, img); lightbox.style.display = 'block'; }, false); img.src = reader.result; }, false); reader.readAsDataURL(file); } }, false); var iconbutton = document.createElement("button"); iconbutton.setAttribute('aria-label', 'Add media'); iconbutton.setAttribute('title', 'Add media'); iconbutton.setAttribute('class', 'compose-form__upload-button-icon icon-button inverted'); iconbutton.setAttribute('style', 'font-size: 18px; width: 23.1429px; line-height: 27px;'); iconbutton.addEventListener('click', function (e) { button.click(); }, false); var icon = document.createElement("i"); icon.setAttribute('class', 'fa fa-fw fa-image'); icon.setAttribute('aria-hidden', 'true'); iconbutton.appendChild(icon); target.appendChild(button); target.appendChild(iconbutton); } } // converts an array of uint bytes into array of ascii/unicode chars // ascii if byte value < 128 function bytesToAscii(bytes) { var ascii = []; for (let i = 0; i < bytes.length; i++) { var byte = bytes[i]; var char = String.fromCharCode(byte); ascii.push(char); } return ascii; } // takes an array of (ascii) digits representing an octal value // returns a decimal number // octal digits may be zero padded function octalDigitsToDecimalValue(octaldigits) { // used to determine digits used for padding (0s) from // digits that are part of the actual octal // (not all zeros encountered are for padding) var padding_ended = false; var total = 0; var valid_digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; for (let i = 0; i < octaldigits.length; i++) { var digit = octaldigits[i]; // quit as soon as a non digit is encountered (e.g. a space) if (valid_digits.indexOf(digit) < 0) break; intdigit = Number(digit); if ( intdigit > 0 ) padding_ended = true; if (padding_ended == false) continue; total = total * 8 + intdigit; } return total; } // find the *next* nearest block. next ~= ceil // block = 512 function roundToNearestBlock(value) { var blocksize = 512; return Math.ceil(value/blocksize) * blocksize; } // header is an array of ascii values // returns an object with many of the propertiesfound in the tar header: // // name, size, type, magic // function asciiToTarHeader(header) { var object = {}; var asciis = []; //////////////// // MAGIC // //////////////// asciis = header.slice(257, 257+6); //print(asciis); // must be [ 'u', 's', 't', 'a', 'r', '\0' ] if (asciis[0] == 'u' && asciis[1] == 's' && asciis[2] == 't' && asciis[3] == 'a' && asciis[4] == 'r' ) { // we're good } else { return null; } object['magic'] = 'ustar'; //////////////// // NAME // //////////////// var name = ''; asciis = header.slice(0, 100); //print(asciis); for (let i = 0; i < 100; i++) { let ascii = asciis[i]; if (ascii == '\0') break; name += ascii; } object['name'] = name; //////////////// // SIZE // //////////////// asciis = header.slice(124, 124+12); //print(asciis); var intsize = octalDigitsToDecimalValue(asciis); //print('size ' + intsize); object['size'] = intsize; //////////////// // TYPE // //////////////// // '0' = file, '1' = link, '2' = reserved, '3' = char special, '4' = block special, '5' = folder, '6' = FIFO, '7' = reserved asciis = header.slice(156, 157); //print('type=' + asciis); var longtype = ''; switch (asciis[0]) { case '0': longtype = 'FILE'; break; case '1': longtype = 'LINK'; break; case '2': case '7': longtype = 'RESERVED'; break; case '3': longtype = 'CHAR'; break; case '4': longtype = 'BLOCK'; break; case '5': longtype = 'DIRECTORY'; break; case '6': longtype = 'FIFO'; break; default: return null; } object['type'] = longtype; return object; } // bytes is an ArrayBuffer function bytesToPngHeader(bytes) { object = {}; var uintbytes = new Uint8Array(bytes); //print(uintbytes); //////////////// // MAGIC // //////////////// // first 8 bytes must be: 137 80 78 71 13 10 26 10 if (uintbytes[0] == 137 && uintbytes[1] == 80 && uintbytes[2] == 78 && uintbytes[3] == 71 && uintbytes[4] == 13 && uintbytes[5] == 10 && uintbytes[6] == 26 && uintbytes[7] == 10) { // we're good } else { return null; } ////////////////// // CHUNK SIZE // ////////////////// // next 4 bytes are the length of the IHDR. Should be 13 // Integers values are in so-called network byte order (big endian) // The most significant byte comes first, the least significant byte comes last // e.g. when using 4 bytes 13 is stored as 00 00 00 13 // However most of the times Uint32Array is Little Endian (depends on the host which is usually little endian) // So Uint32Array expects 13 00 00 00 var intbytes = new Uint8Array( bytes.slice(8, 8+4) ); intbytes.reverse(); intbytes = new Uint32Array(intbytes); var size = intbytes[0]; if (size != 13) return null; object['chunk_size'] = size; ////////////////// // CHUNK TYPE // ////////////////// // next 4 bytes are the chunk type (IHDR) uintbytes = new Uint8Array( bytes.slice(12, 12+4) ); //print(uintbytes); var chunk_type = ''; for (let i = 0; i < 4; i++) { let byte = uintbytes[i]; let char = String.fromCharCode(byte); chunk_type += char; } object['chunk_type'] = chunk_type; ////////////////// // IMAGE WIDTH // ////////////////// // next 4 bytes are the width intbytes = new Uint8Array( bytes.slice(16, 16+4) ); intbytes.reverse(); intbytes = new Uint32Array(intbytes); var width = intbytes[0]; if (width == 0) return null; object['width'] = width; ////////////////// // IMAGE HEIGHT // ////////////////// // next 4 bytes are the height intbytes = new Uint8Array( bytes.slice(20, 20+4) ); intbytes.reverse(); intbytes = new Uint32Array(intbytes); var height = intbytes[0]; if (height == 0) return null; object['height'] = height; //print(' ' + width + 'x' + height); return object; } function get_access_token() { var script = document.querySelector('#initial-state'); if (script) { var jsondata = JSON.parse(script.textContent); if (jsondata && jsondata.hasOwnProperty('meta') && jsondata.meta.hasOwnProperty('access_token') ) return jsondata.meta.access_token; else return null; } else { return null; } } function urljoin(a, b) { return a + b; } // accepts an obj with key/values and returns a url encoded string function urlencode(obj) { var queries = []; for (let key in obj) { if (!obj.hasOwnProperty(key)) continue; queries.push(key + '=' + encodeURIComponent(obj[key])); } querystr = queries.join('&'); return querystr; } function post(url, access_token, message, visibility, spoiler_text) { if (!url) { print('missing url'); return false; } if (!access_token) { print('missing access token'); return false; } message = message || '.'; visibility = visibility || 'public'; var visibilities = ['public', 'unlisted', 'private', 'direct']; if (visibilities.indexOf(visibility) < 0) visibility = 'public'; var endpoint = urljoin(url, '/api/v1/statuses'); var query = new XMLHttpRequest(); query.addEventListener('error', function(ev) { print('ERROR'); }); query.addEventListener('abort', function(ev) { print('ABORT'); }); query.addEventListener('load', function(ev) { print(query.status); print(query.responseText); }); query.open('POST', endpoint); query.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); query.setRequestHeader('Accept', 'application/json'); query.setRequestHeader('Authorization', 'Bearer ' + access_token); //query.setRequestHeader('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0'); var params = {'status':message, 'visibility':visibility}; if (spoiler_text) params['spoiler_text'] = spoiler_text; var data = urlencode(params); query.send(data); } function uploadmedia(url, access_token, fileobj, focalpoint, onload, onerror, onabort) { // focalpoint - (optional) a string with normalized float values separated by comma ('0.5,0.5') // onload - function/callback when media successfully uploaded. // should accept 3 (string) args: url, access_token, media_id // onerror - function/callback when error or status code other than 200 occurs. // should accept 2 args: code (int), description (string) // onabort - function/callback when request is aborted if (!url) { print('missing url'); return false; } if (!access_token) { print('missing access token'); return false; } if (!fileobj) { print('missing file object'); return false; } print('uploading media'); endpoint = urljoin(url, '/api/v1/media'); var query = new XMLHttpRequest(); query.addEventListener('error', function(ev) { if (onerror) onerror(400, 'Request failed'); }); query.addEventListener('abort', function(ev) { if (onabort) onabort(); }); query.addEventListener('load', function(ev) { print(query.status); print(query.responseText); if (query.status == 200) { if (onload) { // {"id":"NUMBER", "type":"image", "url":"PATH/FILE.EXT","preview_url":"PATH/FILE.EXT", "remote_url":null, "text_url":"PATH", // "meta":{ // "original":{"width":NUMBER, "height":NUMBER, "size":"NUMBERxNUMBER", "aspect":FLOAT}, // "small":{"width":NUMBER, "height":NUMBER,"size":"NUMBERxNUMBER","aspect":FLOAT} // }, // "description":null} let jsondata = JSON.parse(query.responseText); let media_id = jsondata.id; onload(url, access_token, media_id); } } else { if (onerror) { // {"status":NUMBER,"error":"STRING"} let jsondata = JSON.parse(query.responseText); onerror(jsondata.status, jsondata.error); } } }); query.open('POST', endpoint); query.setRequestHeader('Accept', 'application/json'); query.setRequestHeader('Authorization', 'Bearer ' + access_token); var data = new FormData(); data.append('file', fileobj); if (focalpoint) data.append('focus', focalpoint); query.send(data); } function postmedia(url, access_token, media_id, message, visibility, spoiler_text, sensitive, onload, onerror, onabort) { // onload - function/callback when media successfully uploaded. // no args // onerror - function/callback when error or status code other than 200 occurs. // should accept 2 args: code (int), description (string) // onabort - function/callback when request is aborted. // no args if (!url) { print('missing url'); return false; } if (!access_token) { print('missing access token'); return false; } if (!media_id) { print('missing media_id'); return false; } message = message || '.'; visibility = visibility || 'public'; var visibilities = ['public', 'unlisted', 'private', 'direct']; if (visibilities.indexOf(visibility) < 0) visibility = 'public'; if (sensitive != 'true') sensitive = 'false'; var endpoint = urljoin(url, '/api/v1/statuses'); var query = new XMLHttpRequest(); query.addEventListener('error', function(ev) { if (onerror) onerror(400, 'Request failed'); }); query.addEventListener('abort', function(ev) { if (onabort) onabort(); }); query.addEventListener('load', function(ev) { print(query.status); print(query.responseText); if (query.status == 200) { if (onload) { //var jsondata = JSON.parse(query.responseText); onload(); } } else { if (onerror) onerror(400, 'Request failed'); } }); query.open('POST', endpoint); query.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); query.setRequestHeader('Accept', 'application/json'); query.setRequestHeader('Authorization', 'Bearer ' + access_token); //query.setRequestHeader('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0'); var params = {'status':message, 'visibility':visibility, 'sensitive':sensitive}; if (spoiler_text) params['spoiler_text'] = spoiler_text; var data = urlencode(params); // arrays require a particular format: array[]=value1&array[]=value2... data += '&media_ids%5B%5D=' + media_id; query.send(data); } window.addEventListener("load", function(event) { var slider_style = document.createElement("style"); slider_style.textContent = ` #tweak_slider::-moz-range-track { height: 10px; border-radius: 5px; } #tweak_slider::-webkit-slider-runnable-track { height: 10px; border-radius: 5px; } #tweak_slider::-moz-range-thumb { width: 25px; height: 25px; border-radius: 25px; cursor: pointer; } #tweak_slider::-webkit-slider-thumb { width: 25px; height: 25px; border-radius: 25px; cursor: pointer; } @keyframes animkeys { 0% 80% 100% {transform: scale(1.0, 1.0);} 40% {transform: scale(0.0, 0.0)} } `; var head = document.querySelector('head'); head.appendChild(slider_style); insertChooseAFile(); });