├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── build ├── darkroom.css └── darkroom.js ├── demo ├── build ├── css │ └── page.css ├── images │ ├── batcat.png │ ├── doka-image-editor-gh.gif │ ├── domokun-big.jpg │ └── domokun.jpg ├── index.html └── vendor │ └── fabric.js ├── gh-pages.sh ├── gulpfile.js ├── lib ├── css │ ├── _layout.scss │ ├── _toolbar.scss │ └── darkroom.scss ├── icons │ ├── close.svg │ ├── crop.svg │ ├── done.svg │ ├── redo.svg │ ├── rotate-left.svg │ ├── rotate-right.svg │ ├── save.svg │ └── undo.svg └── js │ ├── core │ ├── bootstrap.js │ ├── darkroom.js │ ├── plugin.js │ ├── transformation.js │ ├── ui.js │ └── utils.js │ └── plugins │ ├── darkroom.crop.js │ ├── darkroom.history.js │ ├── darkroom.rotate.js │ └── darkroom.save.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = LF 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.sass-cache/ 2 | /bower_components/ 3 | /build/ 4 | /node_modules/ 5 | /docs/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## Unreleased 6 | 7 | - Add type "button" to avoid html5 submit validation (#24) 8 | 9 | ## 2.0.0 (2015-08-01) 10 | 11 | - Use of **Gulp** for the build process 12 | - Replace the webfont by **SVG symbols** (which are direclty included in the compiled javascript) 13 | - Ability to change **canvas ratio** 14 | - Original image is kept and changes are done on a clone 15 | 16 | ## 1.0.x (2014) 17 | 18 | Initial release. 19 | 20 | - Create canvas with FabricJS from an image element 21 | - Plugins: Crop, History, Rotate, Save 22 | - Build process via Grunt 23 | - Build webfont from SVG files to display the icons 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Matthieu Moquet 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DarkroomJS 2 | 3 | ![License MIT](http://img.shields.io/badge/license-MIT-blue.svg) 4 | 5 | DarkroomJS is a JavaScript library which provides basic image editing tools in 6 | your browser, such as **rotation** or **cropping**. It is based on the awesome 7 | [FabricJS](http://fabricjs.com/) library to handle images in HTML5 canvas. 8 | 9 | 10 | ## ⚠️ IMPORTANT UPDATE 11 | 12 | This library has been discontinued and is **no longer maintained**. 13 | 14 | If you're looking for an alternative, you should have a look at **[Pintura Image Editor](https://www.ktm.sh/pintura)**. 15 | 16 | - framework agnostic 17 | - intuitive UI and mobile touch friendly 18 | - resizing / free rotating 19 | - color adjustment / photo filters 20 | - annotating support 21 | - and much more, [try the online demo](https://www.ktm.sh/pintura): 22 | 23 | [![Pintura Image Editor demo](demo/images/doka-image-editor-gh.gif?raw=true "Pintura Image Editor (click on the image to view)")](https://www.ktm.sh/pintura) 24 | 25 | [**[Demo] Try Pintura Image Editor →**](https://www.ktm.sh/pintura) 26 | 27 | ## Building 28 | 29 | - Install [Node](http://nodejs.org/) & `npm`. 30 | - Run `npm install` to build dependencies. 31 | - Run `npm start` to build the assets and start the demo webserver. 32 | 33 | ## Usage 34 | 35 | Simply instanciate a new Darkroom object with a reference to the image element: 36 | 37 | ```html 38 | 39 | 42 | ``` 43 | 44 | You can also pass some options: 45 | 46 | ```javascript 47 | new Darkroom('#target', { 48 | // Canvas initialization size 49 | minWidth: 100, 50 | minHeight: 100, 51 | maxWidth: 500, 52 | maxHeight: 500, 53 | 54 | // Plugins options 55 | plugins: { 56 | crop: { 57 | minHeight: 50, 58 | minWidth: 50, 59 | ratio: 1 60 | }, 61 | save: false // disable plugin 62 | }, 63 | 64 | // Post initialization method 65 | initialize: function() { 66 | // Active crop selection 67 | this.plugins['crop'].requireFocus(); 68 | 69 | // Add custom listener 70 | this.addEventListener('core:transformation', function() { /* ... */ }); 71 | } 72 | }); 73 | ``` 74 | 75 | ## Why? 76 | 77 | It's easy to get a javascript script to crop an image in a web page. 78 | But if your want more features like rotation or brightness adjustment, then you 79 | will have to do it yourself. No more jQuery plugins here. 80 | It only uses the power of HTML5 canvas to make what ever you want with your image. 81 | 82 | ## The concept 83 | 84 | The library is designed to be easily extendable. The core script only transforms 85 | the target image to a canvas with a FabricJS instance, and creates an empty toolbar. 86 | All the features are then implemented in separate plugins. 87 | 88 | Each plugin is responsible for creating its own functionality. 89 | Buttons can easily be added to the toolbar and binded with those features. 90 | 91 | ## Contributing 92 | 93 | Run `npm develop` to build and watch the files while developing. 94 | 95 | ## FAQ 96 | 97 | How can I access the edited image? 98 | 99 | In order to get the edited image data, you must ask the canvas for it. By doing so inside the callback of your choice (in this case save), you can assign the edited image data to wherever you please. 100 | 101 | ```javascript 102 | save: { 103 | callback: function() { 104 | this.darkroom.selfDestroy(); // Cleanup 105 | var newImage = dkrm.canvas.toDataURL(); 106 | fileStorageLocation = newImage; 107 | } 108 | } 109 | ``` 110 | 111 | ## License 112 | 113 | DarkroomJS is released under the MIT License. See the [bundled LICENSE file](LICENSE) 114 | for details. 115 | 116 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "darkroom", 3 | "description": "Extensible image editing tool via HTML canvas", 4 | "version": "2.0.0", 5 | "homepage": "http://mattketmo.github.io/darkroomjs/", 6 | "authors": [ 7 | "Matthieu Moquet " 8 | ], 9 | "license": "MIT", 10 | "dependencies": { 11 | "fabric": "~1.4.*" 12 | }, 13 | "main": [ 14 | "build/darkroom.css", 15 | "build/darkroom.js" 16 | ], 17 | "moduleType": [ 18 | "globals" 19 | ], 20 | "keywords": [ 21 | "image", 22 | "canvas", 23 | "crop" 24 | ], 25 | "ignore": [ 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /build/darkroom.css: -------------------------------------------------------------------------------- 1 | .darkroom-container{position:relative}.darkroom-image-container{top:0;left:0}.darkroom-toolbar{display:block;position:absolute;top:-45px;left:0;background:#444;height:40px;min-width:40px;z-index:99;border-radius:2px;white-space:nowrap;padding:0 5px}.darkroom-toolbar:before{content:"";position:absolute;bottom:-7px;left:20px;width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid #444}.darkroom-button-group{display:inline-block;margin:0;padding:0}.darkroom-button-group:last-child{border-right:none}.darkroom-button{box-sizing:border-box;background:transparent;border:none;outline:none;padding:2px 0 0 0;width:40px;height:40px}.darkroom-button:hover{cursor:pointer;background:#555}.darkroom-button:active{cursor:pointer;background:#333}.darkroom-button:disabled .darkroom-icon{fill:#666}.darkroom-button:disabled:hover{cursor:default;background:transparent}.darkroom-button.darkroom-button-active .darkroom-icon{fill:#33b5e5}.darkroom-button.darkroom-button-hidden{display:none}.darkroom-button.darkroom-button-success .darkroom-icon{fill:#99cc00}.darkroom-button.darkroom-button-warning .darkroom-icon{fill:#FFBB33}.darkroom-button.darkroom-button-danger .darkroom-icon{fill:#FF4444}.darkroom-icon{width:24px;height:24px;fill:#fff} 2 | -------------------------------------------------------------------------------- /build/darkroom.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var element=document.createElement("div");element.id="darkroom-icons",element.style.height=0,element.style.width=0,element.style.position="absolute",element.style.visibility="hidden",element.innerHTML='',document.body.appendChild(element)}(),function(){"use strict";function Darkroom(element,options,plugins){return this.constructor(element,options,plugins)}window.Darkroom=Darkroom,Darkroom.plugins=[],Darkroom.prototype={containerElement:null,canvas:null,image:null,sourceCanvas:null,sourceImage:null,originalImageElement:null,transformations:[],defaults:{minWidth:null,minHeight:null,maxWidth:null,maxHeight:null,ratio:null,backgroundColor:"#fff",plugins:{},initialize:function(){}},plugins:{},options:{},constructor:function(element,options,plugins){if(this.options=Darkroom.Utils.extend(options,this.defaults),"string"==typeof element&&(element=document.querySelector(element)),null!==element){var image=new Image;image.onload=function(){this._initializeDOM(element),this._initializeImage(),this._initializePlugins(Darkroom.plugins),this.refresh(function(){this.options.initialize.bind(this).call()}.bind(this))}.bind(this),image.src=element.src}},selfDestroy:function(){var container=this.containerElement,image=new Image;image.onload=function(){container.parentNode.replaceChild(image,container)},image.src=this.sourceImage.toDataURL()},addEventListener:function(eventName,callback){var el=this.canvas.getElement();el.addEventListener?el.addEventListener(eventName,callback):el.attachEvent&&el.attachEvent("on"+eventName,callback)},dispatchEvent:function(eventName){var event=document.createEvent("Event");event.initEvent(eventName,!0,!0),this.canvas.getElement().dispatchEvent(event)},refresh:function(next){var clone=new Image;clone.onload=function(){this._replaceCurrentImage(new fabric.Image(clone)),next&&next()}.bind(this),clone.src=this.sourceImage.toDataURL()},_replaceCurrentImage:function(newImage){this.image&&this.image.remove(),this.image=newImage,this.image.selectable=!1;var viewport=Darkroom.Utils.computeImageViewPort(this.image),canvasWidth=viewport.width,canvasHeight=viewport.height;if(null!==this.options.ratio){var canvasRatio=+this.options.ratio,currentRatio=canvasWidth/canvasHeight;currentRatio>canvasRatio?canvasHeight=canvasWidth/canvasRatio:canvasRatio>currentRatio&&(canvasWidth=canvasHeight*canvasRatio)}var scaleMin=1,scaleMax=1,scaleX=1,scaleY=1;null!==this.options.maxWidth&&this.options.maxWidthcanvasWidth&&(scaleX=this.options.minWidth/canvasWidth),null!==this.options.minHeight&&this.options.minHeight>canvasHeight&&(scaleY=this.options.minHeight/canvasHeight),scaleMax=Math.max(scaleX,scaleY);var scale=scaleMax*scaleMin;canvasWidth*=scale,canvasHeight*=scale,this.image.setScaleX(1*scale),this.image.setScaleY(1*scale),this.canvas.add(this.image),this.canvas.setWidth(canvasWidth),this.canvas.setHeight(canvasHeight),this.canvas.centerObject(this.image),this.image.setCoords()},applyTransformation:function(transformation){this.transformations.push(transformation),transformation.applyTransformation(this.sourceCanvas,this.sourceImage,this._postTransformation.bind(this))},_postTransformation:function(newImage){newImage&&(this.sourceImage=newImage),this.refresh(function(){this.dispatchEvent("core:transformation")}.bind(this))},reinitializeImage:function(){this.sourceImage.remove(),this._initializeImage(),this._popTransformation(this.transformations.slice())},_popTransformation:function(transformations){if(0===transformations.length)return this.dispatchEvent("core:reinitialized"),void this.refresh();var transformation=transformations.shift(),next=function(newImage){newImage&&(this.sourceImage=newImage),this._popTransformation(transformations)};transformation.applyTransformation(this.sourceCanvas,this.sourceImage,next.bind(this))},_initializeDOM:function(imageElement){var mainContainerElement=document.createElement("div");mainContainerElement.className="darkroom-container";var toolbarElement=document.createElement("div");toolbarElement.className="darkroom-toolbar",mainContainerElement.appendChild(toolbarElement);var canvasContainerElement=document.createElement("div");canvasContainerElement.className="darkroom-image-container";var canvasElement=document.createElement("canvas");canvasContainerElement.appendChild(canvasElement),mainContainerElement.appendChild(canvasContainerElement);var sourceCanvasContainerElement=document.createElement("div");sourceCanvasContainerElement.className="darkroom-source-container",sourceCanvasContainerElement.style.display="none";var sourceCanvasElement=document.createElement("canvas");sourceCanvasContainerElement.appendChild(sourceCanvasElement),mainContainerElement.appendChild(sourceCanvasContainerElement),imageElement.parentNode.replaceChild(mainContainerElement,imageElement),imageElement.style.display="none",mainContainerElement.appendChild(imageElement),this.containerElement=mainContainerElement,this.originalImageElement=imageElement,this.toolbar=new Darkroom.UI.Toolbar(toolbarElement),this.canvas=new fabric.Canvas(canvasElement,{selection:!1,backgroundColor:this.options.backgroundColor}),this.sourceCanvas=new fabric.Canvas(sourceCanvasElement,{selection:!1,backgroundColor:this.options.backgroundColor})},_initializeImage:function(){this.sourceImage=new fabric.Image(this.originalImageElement,{selectable:!1,evented:!1,lockMovementX:!0,lockMovementY:!0,lockRotation:!0,lockScalingX:!0,lockScalingY:!0,lockUniScaling:!0,hasControls:!1,hasBorders:!1}),this.sourceCanvas.add(this.sourceImage);var viewport=Darkroom.Utils.computeImageViewPort(this.sourceImage),canvasWidth=viewport.width,canvasHeight=viewport.height;this.sourceCanvas.setWidth(canvasWidth),this.sourceCanvas.setHeight(canvasHeight),this.sourceCanvas.centerObject(this.sourceImage),this.sourceImage.setCoords()},_initializePlugins:function(plugins){for(var name in plugins){var plugin=plugins[name],options=this.options.plugins[name];options!==!1&&plugins.hasOwnProperty(name)&&(this.plugins[name]=new plugin(this,options))}}}}(),function(){"use strict";function Plugin(darkroom,options){this.darkroom=darkroom,this.options=Darkroom.Utils.extend(options,this.defaults),this.initialize()}Darkroom.Plugin=Plugin,Plugin.prototype={defaults:{},initialize:function(){}},Plugin.extend=function(protoProps){var child,parent=this;child=protoProps&&protoProps.hasOwnProperty("constructor")?protoProps.constructor:function(){return parent.apply(this,arguments)},Darkroom.Utils.extend(child,parent);var Surrogate=function(){this.constructor=child};return Surrogate.prototype=parent.prototype,child.prototype=new Surrogate,protoProps&&Darkroom.Utils.extend(child.prototype,protoProps),child.__super__=parent.prototype,child}}(),function(){"use strict";function Transformation(options){this.options=options}Darkroom.Transformation=Transformation,Transformation.prototype={applyTransformation:function(image){}},Transformation.extend=function(protoProps){var child,parent=this;child=protoProps&&protoProps.hasOwnProperty("constructor")?protoProps.constructor:function(){return parent.apply(this,arguments)},Darkroom.Utils.extend(child,parent);var Surrogate=function(){this.constructor=child};return Surrogate.prototype=parent.prototype,child.prototype=new Surrogate,protoProps&&Darkroom.Utils.extend(child.prototype,protoProps),child.__super__=parent.prototype,child}}(),function(){"use strict";function Toolbar(element){this.element=element}function ButtonGroup(element){this.element=element}function Button(element){this.element=element}Darkroom.UI={Toolbar:Toolbar,ButtonGroup:ButtonGroup,Button:Button},Toolbar.prototype={createButtonGroup:function(options){var buttonGroup=document.createElement("div");return buttonGroup.className="darkroom-button-group",this.element.appendChild(buttonGroup),new ButtonGroup(buttonGroup)}},ButtonGroup.prototype={createButton:function(options){var defaults={image:"help",type:"default",group:"default",hide:!1,disabled:!1};options=Darkroom.Utils.extend(options,defaults);var buttonElement=document.createElement("button");buttonElement.type="button",buttonElement.className="darkroom-button darkroom-button-"+options.type,buttonElement.innerHTML='',this.element.appendChild(buttonElement);var button=new Button(buttonElement);return button.hide(options.hide),button.disable(options.disabled),button}},Button.prototype={addEventListener:function(eventName,listener){this.element.addEventListener?this.element.addEventListener(eventName,listener):this.element.attachEvent&&this.element.attachEvent("on"+eventName,listener)},removeEventListener:function(eventName,listener){this.element.removeEventListener&&this.element.removeEventListener(eventName,listener)},active:function(value){value?this.element.classList.add("darkroom-button-active"):this.element.classList.remove("darkroom-button-active")},hide:function(value){value?this.element.classList.add("darkroom-button-hidden"):this.element.classList.remove("darkroom-button-hidden")},disable:function(value){this.element.disabled=value?!0:!1}}}(),function(){"use strict";function extend(b,a){var prop;if(void 0===b)return a;for(prop in a)a.hasOwnProperty(prop)&&b.hasOwnProperty(prop)===!1&&(b[prop]=a[prop]);return b}function computeImageViewPort(image){return{height:Math.abs(image.getWidth()*Math.sin(image.getAngle()*Math.PI/180))+Math.abs(image.getHeight()*Math.cos(image.getAngle()*Math.PI/180)),width:Math.abs(image.getHeight()*Math.sin(image.getAngle()*Math.PI/180))+Math.abs(image.getWidth()*Math.cos(image.getAngle()*Math.PI/180))}}Darkroom.Utils={extend:extend,computeImageViewPort:computeImageViewPort}}(),function(window,document,Darkroom,fabric){"use strict";Darkroom.plugins.history=Darkroom.Plugin.extend({undoTransformations:[],initialize:function(){this._initButtons(),this.darkroom.addEventListener("core:transformation",this._onTranformationApplied.bind(this))},undo:function(){if(0!==this.darkroom.transformations.length){var lastTransformation=this.darkroom.transformations.pop();this.undoTransformations.unshift(lastTransformation),this.darkroom.reinitializeImage(),this._updateButtons()}},redo:function(){if(0!==this.undoTransformations.length){var cancelTransformation=this.undoTransformations.shift();this.darkroom.transformations.push(cancelTransformation),this.darkroom.reinitializeImage(),this._updateButtons()}},_initButtons:function(){var buttonGroup=this.darkroom.toolbar.createButtonGroup();return this.backButton=buttonGroup.createButton({image:"undo",disabled:!0}),this.forwardButton=buttonGroup.createButton({image:"redo",disabled:!0}),this.backButton.addEventListener("click",this.undo.bind(this)),this.forwardButton.addEventListener("click",this.redo.bind(this)),this},_updateButtons:function(){this.backButton.disable(0===this.darkroom.transformations.length),this.forwardButton.disable(0===this.undoTransformations.length)},_onTranformationApplied:function(){this.undoTransformations=[],this._updateButtons()}})}(window,document,Darkroom,fabric),function(){"use strict";var Rotation=Darkroom.Transformation.extend({applyTransformation:function(canvas,image,next){var angle=(image.getAngle()+this.options.angle)%360;image.rotate(angle);var width,height;height=Math.abs(image.getWidth()*Math.sin(angle*Math.PI/180))+Math.abs(image.getHeight()*Math.cos(angle*Math.PI/180)),width=Math.abs(image.getHeight()*Math.sin(angle*Math.PI/180))+Math.abs(image.getWidth()*Math.cos(angle*Math.PI/180)),canvas.setWidth(width),canvas.setHeight(height),canvas.centerObject(image),image.setCoords(),canvas.renderAll(),next()}});Darkroom.plugins.rotate=Darkroom.Plugin.extend({initialize:function(){var buttonGroup=this.darkroom.toolbar.createButtonGroup(),leftButton=buttonGroup.createButton({image:"rotate-left"}),rightButton=buttonGroup.createButton({image:"rotate-right"});leftButton.addEventListener("click",this.rotateLeft.bind(this)),rightButton.addEventListener("click",this.rotateRight.bind(this))},rotateLeft:function(){this.rotate(-90)},rotateRight:function(){this.rotate(90)},rotate:function(angle){this.darkroom.applyTransformation(new Rotation({angle:angle}))}})}(),function(){"use strict";var Crop=Darkroom.Transformation.extend({applyTransformation:function(canvas,image,next){var snapshot=new Image;snapshot.onload=function(){if(!(1>height||1>width)){var imgInstance=new fabric.Image(this,{selectable:!1,evented:!1,lockMovementX:!0,lockMovementY:!0,lockRotation:!0,lockScalingX:!0,lockScalingY:!0,lockUniScaling:!0,hasControls:!1,hasBorders:!1}),width=this.width,height=this.height;canvas.setWidth(width),canvas.setHeight(height),image.remove(),canvas.add(imgInstance),next(imgInstance)}};var viewport=Darkroom.Utils.computeImageViewPort(image),imageWidth=viewport.width,imageHeight=viewport.height,left=this.options.left*imageWidth,top=this.options.top*imageHeight,width=Math.min(this.options.width*imageWidth,imageWidth-left),height=Math.min(this.options.height*imageHeight,imageHeight-top);snapshot.src=canvas.toDataURL({left:left,top:top,width:width,height:height})}}),CropZone=fabric.util.createClass(fabric.Rect,{_render:function(ctx){this.callSuper("_render",ctx);var dashWidth=(ctx.canvas,7),flipX=this.flipX?-1:1,flipY=this.flipY?-1:1,scaleX=flipX/this.scaleX,scaleY=flipY/this.scaleY;ctx.scale(scaleX,scaleY),ctx.fillStyle="rgba(0, 0, 0, 0.5)",this._renderOverlay(ctx),void 0!==ctx.setLineDash?ctx.setLineDash([dashWidth,dashWidth]):void 0!==ctx.mozDash&&(ctx.mozDash=[dashWidth,dashWidth]),ctx.strokeStyle="rgba(0, 0, 0, 0.2)",this._renderBorders(ctx),this._renderGrid(ctx),ctx.lineDashOffset=dashWidth,ctx.strokeStyle="rgba(255, 255, 255, 0.4)",this._renderBorders(ctx),this._renderGrid(ctx),ctx.scale(1/scaleX,1/scaleY)},_renderOverlay:function(ctx){var canvas=ctx.canvas,borderOffset=0,x0=Math.ceil(-this.getWidth()/2-this.getLeft()),x1=Math.ceil(-this.getWidth()/2),x2=Math.ceil(this.getWidth()/2),x3=Math.ceil(this.getWidth()/2+(canvas.width-this.getWidth()-this.getLeft())),y0=Math.ceil(-this.getHeight()/2-this.getTop()),y1=Math.ceil(-this.getHeight()/2),y2=Math.ceil(this.getHeight()/2),y3=Math.ceil(this.getHeight()/2+(canvas.height-this.getHeight()-this.getTop()));ctx.fillRect(x0,y0,x3-x0,y1-y0+borderOffset),ctx.fillRect(x0,y1,x1-x0,y2-y1+borderOffset),ctx.fillRect(x2,y1,x3-x2,y2-y1+borderOffset),ctx.fillRect(x0,y2,x3-x0,y3-y2)},_renderBorders:function(ctx){ctx.beginPath(),ctx.moveTo(-this.getWidth()/2,-this.getHeight()/2),ctx.lineTo(this.getWidth()/2,-this.getHeight()/2),ctx.lineTo(this.getWidth()/2,this.getHeight()/2),ctx.lineTo(-this.getWidth()/2,this.getHeight()/2),ctx.lineTo(-this.getWidth()/2,-this.getHeight()/2),ctx.stroke()},_renderGrid:function(ctx){ctx.beginPath(),ctx.moveTo(-this.getWidth()/2+1/3*this.getWidth(),-this.getHeight()/2),ctx.lineTo(-this.getWidth()/2+1/3*this.getWidth(),this.getHeight()/2),ctx.stroke(),ctx.beginPath(),ctx.moveTo(-this.getWidth()/2+2/3*this.getWidth(),-this.getHeight()/2),ctx.lineTo(-this.getWidth()/2+2/3*this.getWidth(),this.getHeight()/2),ctx.stroke(),ctx.beginPath(),ctx.moveTo(-this.getWidth()/2,-this.getHeight()/2+1/3*this.getHeight()),ctx.lineTo(this.getWidth()/2,-this.getHeight()/2+1/3*this.getHeight()),ctx.stroke(),ctx.beginPath(),ctx.moveTo(-this.getWidth()/2,-this.getHeight()/2+2/3*this.getHeight()),ctx.lineTo(this.getWidth()/2,-this.getHeight()/2+2/3*this.getHeight()),ctx.stroke()}});Darkroom.plugins.crop=Darkroom.Plugin.extend({startX:null,startY:null,isKeyCroping:!1,isKeyLeft:!1,isKeyUp:!1,defaults:{minHeight:1,minWidth:1,ratio:null,quickCropKey:!1},initialize:function(){var buttonGroup=this.darkroom.toolbar.createButtonGroup();this.cropButton=buttonGroup.createButton({image:"crop"}),this.okButton=buttonGroup.createButton({image:"done",type:"success",hide:!0}),this.cancelButton=buttonGroup.createButton({image:"close",type:"danger",hide:!0}),this.cropButton.addEventListener("click",this.toggleCrop.bind(this)),this.okButton.addEventListener("click",this.cropCurrentZone.bind(this)),this.cancelButton.addEventListener("click",this.releaseFocus.bind(this)),this.darkroom.canvas.on("mouse:down",this.onMouseDown.bind(this)),this.darkroom.canvas.on("mouse:move",this.onMouseMove.bind(this)),this.darkroom.canvas.on("mouse:up",this.onMouseUp.bind(this)),this.darkroom.canvas.on("object:moving",this.onObjectMoving.bind(this)),this.darkroom.canvas.on("object:scaling",this.onObjectScaling.bind(this)),fabric.util.addListener(fabric.document,"keydown",this.onKeyDown.bind(this)),fabric.util.addListener(fabric.document,"keyup",this.onKeyUp.bind(this)),this.darkroom.addEventListener("core:transformation",this.releaseFocus.bind(this))},onObjectMoving:function(event){if(this.hasFocus()){var currentObject=event.target;if(currentObject===this.cropZone){var canvas=this.darkroom.canvas,x=currentObject.getLeft(),y=currentObject.getTop(),w=currentObject.getWidth(),h=currentObject.getHeight(),maxX=canvas.getWidth()-w,maxY=canvas.getHeight()-h;0>x&¤tObject.set("left",0),0>y&¤tObject.set("top",0),x>maxX&¤tObject.set("left",maxX),y>maxY&¤tObject.set("top",maxY),this.darkroom.dispatchEvent("crop:update")}}},onObjectScaling:function(event){if(this.hasFocus()){var preventScaling=!1,currentObject=event.target;if(currentObject===this.cropZone){var canvas=this.darkroom.canvas,pointer=canvas.getPointer(event.e),minX=(pointer.x,pointer.y,currentObject.getLeft()),minY=currentObject.getTop(),maxX=currentObject.getLeft()+currentObject.getWidth(),maxY=currentObject.getTop()+currentObject.getHeight();if(null!==this.options.ratio&&(0>minX||maxX>canvas.getWidth()||0>minY||maxY>canvas.getHeight())&&(preventScaling=!0),0>minX||maxX>canvas.getWidth()||preventScaling){var lastScaleX=this.lastScaleX||1;currentObject.setScaleX(lastScaleX)}if(0>minX&¤tObject.setLeft(0),0>minY||maxY>canvas.getHeight()||preventScaling){var lastScaleY=this.lastScaleY||1;currentObject.setScaleY(lastScaleY)}0>minY&¤tObject.setTop(0),currentObject.getWidth()top&&(height+=top,top=0),0>left&&(width+=left,left=0),this.darkroom.applyTransformation(new Crop({top:top/image.getHeight(),left:left/image.getWidth(),width:width/image.getWidth(),height:height/image.getHeight()}))}},hasFocus:function(){return void 0!==this.cropZone},requireFocus:function(){this.cropZone=new CropZone({fill:"transparent",hasBorders:!1,originX:"left",originY:"top",cornerColor:"#444",cornerSize:8,transparentCorners:!1,lockRotation:!0,hasRotatingPoint:!1}),null!==this.options.ratio&&this.cropZone.set("lockUniScaling",!0),this.darkroom.canvas.add(this.cropZone),this.darkroom.canvas.defaultCursor="crosshair",this.cropButton.active(!0),this.okButton.hide(!1),this.cancelButton.hide(!1)},releaseFocus:function(){void 0!==this.cropZone&&(this.cropZone.remove(),this.cropZone=void 0,this.cropButton.active(!1),this.okButton.hide(!0),this.cancelButton.hide(!0),this.darkroom.canvas.defaultCursor="default",this.darkroom.dispatchEvent("crop:update"))},_renderCropZone:function(fromX,fromY,toX,toY){var canvas=this.darkroom.canvas,isRight=toX>fromX,isLeft=!isRight,isDown=toY>fromY,isUp=!isDown,minWidth=Math.min(+this.options.minWidth,canvas.getWidth()),minHeight=Math.min(+this.options.minHeight,canvas.getHeight()),leftX=Math.min(fromX,toX),rightX=Math.max(fromX,toX),topY=Math.min(fromY,toY),bottomY=Math.max(fromY,toY);leftX=Math.max(0,leftX),rightX=Math.min(canvas.getWidth(),rightX),topY=Math.max(0,topY),bottomY=Math.min(canvas.getHeight(),bottomY),minWidth>rightX-leftX&&(isRight?rightX=leftX+minWidth:leftX=rightX-minWidth),minHeight>bottomY-topY&&(isDown?bottomY=topY+minHeight:topY=bottomY-minHeight),0>leftX&&(rightX+=Math.abs(leftX),leftX=0),rightX>canvas.getWidth()&&(leftX-=rightX-canvas.getWidth(),rightX=canvas.getWidth()),0>topY&&(bottomY+=Math.abs(topY),topY=0),bottomY>canvas.getHeight()&&(topY-=bottomY-canvas.getHeight(),bottomY=canvas.getHeight());var width=rightX-leftX,height=bottomY-topY,currentRatio=width/height;if(this.options.ratio&&+this.options.ratio!==currentRatio){var ratio=+this.options.ratio;if(this.isKeyCroping&&(isLeft=this.isKeyLeft,isUp=this.isKeyUp),ratio>currentRatio){var newWidth=height*ratio;isLeft&&(leftX-=newWidth-width),width=newWidth}else if(currentRatio>ratio){var newHeight=height/(ratio*height/width);isUp&&(topY-=newHeight-height),height=newHeight}if(0>leftX&&(leftX=0),0>topY&&(topY=0),leftX+width>canvas.getWidth()){var newWidth=canvas.getWidth()-leftX;height=newWidth*height/width,width=newWidth,isUp&&(topY=fromY-height)}if(topY+height>canvas.getHeight()){var newHeight=canvas.getHeight()-topY;width=width*newHeight/height,height=newHeight,isLeft&&(leftX=fromX-width)}}this.cropZone.left=leftX,this.cropZone.top=topY,this.cropZone.width=width,this.cropZone.height=height,this.darkroom.canvas.bringToFront(this.cropZone),this.darkroom.dispatchEvent("crop:update")}})}(),function(){"use strict";Darkroom.plugins.save=Darkroom.Plugin.extend({defaults:{callback:function(){this.darkroom.selfDestroy()}},initialize:function(){var buttonGroup=this.darkroom.toolbar.createButtonGroup();this.destroyButton=buttonGroup.createButton({image:"save"}),this.destroyButton.addEventListener("click",this.options.callback.bind(this))}})}(); -------------------------------------------------------------------------------- /demo/build: -------------------------------------------------------------------------------- 1 | ../build -------------------------------------------------------------------------------- /demo/css/page.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | background-color: #f2f2f4; 4 | color: #555; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | a { 10 | text-decoration: none; 11 | color: #0099CC; 12 | } 13 | 14 | figure { 15 | margin: 0; 16 | } 17 | 18 | .sr-only { 19 | position: absolute; 20 | width: 1px; 21 | height: 1px; 22 | padding: 0; 23 | margin: -1px; 24 | overflow: hidden; 25 | clip: rect(0,0,0,0); 26 | border: 0 27 | } 28 | 29 | .copy { 30 | font-size: 62.5%; 31 | } 32 | 33 | .copy p, .copy pre, .copy ul, .copy dl { 34 | font-size: 1.6em; 35 | line-height: 1.5; 36 | } 37 | 38 | .copy p.intro { 39 | font-size: 1.8em; 40 | line-height: 1.5; 41 | font-weight: 300; 42 | } 43 | 44 | .copy h2 { 45 | font-size: 2.5em; 46 | font-weight: 300; 47 | } 48 | 49 | .copy pre { 50 | font-family: monospace; 51 | background: #fafafa; 52 | padding: 10px 15px; 53 | border: 1px solid #e2e2e2; 54 | border-bottom: 1px solid #ccc; 55 | border-right: 1px solid #ccc; 56 | } 57 | 58 | .copy dt { 59 | font-weight: bold; 60 | } 61 | 62 | .container { 63 | width: 700px; 64 | margin: auto; 65 | } 66 | 67 | #header { 68 | background: #222; 69 | padding: 30px; 70 | min-width: 700px; 71 | position: relative; 72 | } 73 | 74 | #content { 75 | margin: 20px 0 40px; 76 | } 77 | 78 | h1 { 79 | font-size: 52px; 80 | margin: 0; 81 | padding: 0; 82 | font-weight: 100; 83 | color: #eee; 84 | } 85 | 86 | .hero { 87 | font-size: 22px; 88 | font-weight: 100; 89 | color: #ccc; 90 | margin: 0; 91 | padding: 0; 92 | } 93 | 94 | .figure-wrapper { 95 | /*text-align: center;*/ 96 | } 97 | 98 | .image-container { 99 | display: inline-block; 100 | max-width: 100%; 101 | background: white; 102 | padding: 10px; 103 | margin: 5px 0; 104 | border: 1px solid #e2e2e2; 105 | border-bottom: 1px solid #ccc; 106 | border-right: 1px solid #ccc; 107 | } 108 | 109 | .image-container.target { 110 | margin-top: 40px; 111 | } 112 | 113 | .image-container img { 114 | max-width: 100%; 115 | } 116 | 117 | .image-container .image-meta { 118 | margin-top: 10px; 119 | font-size: 12px; 120 | text-align: left; 121 | } 122 | .image-container .image-meta a { 123 | text-decoration: none; 124 | color: #444; 125 | } 126 | .image-container .image-meta a:hover .image-meta-title { 127 | text-decoration: underline; 128 | } 129 | -------------------------------------------------------------------------------- /demo/images/batcat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattKetmo/darkroomjs/d124ce092e06be6862769776325ee6a434b32500/demo/images/batcat.png -------------------------------------------------------------------------------- /demo/images/doka-image-editor-gh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattKetmo/darkroomjs/d124ce092e06be6862769776325ee6a434b32500/demo/images/doka-image-editor-gh.gif -------------------------------------------------------------------------------- /demo/images/domokun-big.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattKetmo/darkroomjs/d124ce092e06be6862769776325ee6a434b32500/demo/images/domokun-big.jpg -------------------------------------------------------------------------------- /demo/images/domokun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattKetmo/darkroomjs/d124ce092e06be6862769776325ee6a434b32500/demo/images/domokun.jpg -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DarkroomJS 6 | 7 | 8 | 9 | 10 | 24 | 25 |
26 |
27 |
28 |

Introduction

29 | 30 |

31 | DarkroomJS is a JavaScript library which provides basic 32 | image editing tools in your browser, such as rotation or cropping. 33 | It is based on the awesome FabricJS library 34 | to handle images in HTML5 canvas. 35 |

36 | 37 |
38 |
39 | DomoKun 40 | 41 |
42 | 43 | © 44 | DomoKun 45 | by 46 | Ben Torode 47 | 48 |
49 |
50 |
51 | 52 |

Why?

53 | 54 |

55 | It's easy to get a 56 | javascript script to crop an image in a web page. But if you want 57 | more features like rotation or brightness adjustment, then you will have to do all of this yourself. 58 | No more jQuery plugins here. DarkroomJS allows you to do whatever you want with your images by 59 | using the power of the HTML5 canvas. 60 |

61 | 62 |

The concept

63 | 64 |

65 | The library is designed to be easily extendable. 66 | The core script only transforms the target image to a canvas with a 67 | FabricJS instance, and creates an empty toolbar. 68 | All the features are then implemented in separate plugins. 69 |

70 | 71 |
72 |
.
 73 | ├── darkroom.js
 74 | └── plugins
 75 |     ├── darkroom.crop.js
 76 |     ├── darkroom.history.js
 77 |     ├── darkroom.rotate.js
 78 |     └── darkroom.save.js
79 |
80 | 81 |

82 | Each plugin is responsible for creating its own functionality. 83 | Buttons can easily be added to the toolbar and binded with those features. 84 |

85 | 86 |

Features

87 | 88 |

89 | Currently, the implemented features are: 90 |

91 | 92 |
93 |
Crop
94 |
95 | Crops the image by selecting a zone with your mouse. 96 | This supports several options like ratio and dimensions control (min/max). 97 |
98 | 99 |
Rotation
100 |
101 | Adds two buttons to let you rotate the image left or right. 102 |
103 | 104 |
History
105 |
106 | Allows you to undo and redo any changes you've made to the image. 107 |
108 | 109 |
Save
110 |
111 | Transforms the canvas back into an image. 112 | This is mainly a proof of concept to show how plugins work. 113 | This plugin only takes a few lines. 114 |
115 |
116 | 117 | 121 | 122 |

Contributing

123 | 124 |

125 | The project is released under the MIT license. 126 | Feel free to fork the project on GitHub 127 | or report issues. All ideas are also welcome. 128 |

129 | 130 |
131 |
132 |
133 | 134 | 135 | 136 | 137 | 166 | 167 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Update gh-pages branch 4 | git branch -D gh-pages 5 | git checkout -b gh-pages HEAD 6 | 7 | # Build assets 8 | rm -rf build 9 | gulp build --prod 10 | 11 | # Put build into demo folder 12 | rm demo/build 13 | cp -r build demo/build 14 | 15 | # Commit 16 | git add -f demo 17 | git commit -m "Build GH pages" 18 | 19 | # Push & reset 20 | git push origin `git subtree split --prefix demo HEAD`:gh-pages --force 21 | git checkout - 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var concat = require('gulp-concat') 2 | var connect = require('gulp-connect') 3 | var gulp = require('gulp') 4 | var gutil = require('gulp-util') 5 | var inject = require('gulp-inject') 6 | var plumber = require('gulp-plumber') 7 | var rimraf = require('rimraf') 8 | var sass = require('gulp-sass') 9 | var sourcemaps = require('gulp-sourcemaps') 10 | var svgmin = require('gulp-svgmin') 11 | var svgstore = require('gulp-svgstore') 12 | var uglify = require('gulp-uglify') 13 | 14 | 15 | // 16 | // Variables 17 | // 18 | var srcDir = './lib'; 19 | var distDir = './build'; 20 | var isDebug = !gutil.env.prod; 21 | 22 | // 23 | // Default 24 | // 25 | gulp.task('default', ['build'], function() { 26 | gulp.start('watch'); 27 | }); 28 | 29 | // 30 | // Clean 31 | // 32 | gulp.task('clean', function(cb) { 33 | rimraf(distDir, cb); 34 | }); 35 | 36 | // 37 | // Build 38 | // 39 | gulp.task('build', ['clean'], function() { 40 | gulp.start('scripts', 'styles'); 41 | }); 42 | 43 | // 44 | // Watch 45 | // 46 | gulp.task('watch', ['server'], function() { 47 | gulp.watch(srcDir + '/js/**/*.js', ['scripts']); 48 | 49 | gulp.watch(srcDir + '/css/**/*.scss', ['styles']); 50 | }); 51 | 52 | // 53 | // Server 54 | // 55 | gulp.task('server', function() { 56 | connect.server({ 57 | root: './demo', 58 | port: 2222, 59 | livereload: false 60 | }); 61 | }); 62 | 63 | // 64 | // Javascript 65 | // 66 | gulp.task('scripts', function () { 67 | var svgs = gulp.src(srcDir + '/icons/*.svg') 68 | .pipe(svgmin()) 69 | .pipe(svgstore({inlineSvg: true})) 70 | // .pipe(gulp.dest(distDir)); 71 | 72 | function fileContents (filePath, file) { 73 | return file.contents.toString(); 74 | } 75 | 76 | var files = [ 77 | srcDir + '/js/core/bootstrap.js', 78 | srcDir + '/js/core/darkroom.js', 79 | srcDir + '/js/core/*.js', 80 | // srcDir + '/js/plugins/*.js', 81 | srcDir + '/js/plugins/darkroom.history.js', 82 | srcDir + '/js/plugins/darkroom.rotate.js', 83 | srcDir + '/js/plugins/darkroom.crop.js', 84 | srcDir + '/js/plugins/darkroom.save.js', 85 | ]; 86 | 87 | gulp.src(files) 88 | .pipe(plumber()) 89 | .pipe(isDebug ? sourcemaps.init() : gutil.noop()) 90 | .pipe(concat('darkroom.js', {newLine: ';'})) 91 | .pipe(inject(svgs, { transform: fileContents })) 92 | .pipe(isDebug ? gutil.noop() : uglify({mangle: false})) 93 | .pipe(isDebug ? sourcemaps.write() : gutil.noop()) 94 | .pipe(gulp.dest(distDir)) 95 | }) 96 | 97 | // 98 | // Stylesheet 99 | // 100 | gulp.task('styles', function () { 101 | gulp.src(srcDir + '/css/darkroom.scss') 102 | .pipe(plumber()) 103 | .pipe(isDebug ? sourcemaps.init() : gutil.noop()) 104 | .pipe(sass({ 105 | outputStyle: isDebug ? 'nested' : 'compressed' 106 | })) 107 | .pipe(isDebug ? sourcemaps.write() : gutil.noop()) 108 | .pipe(gulp.dest(distDir)) 109 | }) 110 | -------------------------------------------------------------------------------- /lib/css/_layout.scss: -------------------------------------------------------------------------------- 1 | .darkroom-container { 2 | position: relative; 3 | } 4 | 5 | .darkroom-image-container { 6 | top: 0; 7 | left: 0; 8 | } 9 | 10 | .darkroom-image-container img { 11 | // display: none; 12 | } 13 | -------------------------------------------------------------------------------- /lib/css/_toolbar.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Toolbar 3 | // 4 | .darkroom-toolbar { 5 | display: block; 6 | position: absolute; 7 | top: -45px; 8 | left: 0; 9 | background: #444; 10 | height: 40px; 11 | min-width: 40px; 12 | z-index: 99; 13 | border-radius: 2px; 14 | white-space: nowrap; 15 | padding: 0 5px; 16 | 17 | // Triangle 18 | &:before { 19 | content: ""; 20 | position: absolute; 21 | bottom: -7px; 22 | left: 20px; 23 | width: 0; 24 | height: 0; 25 | border-left: 7px solid transparent; 26 | border-right: 7px solid transparent; 27 | border-top: 7px solid #444; 28 | } 29 | } 30 | 31 | // 32 | // Button Group 33 | // 34 | .darkroom-button-group { 35 | display: inline-block; 36 | margin: 0; 37 | padding: 0; 38 | // border-right: 1px solid #777; 39 | 40 | &:last-child { 41 | border-right: none; 42 | } 43 | } 44 | 45 | 46 | // 47 | // Button 48 | // 49 | .darkroom-button { 50 | box-sizing: border-box; 51 | background: transparent; 52 | border: none; 53 | outline: none; 54 | padding: 2px 0 0 0; 55 | width: 40px; 56 | height: 40px; 57 | 58 | &:hover { 59 | cursor: pointer; 60 | background: #555; 61 | } 62 | &:active { 63 | cursor: pointer; 64 | background: #333; 65 | } 66 | 67 | &:disabled .darkroom-icon { 68 | fill: #666; 69 | } 70 | &:disabled:hover { 71 | cursor: default; 72 | /*cursor: not-allowed;*/ 73 | background: transparent; 74 | } 75 | &.darkroom-button-active .darkroom-icon { 76 | fill: #33b5e5; 77 | } 78 | &.darkroom-button-hidden { 79 | display: none; 80 | } 81 | &.darkroom-button-success .darkroom-icon { 82 | fill: #99cc00; 83 | } 84 | &.darkroom-button-warning .darkroom-icon { 85 | fill: #FFBB33; 86 | } 87 | &.darkroom-button-danger .darkroom-icon { 88 | fill: #FF4444; 89 | } 90 | } 91 | 92 | // 93 | // Icon 94 | // 95 | .darkroom-icon { 96 | width: 24px; 97 | height: 24px; 98 | fill: #fff; 99 | } 100 | -------------------------------------------------------------------------------- /lib/css/darkroom.scss: -------------------------------------------------------------------------------- 1 | @import 'layout'; 2 | @import 'toolbar'; 3 | -------------------------------------------------------------------------------- /lib/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/icons/crop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/icons/done.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/icons/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/icons/rotate-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/icons/rotate-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/icons/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/icons/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/js/core/bootstrap.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // Inject SVG icons into the DOM 5 | var element = document.createElement('div'); 6 | element.id = 'darkroom-icons'; 7 | element.style.height = 0; 8 | element.style.width = 0; 9 | element.style.position = 'absolute'; 10 | element.style.visibility = 'hidden'; 11 | element.innerHTML = ''; 12 | document.body.appendChild(element); 13 | 14 | })(); 15 | -------------------------------------------------------------------------------- /lib/js/core/darkroom.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | window.Darkroom = Darkroom; 5 | 6 | // Core object of DarkroomJS. 7 | // Basically it's a single object, instanciable via an element 8 | // (it could be a CSS selector or a DOM element), some custom options, 9 | // and a list of plugin objects (or none to use default ones). 10 | function Darkroom(element, options, plugins) { 11 | return this.constructor(element, options, plugins); 12 | } 13 | 14 | // Create an empty list of plugin objects, which will be filled by 15 | // other plugin scripts. This is the default plugin list if none is 16 | // specified in Darkroom'ss constructor. 17 | Darkroom.plugins = []; 18 | 19 | Darkroom.prototype = { 20 | // Reference to the main container element 21 | containerElement: null, 22 | 23 | // Reference to the Fabric canvas object 24 | canvas: null, 25 | 26 | // Reference to the Fabric image object 27 | image: null, 28 | 29 | // Reference to the Fabric source canvas object 30 | sourceCanvas: null, 31 | 32 | // Reference to the Fabric source image object 33 | sourceImage: null, 34 | 35 | // Track of the original image element 36 | originalImageElement: null, 37 | 38 | // Stack of transformations to apply to the image source 39 | transformations: [], 40 | 41 | // Default options 42 | defaults: { 43 | // Canvas properties (dimension, ratio, color) 44 | minWidth: null, 45 | minHeight: null, 46 | maxWidth: null, 47 | maxHeight: null, 48 | ratio: null, 49 | backgroundColor: '#fff', 50 | 51 | // Plugins options 52 | plugins: {}, 53 | 54 | // Post-initialisation callback 55 | initialize: function() { /* noop */ } 56 | }, 57 | 58 | // List of the instancied plugins 59 | plugins: {}, 60 | 61 | // This options are a merge between `defaults` and the options passed 62 | // through the constructor 63 | options: {}, 64 | 65 | constructor: function(element, options, plugins) { 66 | this.options = Darkroom.Utils.extend(options, this.defaults); 67 | 68 | if (typeof element === 'string') 69 | element = document.querySelector(element); 70 | if (null === element) 71 | return; 72 | 73 | var image = new Image(); 74 | image.onload = function() { 75 | // Initialize the DOM/Fabric elements 76 | this._initializeDOM(element); 77 | this._initializeImage(); 78 | 79 | // Then initialize the plugins 80 | this._initializePlugins(Darkroom.plugins); 81 | 82 | // Public method to adjust image according to the canvas 83 | this.refresh(function() { 84 | // Execute a custom callback after initialization 85 | this.options.initialize.bind(this).call(); 86 | }.bind(this)); 87 | 88 | }.bind(this) 89 | 90 | //image.crossOrigin = 'anonymous'; 91 | image.src = element.src; 92 | }, 93 | 94 | selfDestroy: function() { 95 | var container = this.containerElement; 96 | var image = new Image(); 97 | image.onload = function() { 98 | container.parentNode.replaceChild(image, container); 99 | } 100 | 101 | image.src = this.sourceImage.toDataURL(); 102 | }, 103 | 104 | // Add ability to attach event listener on the core object. 105 | // It uses the canvas element to process events. 106 | addEventListener: function(eventName, callback) { 107 | var el = this.canvas.getElement(); 108 | if (el.addEventListener){ 109 | el.addEventListener(eventName, callback); 110 | } else if (el.attachEvent) { 111 | el.attachEvent('on' + eventName, callback); 112 | } 113 | }, 114 | 115 | dispatchEvent: function(eventName) { 116 | // Use the old way of creating event to be IE compatible 117 | // See https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events 118 | var event = document.createEvent('Event'); 119 | event.initEvent(eventName, true, true); 120 | 121 | this.canvas.getElement().dispatchEvent(event); 122 | }, 123 | 124 | // Adjust image & canvas dimension according to min/max width/height 125 | // and ratio specified in the options. 126 | // This method should be called after each image transformation. 127 | refresh: function(next) { 128 | var clone = new Image(); 129 | clone.onload = function() { 130 | this._replaceCurrentImage(new fabric.Image(clone)); 131 | 132 | if (next) next(); 133 | }.bind(this); 134 | clone.src = this.sourceImage.toDataURL(); 135 | }, 136 | 137 | _replaceCurrentImage: function(newImage) { 138 | if (this.image) { 139 | this.image.remove(); 140 | } 141 | 142 | this.image = newImage; 143 | this.image.selectable = false; 144 | 145 | // Adjust width or height according to specified ratio 146 | var viewport = Darkroom.Utils.computeImageViewPort(this.image); 147 | var canvasWidth = viewport.width; 148 | var canvasHeight = viewport.height; 149 | 150 | if (null !== this.options.ratio) { 151 | var canvasRatio = +this.options.ratio; 152 | var currentRatio = canvasWidth / canvasHeight; 153 | 154 | if (currentRatio > canvasRatio) { 155 | canvasHeight = canvasWidth / canvasRatio; 156 | } else if (currentRatio < canvasRatio) { 157 | canvasWidth = canvasHeight * canvasRatio; 158 | } 159 | } 160 | 161 | // Then scale the image to fit into dimension limits 162 | var scaleMin = 1; 163 | var scaleMax = 1; 164 | var scaleX = 1; 165 | var scaleY = 1; 166 | 167 | if (null !== this.options.maxWidth && this.options.maxWidth < canvasWidth) { 168 | scaleX = this.options.maxWidth / canvasWidth; 169 | } 170 | if (null !== this.options.maxHeight && this.options.maxHeight < canvasHeight) { 171 | scaleY = this.options.maxHeight / canvasHeight; 172 | } 173 | scaleMin = Math.min(scaleX, scaleY); 174 | 175 | scaleX = 1; 176 | scaleY = 1; 177 | if (null !== this.options.minWidth && this.options.minWidth > canvasWidth) { 178 | scaleX = this.options.minWidth / canvasWidth; 179 | } 180 | if (null !== this.options.minHeight && this.options.minHeight > canvasHeight) { 181 | scaleY = this.options.minHeight / canvasHeight; 182 | } 183 | scaleMax = Math.max(scaleX, scaleY); 184 | 185 | var scale = scaleMax * scaleMin; // one should be equals to 1 186 | 187 | canvasWidth *= scale; 188 | canvasHeight *= scale; 189 | 190 | // Finally place the image in the center of the canvas 191 | this.image.setScaleX(1 * scale); 192 | this.image.setScaleY(1 * scale); 193 | this.canvas.add(this.image); 194 | this.canvas.setWidth(canvasWidth); 195 | this.canvas.setHeight(canvasHeight); 196 | this.canvas.centerObject(this.image); 197 | this.image.setCoords(); 198 | }, 199 | 200 | // Apply the transformation on the current image and save it in the 201 | // transformations stack (in order to reconstitute the previous states 202 | // of the image). 203 | applyTransformation: function(transformation) { 204 | this.transformations.push(transformation); 205 | 206 | transformation.applyTransformation( 207 | this.sourceCanvas, 208 | this.sourceImage, 209 | this._postTransformation.bind(this) 210 | ); 211 | }, 212 | 213 | _postTransformation: function(newImage) { 214 | if (newImage) 215 | this.sourceImage = newImage; 216 | 217 | this.refresh(function() { 218 | this.dispatchEvent('core:transformation'); 219 | }.bind(this)); 220 | }, 221 | 222 | // Initialize image from original element plus re-apply every 223 | // transformations. 224 | reinitializeImage: function() { 225 | this.sourceImage.remove(); 226 | this._initializeImage(); 227 | this._popTransformation(this.transformations.slice()) 228 | }, 229 | 230 | _popTransformation: function(transformations) { 231 | if (0 === transformations.length) { 232 | this.dispatchEvent('core:reinitialized'); 233 | this.refresh(); 234 | return; 235 | } 236 | 237 | var transformation = transformations.shift(); 238 | 239 | var next = function(newImage) { 240 | if (newImage) this.sourceImage = newImage; 241 | this._popTransformation(transformations) 242 | }; 243 | 244 | transformation.applyTransformation( 245 | this.sourceCanvas, 246 | this.sourceImage, 247 | next.bind(this) 248 | ); 249 | }, 250 | 251 | // Create the DOM elements and instanciate the Fabric canvas. 252 | // The image element is replaced by a new `div` element. 253 | // However the original image is re-injected in order to keep a trace of it. 254 | _initializeDOM: function(imageElement) { 255 | // Container 256 | var mainContainerElement = document.createElement('div'); 257 | mainContainerElement.className = 'darkroom-container'; 258 | 259 | // Toolbar 260 | var toolbarElement = document.createElement('div'); 261 | toolbarElement.className = 'darkroom-toolbar'; 262 | mainContainerElement.appendChild(toolbarElement); 263 | 264 | // Viewport canvas 265 | var canvasContainerElement = document.createElement('div'); 266 | canvasContainerElement.className = 'darkroom-image-container'; 267 | var canvasElement = document.createElement('canvas'); 268 | canvasContainerElement.appendChild(canvasElement); 269 | mainContainerElement.appendChild(canvasContainerElement); 270 | 271 | // Source canvas 272 | var sourceCanvasContainerElement = document.createElement('div'); 273 | sourceCanvasContainerElement.className = 'darkroom-source-container'; 274 | sourceCanvasContainerElement.style.display = 'none'; 275 | var sourceCanvasElement = document.createElement('canvas'); 276 | sourceCanvasContainerElement.appendChild(sourceCanvasElement); 277 | mainContainerElement.appendChild(sourceCanvasContainerElement); 278 | 279 | // Original image 280 | imageElement.parentNode.replaceChild(mainContainerElement, imageElement); 281 | imageElement.style.display = 'none'; 282 | mainContainerElement.appendChild(imageElement); 283 | 284 | // Instanciate object from elements 285 | this.containerElement = mainContainerElement; 286 | this.originalImageElement = imageElement; 287 | 288 | this.toolbar = new Darkroom.UI.Toolbar(toolbarElement); 289 | 290 | this.canvas = new fabric.Canvas(canvasElement, { 291 | selection: false, 292 | backgroundColor: this.options.backgroundColor 293 | }); 294 | 295 | this.sourceCanvas = new fabric.Canvas(sourceCanvasElement, { 296 | selection: false, 297 | backgroundColor: this.options.backgroundColor 298 | }); 299 | }, 300 | 301 | // Instanciate the Fabric image object. 302 | // The image is created as a static element with no control, 303 | // then it is add in the Fabric canvas object. 304 | _initializeImage: function() { 305 | this.sourceImage = new fabric.Image(this.originalImageElement, { 306 | // Some options to make the image static 307 | selectable: false, 308 | evented: false, 309 | lockMovementX: true, 310 | lockMovementY: true, 311 | lockRotation: true, 312 | lockScalingX: true, 313 | lockScalingY: true, 314 | lockUniScaling: true, 315 | hasControls: false, 316 | hasBorders: false, 317 | }); 318 | 319 | this.sourceCanvas.add(this.sourceImage); 320 | 321 | // Adjust width or height according to specified ratio 322 | var viewport = Darkroom.Utils.computeImageViewPort(this.sourceImage); 323 | var canvasWidth = viewport.width; 324 | var canvasHeight = viewport.height; 325 | 326 | this.sourceCanvas.setWidth(canvasWidth); 327 | this.sourceCanvas.setHeight(canvasHeight); 328 | this.sourceCanvas.centerObject(this.sourceImage); 329 | this.sourceImage.setCoords(); 330 | }, 331 | 332 | // Initialize every plugins. 333 | // Note that plugins are instanciated in the same order than they 334 | // are declared in the parameter object. 335 | _initializePlugins: function(plugins) { 336 | for (var name in plugins) { 337 | var plugin = plugins[name]; 338 | var options = this.options.plugins[name]; 339 | 340 | // Setting false into the plugin options will disable the plugin 341 | if (options === false) 342 | continue; 343 | 344 | // Avoid any issues with _proto_ 345 | if (!plugins.hasOwnProperty(name)) 346 | continue; 347 | 348 | this.plugins[name] = new plugin(this, options); 349 | } 350 | }, 351 | } 352 | 353 | })(); 354 | -------------------------------------------------------------------------------- /lib/js/core/plugin.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | Darkroom.Plugin = Plugin; 5 | 6 | // Define a plugin object. This is the (abstract) parent class which 7 | // has to be extended for each plugin. 8 | function Plugin(darkroom, options) { 9 | this.darkroom = darkroom; 10 | this.options = Darkroom.Utils.extend(options, this.defaults); 11 | this.initialize(); 12 | } 13 | 14 | Plugin.prototype = { 15 | defaults: {}, 16 | initialize: function() { } 17 | } 18 | 19 | // Inspired by Backbone.js extend capability. 20 | Plugin.extend = function(protoProps) { 21 | var parent = this; 22 | var child; 23 | 24 | if (protoProps && protoProps.hasOwnProperty('constructor')) { 25 | child = protoProps.constructor; 26 | } else { 27 | child = function(){ return parent.apply(this, arguments); }; 28 | } 29 | 30 | Darkroom.Utils.extend(child, parent); 31 | 32 | var Surrogate = function(){ this.constructor = child; }; 33 | Surrogate.prototype = parent.prototype; 34 | child.prototype = new Surrogate; 35 | 36 | if (protoProps) Darkroom.Utils.extend(child.prototype, protoProps); 37 | 38 | child.__super__ = parent.prototype; 39 | 40 | return child; 41 | } 42 | 43 | })(); 44 | -------------------------------------------------------------------------------- /lib/js/core/transformation.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | Darkroom.Transformation = Transformation; 5 | 6 | function Transformation(options) { 7 | this.options = options; 8 | } 9 | 10 | Transformation.prototype = { 11 | applyTransformation: function(image) { /* no-op */ } 12 | } 13 | 14 | // Inspired by Backbone.js extend capability. 15 | Transformation.extend = function(protoProps) { 16 | var parent = this; 17 | var child; 18 | 19 | if (protoProps && protoProps.hasOwnProperty('constructor')) { 20 | child = protoProps.constructor; 21 | } else { 22 | child = function(){ return parent.apply(this, arguments); }; 23 | } 24 | 25 | Darkroom.Utils.extend(child, parent); 26 | 27 | var Surrogate = function(){ this.constructor = child; }; 28 | Surrogate.prototype = parent.prototype; 29 | child.prototype = new Surrogate; 30 | 31 | if (protoProps) Darkroom.Utils.extend(child.prototype, protoProps); 32 | 33 | child.__super__ = parent.prototype; 34 | 35 | return child; 36 | } 37 | 38 | })(); 39 | -------------------------------------------------------------------------------- /lib/js/core/ui.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | Darkroom.UI = { 5 | Toolbar: Toolbar, 6 | ButtonGroup: ButtonGroup, 7 | Button: Button, 8 | }; 9 | 10 | // Toolbar object. 11 | function Toolbar(element) { 12 | this.element = element; 13 | } 14 | 15 | Toolbar.prototype = { 16 | createButtonGroup: function(options) { 17 | var buttonGroup = document.createElement('div'); 18 | buttonGroup.className = 'darkroom-button-group'; 19 | this.element.appendChild(buttonGroup); 20 | 21 | return new ButtonGroup(buttonGroup); 22 | } 23 | }; 24 | 25 | // ButtonGroup object. 26 | function ButtonGroup(element) { 27 | this.element = element; 28 | } 29 | 30 | ButtonGroup.prototype = { 31 | createButton: function(options) { 32 | var defaults = { 33 | image: 'help', 34 | type: 'default', 35 | group: 'default', 36 | hide: false, 37 | disabled: false 38 | }; 39 | 40 | options = Darkroom.Utils.extend(options, defaults); 41 | 42 | var buttonElement = document.createElement('button'); 43 | buttonElement.type = 'button'; 44 | buttonElement.className = 'darkroom-button darkroom-button-' + options.type; 45 | buttonElement.innerHTML = ''; 46 | this.element.appendChild(buttonElement); 47 | 48 | var button = new Button(buttonElement); 49 | button.hide(options.hide); 50 | button.disable(options.disabled); 51 | 52 | return button; 53 | } 54 | } 55 | 56 | // Button object. 57 | function Button(element) { 58 | this.element = element; 59 | } 60 | 61 | Button.prototype = { 62 | addEventListener: function(eventName, listener) { 63 | if (this.element.addEventListener){ 64 | this.element.addEventListener(eventName, listener); 65 | } else if (this.element.attachEvent) { 66 | this.element.attachEvent('on' + eventName, listener); 67 | } 68 | }, 69 | removeEventListener: function(eventName, listener) { 70 | if (this.element.removeEventListener){ 71 | this.element.removeEventListener(eventName, listener); 72 | } 73 | }, 74 | active: function(value) { 75 | if (value) 76 | this.element.classList.add('darkroom-button-active'); 77 | else 78 | this.element.classList.remove('darkroom-button-active'); 79 | }, 80 | hide: function(value) { 81 | if (value) 82 | this.element.classList.add('darkroom-button-hidden'); 83 | else 84 | this.element.classList.remove('darkroom-button-hidden'); 85 | }, 86 | disable: function(value) { 87 | this.element.disabled = (value) ? true : false; 88 | } 89 | }; 90 | 91 | })(); 92 | -------------------------------------------------------------------------------- /lib/js/core/utils.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | Darkroom.Utils = { 5 | extend: extend, 6 | computeImageViewPort: computeImageViewPort, 7 | }; 8 | 9 | 10 | // Utility method to easily extend objects. 11 | function extend(b, a) { 12 | var prop; 13 | if (b === undefined) { 14 | return a; 15 | } 16 | for (prop in a) { 17 | if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) { 18 | b[prop] = a[prop]; 19 | } 20 | } 21 | return b; 22 | } 23 | 24 | function computeImageViewPort(image) { 25 | return { 26 | height: Math.abs(image.getWidth() * (Math.sin(image.getAngle() * Math.PI/180))) + Math.abs(image.getHeight() * (Math.cos(image.getAngle() * Math.PI/180))), 27 | width: Math.abs(image.getHeight() * (Math.sin(image.getAngle() * Math.PI/180))) + Math.abs(image.getWidth() * (Math.cos(image.getAngle() * Math.PI/180))), 28 | } 29 | } 30 | 31 | })(); 32 | -------------------------------------------------------------------------------- /lib/js/plugins/darkroom.crop.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Crop = Darkroom.Transformation.extend({ 5 | applyTransformation: function(canvas, image, next) { 6 | // Snapshot the image delimited by the crop zone 7 | var snapshot = new Image(); 8 | snapshot.onload = function() { 9 | // Validate image 10 | if (height < 1 || width < 1) 11 | return; 12 | 13 | var imgInstance = new fabric.Image(this, { 14 | // options to make the image static 15 | selectable: false, 16 | evented: false, 17 | lockMovementX: true, 18 | lockMovementY: true, 19 | lockRotation: true, 20 | lockScalingX: true, 21 | lockScalingY: true, 22 | lockUniScaling: true, 23 | hasControls: false, 24 | hasBorders: false 25 | }); 26 | 27 | var width = this.width; 28 | var height = this.height; 29 | 30 | // Update canvas size 31 | canvas.setWidth(width); 32 | canvas.setHeight(height); 33 | 34 | // Add image 35 | image.remove(); 36 | canvas.add(imgInstance); 37 | 38 | next(imgInstance); 39 | }; 40 | 41 | var viewport = Darkroom.Utils.computeImageViewPort(image); 42 | var imageWidth = viewport.width; 43 | var imageHeight = viewport.height; 44 | 45 | var left = this.options.left * imageWidth; 46 | var top = this.options.top * imageHeight; 47 | var width = Math.min(this.options.width * imageWidth, imageWidth - left); 48 | var height = Math.min(this.options.height * imageHeight, imageHeight - top); 49 | 50 | snapshot.src = canvas.toDataURL({ 51 | left: left, 52 | top: top, 53 | width: width, 54 | height: height, 55 | }); 56 | } 57 | }); 58 | 59 | var CropZone = fabric.util.createClass(fabric.Rect, { 60 | _render: function(ctx) { 61 | this.callSuper('_render', ctx); 62 | 63 | var canvas = ctx.canvas; 64 | var dashWidth = 7; 65 | 66 | // Set original scale 67 | var flipX = this.flipX ? -1 : 1; 68 | var flipY = this.flipY ? -1 : 1; 69 | var scaleX = flipX / this.scaleX; 70 | var scaleY = flipY / this.scaleY; 71 | 72 | ctx.scale(scaleX, scaleY); 73 | 74 | // Overlay rendering 75 | ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; 76 | this._renderOverlay(ctx); 77 | 78 | // Set dashed borders 79 | if (ctx.setLineDash !== undefined) 80 | ctx.setLineDash([dashWidth, dashWidth]); 81 | else if (ctx.mozDash !== undefined) 82 | ctx.mozDash = [dashWidth, dashWidth]; 83 | 84 | // First lines rendering with black 85 | ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)'; 86 | this._renderBorders(ctx); 87 | this._renderGrid(ctx); 88 | 89 | // Re render lines in white 90 | ctx.lineDashOffset = dashWidth; 91 | ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; 92 | this._renderBorders(ctx); 93 | this._renderGrid(ctx); 94 | 95 | // Reset scale 96 | ctx.scale(1/scaleX, 1/scaleY); 97 | }, 98 | 99 | _renderOverlay: function(ctx) { 100 | var canvas = ctx.canvas; 101 | 102 | // 103 | // x0 x1 x2 x3 104 | // y0 +------------------------+ 105 | // |\\\\\\\\\\\\\\\\\\\\\\\\| 106 | // |\\\\\\\\\\\\\\\\\\\\\\\\| 107 | // y1 +------+---------+-------+ 108 | // |\\\\\\| |\\\\\\\| 109 | // |\\\\\\| 0 |\\\\\\\| 110 | // |\\\\\\| |\\\\\\\| 111 | // y2 +------+---------+-------+ 112 | // |\\\\\\\\\\\\\\\\\\\\\\\\| 113 | // |\\\\\\\\\\\\\\\\\\\\\\\\| 114 | // y3 +------------------------+ 115 | // 116 | 117 | var x0 = Math.ceil(-this.getWidth() / 2 - this.getLeft()); 118 | var x1 = Math.ceil(-this.getWidth() / 2); 119 | var x2 = Math.ceil(this.getWidth() / 2); 120 | var x3 = Math.ceil(this.getWidth() / 2 + (canvas.width - this.getWidth() - this.getLeft())); 121 | 122 | var y0 = Math.ceil(-this.getHeight() / 2 - this.getTop()); 123 | var y1 = Math.ceil(-this.getHeight() / 2); 124 | var y2 = Math.ceil(this.getHeight() / 2); 125 | var y3 = Math.ceil(this.getHeight() / 2 + (canvas.height - this.getHeight() - this.getTop())); 126 | 127 | ctx.beginPath(); 128 | 129 | // Draw outer rectangle. 130 | // Numbers are +/-1 so that overlay edges don't get blurry. 131 | ctx.moveTo(x0 - 1, y0 - 1); 132 | ctx.lineTo(x3 + 1, y0 - 1); 133 | ctx.lineTo(x3 + 1, y3 + 1); 134 | ctx.lineTo(x0 - 1, y3 - 1); 135 | ctx.lineTo(x0 - 1, y0 - 1); 136 | ctx.closePath(); 137 | 138 | // Draw inner rectangle. 139 | ctx.moveTo(x1, y1); 140 | ctx.lineTo(x1, y2); 141 | ctx.lineTo(x2, y2); 142 | ctx.lineTo(x2, y1); 143 | ctx.lineTo(x1, y1); 144 | 145 | ctx.closePath(); 146 | ctx.fill(); 147 | }, 148 | 149 | _renderBorders: function(ctx) { 150 | ctx.beginPath(); 151 | ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2); // upper left 152 | ctx.lineTo(this.getWidth()/2, -this.getHeight()/2); // upper right 153 | ctx.lineTo(this.getWidth()/2, this.getHeight()/2); // down right 154 | ctx.lineTo(-this.getWidth()/2, this.getHeight()/2); // down left 155 | ctx.lineTo(-this.getWidth()/2, -this.getHeight()/2); // upper left 156 | ctx.stroke(); 157 | }, 158 | 159 | _renderGrid: function(ctx) { 160 | // Vertical lines 161 | ctx.beginPath(); 162 | ctx.moveTo(-this.getWidth()/2 + 1/3 * this.getWidth(), -this.getHeight()/2); 163 | ctx.lineTo(-this.getWidth()/2 + 1/3 * this.getWidth(), this.getHeight()/2); 164 | ctx.stroke(); 165 | ctx.beginPath(); 166 | ctx.moveTo(-this.getWidth()/2 + 2/3 * this.getWidth(), -this.getHeight()/2); 167 | ctx.lineTo(-this.getWidth()/2 + 2/3 * this.getWidth(), this.getHeight()/2); 168 | ctx.stroke(); 169 | // Horizontal lines 170 | ctx.beginPath(); 171 | ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2 + 1/3 * this.getHeight()); 172 | ctx.lineTo(this.getWidth()/2, -this.getHeight()/2 + 1/3 * this.getHeight()); 173 | ctx.stroke(); 174 | ctx.beginPath(); 175 | ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2 + 2/3 * this.getHeight()); 176 | ctx.lineTo(this.getWidth()/2, -this.getHeight()/2 + 2/3 * this.getHeight()); 177 | ctx.stroke(); 178 | } 179 | }); 180 | 181 | Darkroom.plugins['crop'] = Darkroom.Plugin.extend({ 182 | // Init point 183 | startX: null, 184 | startY: null, 185 | 186 | // Keycrop 187 | isKeyCroping: false, 188 | isKeyLeft: false, 189 | isKeyUp: false, 190 | 191 | defaults: { 192 | // min crop dimension 193 | minHeight: 1, 194 | minWidth: 1, 195 | // ensure crop ratio 196 | ratio: null, 197 | // quick crop feature (set a key code to enable it) 198 | quickCropKey: false 199 | }, 200 | 201 | initialize: function InitDarkroomCropPlugin() { 202 | var buttonGroup = this.darkroom.toolbar.createButtonGroup(); 203 | 204 | this.cropButton = buttonGroup.createButton({ 205 | image: 'crop' 206 | }); 207 | this.okButton = buttonGroup.createButton({ 208 | image: 'done', 209 | type: 'success', 210 | hide: true 211 | }); 212 | this.cancelButton = buttonGroup.createButton({ 213 | image: 'close', 214 | type: 'danger', 215 | hide: true 216 | }); 217 | 218 | // Buttons click 219 | this.cropButton.addEventListener('click', this.toggleCrop.bind(this)); 220 | this.okButton.addEventListener('click', this.cropCurrentZone.bind(this)); 221 | this.cancelButton.addEventListener('click', this.releaseFocus.bind(this)); 222 | 223 | // Canvas events 224 | this.darkroom.canvas.on('mouse:down', this.onMouseDown.bind(this)); 225 | this.darkroom.canvas.on('mouse:move', this.onMouseMove.bind(this)); 226 | this.darkroom.canvas.on('mouse:up', this.onMouseUp.bind(this)); 227 | this.darkroom.canvas.on('object:moving', this.onObjectMoving.bind(this)); 228 | this.darkroom.canvas.on('object:scaling', this.onObjectScaling.bind(this)); 229 | 230 | fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this)); 231 | fabric.util.addListener(fabric.document, 'keyup', this.onKeyUp.bind(this)); 232 | 233 | this.darkroom.addEventListener('core:transformation', this.releaseFocus.bind(this)); 234 | }, 235 | 236 | // Avoid crop zone to go beyond the canvas edges 237 | onObjectMoving: function(event) { 238 | if (!this.hasFocus()) { 239 | return; 240 | } 241 | 242 | var currentObject = event.target; 243 | if (currentObject !== this.cropZone) 244 | return; 245 | 246 | var canvas = this.darkroom.canvas; 247 | var x = currentObject.getLeft(), y = currentObject.getTop(); 248 | var w = currentObject.getWidth(), h = currentObject.getHeight(); 249 | var maxX = canvas.getWidth() - w; 250 | var maxY = canvas.getHeight() - h; 251 | 252 | if (x < 0) 253 | currentObject.set('left', 0); 254 | if (y < 0) 255 | currentObject.set('top', 0); 256 | if (x > maxX) 257 | currentObject.set('left', maxX); 258 | if (y > maxY) 259 | currentObject.set('top', maxY); 260 | 261 | this.darkroom.dispatchEvent('crop:update'); 262 | }, 263 | 264 | // Prevent crop zone from going beyond the canvas edges (like mouseMove) 265 | onObjectScaling: function(event) { 266 | if (!this.hasFocus()) { 267 | return; 268 | } 269 | 270 | var preventScaling = false; 271 | var currentObject = event.target; 272 | if (currentObject !== this.cropZone) 273 | return; 274 | 275 | var canvas = this.darkroom.canvas; 276 | var pointer = canvas.getPointer(event.e); 277 | var x = pointer.x; 278 | var y = pointer.y; 279 | 280 | var minX = currentObject.getLeft(); 281 | var minY = currentObject.getTop(); 282 | var maxX = currentObject.getLeft() + currentObject.getWidth(); 283 | var maxY = currentObject.getTop() + currentObject.getHeight(); 284 | 285 | if (null !== this.options.ratio) { 286 | if (minX < 0 || maxX > canvas.getWidth() || minY < 0 || maxY > canvas.getHeight()) { 287 | preventScaling = true; 288 | } 289 | } 290 | 291 | if (minX < 0 || maxX > canvas.getWidth() || preventScaling) { 292 | var lastScaleX = this.lastScaleX || 1; 293 | currentObject.setScaleX(lastScaleX); 294 | } 295 | if (minX < 0) { 296 | currentObject.setLeft(0); 297 | } 298 | 299 | if (minY < 0 || maxY > canvas.getHeight() || preventScaling) { 300 | var lastScaleY = this.lastScaleY || 1; 301 | currentObject.setScaleY(lastScaleY); 302 | } 303 | if (minY < 0) { 304 | currentObject.setTop(0); 305 | } 306 | 307 | if (currentObject.getWidth() < this.options.minWidth) { 308 | currentObject.scaleToWidth(this.options.minWidth); 309 | } 310 | if (currentObject.getHeight() < this.options.minHeight) { 311 | currentObject.scaleToHeight(this.options.minHeight); 312 | } 313 | 314 | this.lastScaleX = currentObject.getScaleX(); 315 | this.lastScaleY = currentObject.getScaleY(); 316 | 317 | this.darkroom.dispatchEvent('crop:update'); 318 | }, 319 | 320 | // Init crop zone 321 | onMouseDown: function(event) { 322 | if (!this.hasFocus()) { 323 | return; 324 | } 325 | 326 | var canvas = this.darkroom.canvas; 327 | 328 | // recalculate offset, in case canvas was manipulated since last `calcOffset` 329 | canvas.calcOffset(); 330 | var pointer = canvas.getPointer(event.e); 331 | var x = pointer.x; 332 | var y = pointer.y; 333 | var point = new fabric.Point(x, y); 334 | 335 | // Check if user want to scale or drag the crop zone. 336 | var activeObject = canvas.getActiveObject(); 337 | if (activeObject === this.cropZone || this.cropZone.containsPoint(point)) { 338 | return; 339 | } 340 | 341 | canvas.discardActiveObject(); 342 | this.cropZone.setWidth(0); 343 | this.cropZone.setHeight(0); 344 | this.cropZone.setScaleX(1); 345 | this.cropZone.setScaleY(1); 346 | 347 | this.startX = x; 348 | this.startY = y; 349 | }, 350 | 351 | // Extend crop zone 352 | onMouseMove: function(event) { 353 | // Quick crop feature 354 | if (this.isKeyCroping) 355 | return this.onMouseMoveKeyCrop(event); 356 | 357 | if (null === this.startX || null === this.startY) { 358 | return; 359 | } 360 | 361 | var canvas = this.darkroom.canvas; 362 | var pointer = canvas.getPointer(event.e); 363 | var x = pointer.x; 364 | var y = pointer.y; 365 | 366 | this._renderCropZone(this.startX, this.startY, x, y); 367 | }, 368 | 369 | onMouseMoveKeyCrop: function(event) { 370 | var canvas = this.darkroom.canvas; 371 | var zone = this.cropZone; 372 | 373 | var pointer = canvas.getPointer(event.e); 374 | var x = pointer.x; 375 | var y = pointer.y; 376 | 377 | if (!zone.left || !zone.top) { 378 | zone.setTop(y); 379 | zone.setLeft(x); 380 | } 381 | 382 | this.isKeyLeft = x < zone.left + zone.width / 2 ; 383 | this.isKeyUp = y < zone.top + zone.height / 2 ; 384 | 385 | this._renderCropZone( 386 | Math.min(zone.left, x), 387 | Math.min(zone.top, y), 388 | Math.max(zone.left+zone.width, x), 389 | Math.max(zone.top+zone.height, y) 390 | ); 391 | }, 392 | 393 | // Finish crop zone 394 | onMouseUp: function(event) { 395 | if (null === this.startX || null === this.startY) { 396 | return; 397 | } 398 | 399 | var canvas = this.darkroom.canvas; 400 | this.cropZone.setCoords(); 401 | canvas.setActiveObject(this.cropZone); 402 | canvas.calcOffset(); 403 | 404 | this.startX = null; 405 | this.startY = null; 406 | }, 407 | 408 | onKeyDown: function(event) { 409 | if (false === this.options.quickCropKey || event.keyCode !== this.options.quickCropKey || this.isKeyCroping) 410 | return; 411 | 412 | // Active quick crop flow 413 | this.isKeyCroping = true ; 414 | this.darkroom.canvas.discardActiveObject(); 415 | this.cropZone.setWidth(0); 416 | this.cropZone.setHeight(0); 417 | this.cropZone.setScaleX(1); 418 | this.cropZone.setScaleY(1); 419 | this.cropZone.setTop(0); 420 | this.cropZone.setLeft(0); 421 | }, 422 | 423 | onKeyUp: function(event) { 424 | if (false === this.options.quickCropKey || event.keyCode !== this.options.quickCropKey || !this.isKeyCroping) 425 | return; 426 | 427 | // Unactive quick crop flow 428 | this.isKeyCroping = false; 429 | this.startX = 1; 430 | this.startY = 1; 431 | this.onMouseUp(); 432 | }, 433 | 434 | selectZone: function(x, y, width, height, forceDimension) { 435 | if (!this.hasFocus()) 436 | this.requireFocus(); 437 | 438 | if (!forceDimension) { 439 | this._renderCropZone(x, y, x+width, y+height); 440 | } else { 441 | this.cropZone.set({ 442 | 'left': x, 443 | 'top': y, 444 | 'width': width, 445 | 'height': height 446 | }); 447 | } 448 | 449 | var canvas = this.darkroom.canvas; 450 | canvas.bringToFront(this.cropZone); 451 | this.cropZone.setCoords(); 452 | canvas.setActiveObject(this.cropZone); 453 | canvas.calcOffset(); 454 | 455 | this.darkroom.dispatchEvent('crop:update'); 456 | }, 457 | 458 | toggleCrop: function() { 459 | if (!this.hasFocus()) 460 | this.requireFocus(); 461 | else 462 | this.releaseFocus(); 463 | }, 464 | 465 | cropCurrentZone: function() { 466 | if (!this.hasFocus()) 467 | return; 468 | 469 | // Avoid croping empty zone 470 | if (this.cropZone.width < 1 && this.cropZone.height < 1) 471 | return; 472 | 473 | var image = this.darkroom.image; 474 | 475 | // Compute crop zone dimensions 476 | var top = this.cropZone.getTop() - image.getTop(); 477 | var left = this.cropZone.getLeft() - image.getLeft(); 478 | var width = this.cropZone.getWidth(); 479 | var height = this.cropZone.getHeight(); 480 | 481 | // Adjust dimensions to image only 482 | if (top < 0) { 483 | height += top; 484 | top = 0; 485 | } 486 | 487 | if (left < 0) { 488 | width += left; 489 | left = 0; 490 | } 491 | 492 | // Apply crop transformation. 493 | // Make sure to use relative dimension since the crop will be applied 494 | // on the source image. 495 | this.darkroom.applyTransformation(new Crop({ 496 | top: top / image.getHeight(), 497 | left: left / image.getWidth(), 498 | width: width / image.getWidth(), 499 | height: height / image.getHeight(), 500 | })); 501 | }, 502 | 503 | // Test wether crop zone is set 504 | hasFocus: function() { 505 | return this.cropZone !== undefined; 506 | }, 507 | 508 | // Create the crop zone 509 | requireFocus: function() { 510 | this.cropZone = new CropZone({ 511 | fill: 'transparent', 512 | hasBorders: false, 513 | originX: 'left', 514 | originY: 'top', 515 | //stroke: '#444', 516 | //strokeDashArray: [5, 5], 517 | //borderColor: '#444', 518 | cornerColor: '#444', 519 | cornerSize: 8, 520 | transparentCorners: false, 521 | lockRotation: true, 522 | hasRotatingPoint: false, 523 | }); 524 | 525 | if (null !== this.options.ratio) { 526 | this.cropZone.set('lockUniScaling', true); 527 | } 528 | 529 | this.darkroom.canvas.add(this.cropZone); 530 | this.darkroom.canvas.defaultCursor = 'crosshair'; 531 | 532 | this.cropButton.active(true); 533 | this.okButton.hide(false); 534 | this.cancelButton.hide(false); 535 | }, 536 | 537 | // Remove the crop zone 538 | releaseFocus: function() { 539 | if (undefined === this.cropZone) 540 | return; 541 | 542 | this.cropZone.remove(); 543 | this.cropZone = undefined; 544 | 545 | this.cropButton.active(false); 546 | this.okButton.hide(true); 547 | this.cancelButton.hide(true); 548 | 549 | this.darkroom.canvas.defaultCursor = 'default'; 550 | 551 | this.darkroom.dispatchEvent('crop:update'); 552 | }, 553 | 554 | _renderCropZone: function(fromX, fromY, toX, toY) { 555 | var canvas = this.darkroom.canvas; 556 | 557 | var isRight = (toX > fromX); 558 | var isLeft = !isRight; 559 | var isDown = (toY > fromY); 560 | var isUp = !isDown; 561 | 562 | var minWidth = Math.min(+this.options.minWidth, canvas.getWidth()); 563 | var minHeight = Math.min(+this.options.minHeight, canvas.getHeight()); 564 | 565 | // Define corner coordinates 566 | var leftX = Math.min(fromX, toX); 567 | var rightX = Math.max(fromX, toX); 568 | var topY = Math.min(fromY, toY); 569 | var bottomY = Math.max(fromY, toY); 570 | 571 | // Replace current point into the canvas 572 | leftX = Math.max(0, leftX); 573 | rightX = Math.min(canvas.getWidth(), rightX); 574 | topY = Math.max(0, topY) 575 | bottomY = Math.min(canvas.getHeight(), bottomY); 576 | 577 | // Recalibrate coordinates according to given options 578 | if (rightX - leftX < minWidth) { 579 | if (isRight) 580 | rightX = leftX + minWidth; 581 | else 582 | leftX = rightX - minWidth; 583 | } 584 | if (bottomY - topY < minHeight) { 585 | if (isDown) 586 | bottomY = topY + minHeight; 587 | else 588 | topY = bottomY - minHeight; 589 | } 590 | 591 | // Truncate truncate according to canvas dimensions 592 | if (leftX < 0) { 593 | // Translate to the left 594 | rightX += Math.abs(leftX); 595 | leftX = 0 596 | } 597 | if (rightX > canvas.getWidth()) { 598 | // Translate to the right 599 | leftX -= (rightX - canvas.getWidth()); 600 | rightX = canvas.getWidth(); 601 | } 602 | if (topY < 0) { 603 | // Translate to the bottom 604 | bottomY += Math.abs(topY); 605 | topY = 0 606 | } 607 | if (bottomY > canvas.getHeight()) { 608 | // Translate to the right 609 | topY -= (bottomY - canvas.getHeight()); 610 | bottomY = canvas.getHeight(); 611 | } 612 | 613 | var width = rightX - leftX; 614 | var height = bottomY - topY; 615 | var currentRatio = width / height; 616 | 617 | if (this.options.ratio && +this.options.ratio !== currentRatio) { 618 | var ratio = +this.options.ratio; 619 | 620 | if(this.isKeyCroping) { 621 | isLeft = this.isKeyLeft; 622 | isUp = this.isKeyUp; 623 | } 624 | 625 | if (currentRatio < ratio) { 626 | var newWidth = height * ratio; 627 | if (isLeft) { 628 | leftX -= (newWidth - width); 629 | } 630 | width = newWidth; 631 | } else if (currentRatio > ratio) { 632 | var newHeight = height / (ratio * height/width); 633 | if (isUp) { 634 | topY -= (newHeight - height); 635 | } 636 | height = newHeight; 637 | } 638 | 639 | if (leftX < 0) { 640 | leftX = 0; 641 | //TODO 642 | } 643 | if (topY < 0) { 644 | topY = 0; 645 | //TODO 646 | } 647 | if (leftX + width > canvas.getWidth()) { 648 | var newWidth = canvas.getWidth() - leftX; 649 | height = newWidth * height / width; 650 | width = newWidth; 651 | if (isUp) { 652 | topY = fromY - height; 653 | } 654 | } 655 | if (topY + height > canvas.getHeight()) { 656 | var newHeight = canvas.getHeight() - topY; 657 | width = width * newHeight / height; 658 | height = newHeight; 659 | if (isLeft) { 660 | leftX = fromX - width; 661 | } 662 | } 663 | } 664 | 665 | // Apply coordinates 666 | this.cropZone.left = leftX; 667 | this.cropZone.top = topY; 668 | this.cropZone.width = width; 669 | this.cropZone.height = height; 670 | 671 | this.darkroom.canvas.bringToFront(this.cropZone); 672 | 673 | this.darkroom.dispatchEvent('crop:update'); 674 | } 675 | }); 676 | 677 | })(); 678 | -------------------------------------------------------------------------------- /lib/js/plugins/darkroom.history.js: -------------------------------------------------------------------------------- 1 | ;(function(window, document, Darkroom, fabric) { 2 | 'use strict'; 3 | 4 | Darkroom.plugins['history'] = Darkroom.Plugin.extend({ 5 | undoTransformations: [], 6 | 7 | initialize: function InitDarkroomHistoryPlugin() { 8 | this._initButtons(); 9 | 10 | this.darkroom.addEventListener('core:transformation', this._onTranformationApplied.bind(this)); 11 | }, 12 | 13 | undo: function() { 14 | if (this.darkroom.transformations.length === 0) { 15 | return; 16 | } 17 | 18 | var lastTransformation = this.darkroom.transformations.pop(); 19 | this.undoTransformations.unshift(lastTransformation); 20 | 21 | this.darkroom.reinitializeImage(); 22 | this._updateButtons(); 23 | }, 24 | 25 | redo: function() { 26 | if (this.undoTransformations.length === 0) { 27 | return; 28 | } 29 | 30 | var cancelTransformation = this.undoTransformations.shift(); 31 | this.darkroom.transformations.push(cancelTransformation); 32 | 33 | this.darkroom.reinitializeImage(); 34 | this._updateButtons(); 35 | }, 36 | 37 | _initButtons: function() { 38 | var buttonGroup = this.darkroom.toolbar.createButtonGroup(); 39 | 40 | this.backButton = buttonGroup.createButton({ 41 | image: 'undo', 42 | disabled: true 43 | }); 44 | 45 | this.forwardButton = buttonGroup.createButton({ 46 | image: 'redo', 47 | disabled: true 48 | }); 49 | 50 | this.backButton.addEventListener('click', this.undo.bind(this)); 51 | this.forwardButton.addEventListener('click', this.redo.bind(this)); 52 | 53 | return this; 54 | }, 55 | 56 | _updateButtons: function() { 57 | this.backButton.disable((this.darkroom.transformations.length === 0)) 58 | this.forwardButton.disable((this.undoTransformations.length === 0)) 59 | }, 60 | 61 | _onTranformationApplied: function() { 62 | this.undoTransformations = []; 63 | this._updateButtons(); 64 | } 65 | }); 66 | })(window, document, Darkroom, fabric); 67 | -------------------------------------------------------------------------------- /lib/js/plugins/darkroom.rotate.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Rotation = Darkroom.Transformation.extend({ 5 | applyTransformation: function(canvas, image, next) { 6 | var angle = (image.getAngle() + this.options.angle) % 360; 7 | image.rotate(angle); 8 | 9 | var width, height; 10 | height = Math.abs(image.getWidth()*(Math.sin(angle*Math.PI/180)))+Math.abs(image.getHeight()*(Math.cos(angle*Math.PI/180))); 11 | width = Math.abs(image.getHeight()*(Math.sin(angle*Math.PI/180)))+Math.abs(image.getWidth()*(Math.cos(angle*Math.PI/180))); 12 | 13 | canvas.setWidth(width); 14 | canvas.setHeight(height); 15 | 16 | canvas.centerObject(image); 17 | image.setCoords(); 18 | canvas.renderAll(); 19 | 20 | next(); 21 | } 22 | }); 23 | 24 | Darkroom.plugins['rotate'] = Darkroom.Plugin.extend({ 25 | 26 | initialize: function InitDarkroomRotatePlugin() { 27 | var buttonGroup = this.darkroom.toolbar.createButtonGroup(); 28 | 29 | var leftButton = buttonGroup.createButton({ 30 | image: 'rotate-left' 31 | }); 32 | 33 | var rightButton = buttonGroup.createButton({ 34 | image: 'rotate-right' 35 | }); 36 | 37 | leftButton.addEventListener('click', this.rotateLeft.bind(this)); 38 | rightButton.addEventListener('click', this.rotateRight.bind(this)); 39 | }, 40 | 41 | rotateLeft: function rotateLeft() { 42 | this.rotate(-90); 43 | }, 44 | 45 | rotateRight: function rotateRight() { 46 | this.rotate(90); 47 | }, 48 | 49 | rotate: function rotate(angle) { 50 | this.darkroom.applyTransformation( 51 | new Rotation({angle: angle}) 52 | ); 53 | } 54 | 55 | }); 56 | 57 | })(); 58 | -------------------------------------------------------------------------------- /lib/js/plugins/darkroom.save.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | Darkroom.plugins['save'] = Darkroom.Plugin.extend({ 5 | 6 | defaults: { 7 | callback: function() { 8 | this.darkroom.selfDestroy(); 9 | } 10 | }, 11 | 12 | initialize: function InitializeDarkroomSavePlugin() { 13 | var buttonGroup = this.darkroom.toolbar.createButtonGroup(); 14 | 15 | this.destroyButton = buttonGroup.createButton({ 16 | image: 'save' 17 | }); 18 | 19 | this.destroyButton.addEventListener('click', this.options.callback.bind(this)); 20 | }, 21 | }); 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "darkroom", 3 | "description": "Extensible image editing tool via HTML canvas", 4 | "version": "2.0.0", 5 | "license": "MIT", 6 | "homepage": "https://mattketmo.github.io/darkroomjs", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/mattketmo/darkroomjs.git" 10 | }, 11 | "author": "Matthieu Moquet (http://moquet.net/)", 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "gulp": "^3.9.0", 15 | "gulp-concat": "^2.6.0", 16 | "gulp-connect": "^2.2.0", 17 | "gulp-inject": "^1.2.0", 18 | "gulp-plumber": "^1.0.1", 19 | "gulp-sass": "^2.0.4", 20 | "gulp-sourcemaps": "^1.1.0", 21 | "gulp-svgmin": "^1.2.0", 22 | "gulp-svgstore": "^5.0.0", 23 | "gulp-uglify": "^1.4.1", 24 | "gulp-util": "^3.0.0", 25 | "rimraf": "^2.2.8" 26 | }, 27 | "scripts": { 28 | "start": "node_modules/.bin/gulp server build --prod", 29 | "develop": "node_modules/.bin/gulp" 30 | }, 31 | "ignore": [ 32 | "**/.*", 33 | "node_modules", 34 | "bower_components" 35 | ] 36 | } 37 | --------------------------------------------------------------------------------