commit c73e1ed9b4a1c9089d8efedf1f8b7927e2642264 Author: Rikard Bartholf Date: Tue May 9 12:18:29 2017 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..5738b8c --- /dev/null +++ b/.jshintrc @@ -0,0 +1,30 @@ +{ + "globalstrict": true, + + "globals": { + "$": false, + "console": false, + "document": false, + "fabric": false, + "jQuery": false, + "module": false, + "QUnit": false, + "require": false, + "strict": false, + "node": false + }, + "browserify": false, + "strict": true, + "curly": true, + "forin": true, + "latedef": true, + "noarg": true, + "nonbsp": true, + "undef": true, + "unused": true, + "eqeqeq": true, + "esversion": 5, + "freeze": true, + "futurehostile": true, + "scripturl": true +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe7bb0d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# My project's README diff --git a/build-tasks/compile-js.js b/build-tasks/compile-js.js new file mode 100644 index 0000000..6af823e --- /dev/null +++ b/build-tasks/compile-js.js @@ -0,0 +1,35 @@ +'use strict'; + +const gulp = require('gulp'); +const path = require('path'); +const browserify = require('browserify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglifyJs = require('gulp-uglify'); +const sourcemaps = require('gulp-sourcemaps'); +const concat = require('gulp-concat'); +const rename = require('gulp-rename'); +const merge = require('merge-stream'); + +module.exports = () => { + const debug = browserify(path.join('src', 'js', 'main.js'), { + debug: true + }) + .bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(sourcemaps.write()) + .pipe(gulp.dest(path.join('dist', 'js'))); + + const min = browserify(path.join('src', 'js', 'main.js'), { + debug: false + }) + .bundle() + .pipe(source('main.min.js')) + .pipe(buffer()) + .pipe(uglifyJs()) + .pipe(gulp.dest(path.join('dist', 'js'))); + + return merge(debug, min); +}; diff --git a/build-tasks/watcher.js b/build-tasks/watcher.js new file mode 100644 index 0000000..b83b5ea --- /dev/null +++ b/build-tasks/watcher.js @@ -0,0 +1,24 @@ +'use strict'; + +const gulp = require('gulp'); +const path = require('path'); +const watch = require('gulp-watch'); + +module.exports = () => { + const less = watch(path.join('src', 'less', '**', '*.less'), () => { + gulp.start('compile-less'); + }); + + const js = watch(path.join('src', 'js', '**', '*.js'), () => { + gulp.start('compile-js'); + }); +/* + const vendorJs = watch(path.join('src', 'vendor', '**', '*.js'), () => { + gulp.start('compile-vendor-js'); + }); + + const handlebars = watch(path.join('src', 'js', 'handlebars', '**', '*.hbs'), () => { + gulp.start('compile-handlebars'); + }); +*/ +}; diff --git a/fonts/breuertext-medium-webfont.ttf b/fonts/breuertext-medium-webfont.ttf new file mode 100644 index 0000000..8625ebc Binary files /dev/null and b/fonts/breuertext-medium-webfont.ttf differ diff --git a/fonts/breuertext-medium-webfont.woff b/fonts/breuertext-medium-webfont.woff new file mode 100644 index 0000000..a258386 Binary files /dev/null and b/fonts/breuertext-medium-webfont.woff differ diff --git a/fonts/breuertext-regular-webfont.ttf b/fonts/breuertext-regular-webfont.ttf new file mode 100644 index 0000000..978161d Binary files /dev/null and b/fonts/breuertext-regular-webfont.ttf differ diff --git a/fonts/breuertext-regular-webfont.woff b/fonts/breuertext-regular-webfont.woff new file mode 100644 index 0000000..2341d1d Binary files /dev/null and b/fonts/breuertext-regular-webfont.woff differ diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..2121f83 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,20 @@ +'use strict'; + +const gulp = require('gulp'); +const mkdirp = require('mkdirp'); +const del = require('del'); +const path = require('path'); +const buildTasks = path.join(process.cwd(), 'build-tasks'); + +gulp.task('clean-dist', () => del('dist')); +//gulp.task('compile-vendor-js', require(path.join(buildTasks, 'compile-vendor-js'))); +gulp.task('compile-js', require(path.join(buildTasks, 'compile-js'))); +gulp.task('watcher', require(path.join(buildTasks, 'watcher'))); + +gulp.task('build', ['clean-dist'], () => { + mkdirp.sync('dist'); + gulp.start('compile-js'); +}); + +gulp.task('watch', ['build', 'watcher']); +gulp.task('default', ['build']); diff --git a/index.php b/index.php new file mode 100644 index 0000000..f2195d7 --- /dev/null +++ b/index.php @@ -0,0 +1,31 @@ + + + + + + + + Crop - dev + + +
+

Test

+
+
+ + + + Mirror: +
+
+

+
+ +
+


+
+ + + + + diff --git a/main.css b/main.css new file mode 100644 index 0000000..543e825 --- /dev/null +++ b/main.css @@ -0,0 +1,28 @@ +@font-face { + font-family: breuer; + font-style: normal; + font-weight: normal; + src: url('/fonts/breuertext-regular-webfont.woff') format('woff'), + url('/fonts/breuertext-regular-webfont.ttf') format('truetype'); +} + +@font-face { + font-family: breuerMedium; + src: url('/fonts/breuertext-medium-webfont.woff') format('woff'), + url('/fonts/breuertext-medium-webfont.ttf') format('truetype'); +} + +* { + font-family: breuer; +} + +#container { + background: #fff; + text-align:center; + margin: 0; + width: 100%; +} + +.canvas-container { + margin: 0 auto; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c36517a --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "Croppy", + "version": "0.0.1", + "description": "Croppy", + "main": "gulpfile.js", + "repository": { + "type": "git", + "url": "git@bitbucket.org:rikbar/croppy.git" + }, + "devDependencies": { + "browserify": "^13.1.1", + "del": "^2.2.2", + "gulp": "^3.9.0", + "gulp-autoprefixer": "^3.1.1", + "gulp-concat": "^2.6.0", + "gulp-declare": "^0.3.0", + "gulp-handlebars": "^4.0.0", + "gulp-jscs": "^4.0.0", + "gulp-jshint": "^2.0.4", + "gulp-less": "^3.0.3", + "gulp-pxtorem": "^2.0.0", + "gulp-rename": "^1.2.2", + "gulp-sourcemaps": "^1.9.1", + "gulp-strip-comments": "^2.4.3", + "gulp-uglify": "^2.0.0", + "gulp-uglifycss": "^1.0.6", + "gulp-watch": "^4.3.11", + "gulp-wrap": "^0.13.0", + "handlebars": "^4.0.6", + "jshint": "^2.9.4", + "gulp-wrap": "^0.13.0", + "handlebars": "^4.0.6", + "jshint-stylish": "^2.2.1", + "merge-stream": "^1.0.1", + "mkdirp": "^0.5.1", + "vinyl-buffer": "^1.0.0", + "vinyl-source-stream": "^1.1.0" + } +} diff --git a/src/js/components/ImageEditor/ApertureHandler.js b/src/js/components/ImageEditor/ApertureHandler.js new file mode 100644 index 0000000..d49fa7a --- /dev/null +++ b/src/js/components/ImageEditor/ApertureHandler.js @@ -0,0 +1,69 @@ +'use strict'; + +function ApertureHandler(canvas) { + var _items = {x: [], y: []}; + + function init() { + var bounds = canvas; + var hRect = new fabric.Rect({ + width: bounds.width, + height: bounds.height / 2, + fill: canvas.backgroundColor, + opacity: 0.7, + }); + + var vRect = hRect.clone().set({ + width: bounds.width / 2, + height: bounds.height + }); + + _items.x = [].concat([hRect, hRect.clone()]); + _items.y = [].concat([vRect, vRect.clone()]); + + [].concat(_items.x, _items.y).forEach(function (o) { + canvas.add(o); + }); + canvas.renderAll(); + } + init(); + + this.apply = function (axis, viewport) { + this.getApertures(axis).forEach(function (o) { + o.visible = false; + }); + + var active = this.getApertures(axis === 'x' ? 'y' : 'x'); + + active.forEach(function (o) { + o.visible = true; + }); + + if (axis === 'y') { + active[0].top = viewport.top - active[0].height + 0.5; + active[1].top = viewport.top + viewport.height - 0.5; + } else { + active[0].left = viewport.left - active[0].width + 0.5; + active[1].left = viewport.left + viewport.width - 0.5; + } + + this.getApertures().forEach(function (o) { + o.setCoords(); + }); + + canvas.renderAll(); + }; + + this.getApertures = function (axis) { + return axis ? _items[axis] : [].concat(_items.x, _items.y); + }; + + this.setTransparent = function (apply) { + this.getApertures().forEach(function (o) { + o.opacity = apply ? 0.7 : 1; + }); + canvas.renderAll(); + return this; + }; +} + +module.exports = ApertureHandler; diff --git a/src/js/components/ImageEditor/ButtonMenu.js b/src/js/components/ImageEditor/ButtonMenu.js new file mode 100644 index 0000000..61500b3 --- /dev/null +++ b/src/js/components/ImageEditor/ButtonMenu.js @@ -0,0 +1,95 @@ +'use strict'; + +var TextButton = require('./TextButton'); + +function ButtonMenu(canvas, settings) { + var _items = []; + var _width = 0; + + var _settings = $.extend({}, settings, { + background: '#444', + fontSize: 14, + height: 30, + margin: 5, + padding: 20 + }); + + /** + * Calculates the width of all contained items + * @return {Number} + */ + function calcWidth() { + var width = 0; + _items.forEach(function (o) { + width += o.width; + }); + width += _settings.margin * (_items.length - 1); + return width; + } + + /** + * @param {String} text + * @param {Function} callback + * @return {ButtonMenu} + */ + this.addItem = function (text, callback) { + _items.push( + new TextButton(text, _settings) + .onClick(callback) + .set({ __group: this }) + ); + return this; + }; + + /** + * @return {ButtonMenu} + */ + this.bringToFront = function () { + _items.forEach(function (o) { + o.bringToFront(); + }); + return this; + }; + + /** + * @return {Array} + */ + this.getItems = function () { + return _items; + }; + + /** + * @return {Number} + */ + this.getWidth = function () { + _width = calcWidth(); + return _width; + }; + + /** + * Recalculate boundaries and add all items to canvas if required + * @return {ButtonMenu} + */ + this.render = function () { + var start = canvas.width / 2 - this.getWidth() / 2; + var currPos = Math.round(start); + + _items.forEach(function (o) { + o.set({ top: 10, left: currPos }) + .setCoords(); + + if (!o.canvas) { + o.addTo(canvas); + } + currPos += Math.round(o.width + _settings.margin); + + // Disable moving of buttons + o.lockMovementX = o.lockMovementY = true; + }); + + canvas.renderAll(); + return this; + }; +} + +module.exports = ButtonMenu; diff --git a/src/js/components/ImageEditor/Canvas.js b/src/js/components/ImageEditor/Canvas.js new file mode 100644 index 0000000..585b45e --- /dev/null +++ b/src/js/components/ImageEditor/Canvas.js @@ -0,0 +1,102 @@ +'use strict'; +var util = require('./Util'); +var ApertureHandler = require('./ApertureHandler'); +var ButtonMenu = require('./ButtonMenu'); +var DragbarHandler = require('./DragbarHandler'); +var Viewport = require('./Viewport'); + +var Canvas = fabric.util.createClass(fabric.Canvas, { + initialize: function (id) { + this.callSuper('initialize', id, { + enableRetinaScaling: true, + renderOnAddRemove: false, + stateful: false, + backgroundColor: '#d4d4d4', + preserveObjectStacking: true, + selection: false, + }); + + this.on('mouse:up', function () { + if (this.image.__moved) { + this.aperture.setTransparent(false); + } + $(this.upperCanvasEl).css('cursor', 'default'); + this.renderAll(); + }); + }, + + get_apertures: function () { + return this.__aperturehandler || + (this.__aperturehandler = new ApertureHandler(this)); + }, + + get_dragbars: function () { + return this.__dragbars || + (this.__dragbars = new DragbarHandler(this)); + }, + + /** + * @return {fabric.Image} + */ + get_image: function () { + return this.__image; + }, + + /** + * @return {Viewport} + */ + get_viewport: function () { + return this.__viewport || + (this.__viewport = new Viewport(this)); + }, + + /** + * @return {Canvas} + */ + removeImage: function () { + if (!this.__image) { + return this; + } + return this.remove(this.__image); + }, + + /** + * @param {fabric.Image} img + * @return {Canvas} + */ + setImage: function (img) { + this.removeImage(); + this.__image = img; + this.__image.addTo(this).center().on('mousedown', function () { + img.canvas.aperture.setTransparent(true); + }).setCoords(); + this.viewport.reset(); + return this; + } +}); + +// Define properties +util.setProperties(Canvas.prototype, { + aperture: { + get: Canvas.prototype.get_apertures + }, + dragbars: { + get: Canvas.prototype.get_dragbars + }, + image: { + get: Canvas.prototype.get_image + }, + topMenu: { + get: function () { + if (!this.__topMenu) { + this.__topMenu = new ButtonMenu(this); + } + return this.__topMenu; + } + }, + viewport: { + get: Canvas.prototype.get_viewport + } +}); + +module.exports = Canvas; diff --git a/src/js/components/ImageEditor/DragHandle.js b/src/js/components/ImageEditor/DragHandle.js new file mode 100644 index 0000000..1a68e75 --- /dev/null +++ b/src/js/components/ImageEditor/DragHandle.js @@ -0,0 +1,97 @@ +'use strict'; + +var DragHandle = function (handler) { + var _currentAxis; + var _dragging; + + // Event declarations + var events = { + mouseDown: function () { + _dragging = true; + _currentAxis = this.axis; + handler.canvas.image.trigger('mousedown'); + }, + mouseUp: function () { + _dragging = false; + _currentAxis = null; + handler.canvas.image.trigger('mouseup'); + }, + mouseMove: function (e) { + if (!_dragging) { + return; + } + + if (_currentAxis === 'h') { + handler.canvas.image.left -= e.e.movementX; + } else { + handler.canvas.image.top -= e.e.movementY; + } + // Trigger moving for image, to stay within bounds + handler.canvas.image.setCoords().trigger('moving'); + handler.canvas.renderAll(); + } + }; + + function getRect(text, width) { + var txt = new fabric.Text(text, { + fontFamily: 'breuer', + fontSize: 12, + fill: '#fff', + originX: 'center', + top: 2 + }); + + var rect = new fabric.Rect({ + fill: '#666', + width: width, + height: 20, + originX: 'center', + }); + + return new fabric.Group([rect, txt]).set({ + evented: false, + }); + } + + this.getCanvas = function () { + return handler.canvas; + }; + + this.getHandles = function () { + if (!this.__handles) { + return []; + } + + return [].concat(this.__handles.h, this.__handles.v); + }; + + this.init = function () { + var viewportData = this.getCanvas().viewport.data; + var tracks = handler.getTracks(); + + if (this.__handles) { + this.getCanvas().remove(this.__handles.h); + this.getCanvas().remove(this.__handles.v); + } + + this.__handles = { + h: getRect(viewportData.dim.width + ' cm', viewportData.width), + v: getRect(viewportData.dim.height + ' cm', viewportData.height) + }; + + this.__handles.h.addTo(this.getCanvas()).centerH().set({ top: tracks.h.top }).setCoords(); + this.__handles.v.addTo(this.getCanvas()) + .setAngle(90) + .centerV().set({ left: tracks.v.left + tracks.v.width }) + .setCoords(); + + this.getCanvas() + .on('mouse:move', events.mouseMove) + .on('mouse:up', events.mouseUp); + + tracks.h.on('mousedown', events.mouseDown.bind(tracks.h)); + tracks.v.on('mousedown', events.mouseDown.bind(tracks.v)); + }; +}; + +module.exports = DragHandle; diff --git a/src/js/components/ImageEditor/DragbarHandler.js b/src/js/components/ImageEditor/DragbarHandler.js new file mode 100644 index 0000000..9c2d585 --- /dev/null +++ b/src/js/components/ImageEditor/DragbarHandler.js @@ -0,0 +1,81 @@ +'use strict'; +var DragHandle = require('./DragHandle'); + +function DragbarHandler(canvas) { + var _items = { h: null, v: null }; + + /** + * @return {DragbarHandler} + */ + this.apply = function () { + if (!canvas.__hasDragbars) { + this.init(); + canvas.__hasDragbars = true; + } + this.sync(); + + if (!this.__handles) { + this.__handles = new DragHandle(this); + } + + this.__handles.init(); + + [].concat(_items.h, _items.v).forEach(function (o) { + o.bringToFront(); + }); + + this.__handles.getHandles().forEach(function (o) { + o.bringToFront(); + }); + + canvas.renderAll(); + return this; + }; + + this.canvas = canvas; + + /** + * Removes dragbars from canvas + */ + this.clear = function () { + canvas + .remove(_items.h) + .remove(_items.v) + .renderAll(); + return this; + }; + + this.getTracks = function () { + return _items; + }; + + /** + * @return {DragbarHandler} + */ + this.init = function () { + this.clear(); + + _items.h = new fabric.Rect({ axis: 'h', fill: '#bbb', width: canvas.image.width, height: 20 }) + .addTo(canvas) + .centerH().set({top: canvas.height - 30}).setCoords(); + + _items.v = new fabric.Rect({ axis: 'v', fill: '#bbb', width: 20, height: canvas.image.height }) + .addTo(canvas) + .set({left: canvas.width - 30}).setCoords(); + + canvas.image.on('moving', this.sync); + + return this; + }; + + /** + * @return {DragbarHandler} + */ + this.sync = function () { + _items.h.set({left: canvas.image.left}).setCoords(); + _items.v.set({top: canvas.image.top}).setCoords(); + return this; + }; +} + +module.exports = DragbarHandler; diff --git a/src/js/components/ImageEditor/Draggable.js b/src/js/components/ImageEditor/Draggable.js new file mode 100644 index 0000000..258f72d --- /dev/null +++ b/src/js/components/ImageEditor/Draggable.js @@ -0,0 +1,37 @@ +'use strict'; + +var util = require('./Util'); + +/** + * @param {fabric.Image} img + * @param {fabric.Rect} boundingRect + */ +function Draggable(img, boundingRect) { + var _bounds = boundingRect; + + /** + * Triggers when object is moved + */ + function onMove() { + this.__moved = true; + util.setPositionInside(this, _bounds); + } + + // Attach onMove event to image + img.on('moving', onMove); + + /** + * @param {fabric.Rect} localBoundingRect + * @return {Draggable} + */ + this.enable = function (localBoundingRect) { + if (localBoundingRect) { + _bounds = localBoundingRect; + } + img.lockMovementX = img.lockMovementY = false; + img.selectable = true; + return this; + }; +} + +module.exports = Draggable; diff --git a/src/js/components/ImageEditor/GoreHandler.js b/src/js/components/ImageEditor/GoreHandler.js new file mode 100644 index 0000000..9d2871e --- /dev/null +++ b/src/js/components/ImageEditor/GoreHandler.js @@ -0,0 +1,70 @@ +'use strict'; + +function GoreHandler(viewport) { + var _enabled = false; + var _lines = []; + + /** + * @return {fabric.Line} + */ + function getLine(left) { + return new fabric.Line([0, 0, 0, viewport.data.height], { + evented: false, + strokeDashArray: [5, 5], + stroke: '#000', + strokeWidth: 1, + left: left, + top: viewport.getBounds().top + }); + } + + /** + * @return {GoreHandler} + */ + this.clear = function () { + _lines.forEach(function (o) { + viewport.canvas.remove(o); + }); + _lines = []; + return this; + }; + + /** + * @param {Boolean} enabled + * @return {GoreHandler} + */ + this.enable = function (enabled) { + _enabled = enabled; + this.clear(); + + if (!_enabled) { + viewport.canvas.renderAll(); + return; + } + + var bounds = viewport.getBounds(); + var step = viewport.data.ppmm * 450; + var count = Math.ceil(bounds.width / step); + + for (var i = 1; i < count; i++) { + _lines.push(getLine(bounds.left + step * i)); + } + + _lines.forEach(function (o) { + viewport.canvas.add(o); + }); + + viewport.canvas.renderAll(); + + return this; + }; + + /** + * @return {GoreHandler} + */ + this.reset = function () { + return this.enable(_enabled); + }; +} + +module.exports = GoreHandler; diff --git a/src/js/components/ImageEditor/ImageEditor.js b/src/js/components/ImageEditor/ImageEditor.js new file mode 100644 index 0000000..01cd42a --- /dev/null +++ b/src/js/components/ImageEditor/ImageEditor.js @@ -0,0 +1,110 @@ +'use strict'; + +var Canvas = require('./Canvas'); +var util = require('./Util'); + +function ImageEditor(args) { + var _canvas; + + var _settings = $.extend({ + 'type': 'wallpaper' + }, args); + + // Initialize canvas + _canvas = new Canvas('canvas'); + + _canvas.topMenu + .addItem(['Show Murals Panel', 'Hide Murals Panel'], function () { + _canvas.viewport.gores.enable(this.isActive()); + }) + .addItem(['Show Ruler', 'Hide Ruler'], function () { + _canvas.viewport.rulers.enable(this.isActive()); + }) + .addItem(['Fullscreen', 'Exit Fullscreen'], function () { + util.toggleFullScreen(_canvas.getSelectionElement().parentNode); + }) + .render(); + + /** + * Crops image to desired dimensions + * @param {int} width [description] + * @param {int} height [description] + * @return {ImageEditor} + */ + this.crop = function (width, height) { + _canvas.viewport.set(width, height); + return this; + }; + + /** + * @param {string|object} key + * @param {object} defaultValue + * @return {object} + */ + this.get = function (key, defaultValue) { + var value = _settings[key]; + return typeof value === 'undefined' ? defaultValue : value; + }; + + /** + * @param {Number|String} url + * @param {Function} callback + * @return {ImageEditor} + */ + this.loadImage = function (url, callback) { + callback = typeof callback === 'function' ? callback : function () {}; + fabric.Image.fromURL(url, function (img) { + _canvas.setImage(img); + callback(img); + }); + return this; + }; + + /** + * @param {string|object} key + * @param {object} value + * @return {ImageEditor} + */ + this.set = function (key, value) { + $.extend(_settings, util.assertKeyValuePair(key, value)); + return this; + }; + + util.setProperties(this, { + canvas: { + get: function () { + return _canvas; + } + } + }); +} + +$(function () { + // Inputs for the editor + var $input = { + w: $('#form-control input[name="w"]'), + h: $('#form-control input[name="h"]'), + image_id: $('#form-control input[name="image_id"]'), + mirror: $('#form-control input[name="mirror"]') + }; + + var imageUrl = '//images.photowall.com/products/' + $input.image_id.val() + '.jpg?h=450'; + + var editor = new ImageEditor({ 'type': 'canvas' }) + .loadImage(imageUrl, function () { + $input.w.trigger('keyup'); + }); + + var Event = { + DimensionChange: function () { + editor.crop($input.w.val(), $input.h.val()); + }, + Mirror: function () { + editor.canvas.image.transformation.set('mirror', this.checked); + } + }; + + $input.w.keyup(Event.DimensionChange); + $input.h.keyup(Event.DimensionChange); + $input.mirror.click(Event.Mirror); +}); diff --git a/src/js/components/ImageEditor/ImageTransformationHandler.js b/src/js/components/ImageEditor/ImageTransformationHandler.js new file mode 100644 index 0000000..0b2be17 --- /dev/null +++ b/src/js/components/ImageEditor/ImageTransformationHandler.js @@ -0,0 +1,22 @@ +'use strict'; + +function ImageTransformationHandler(img) { + var _transformations = {}; + + /** + * @param {String} name + * @param {*} args + * @return {ImageTransformationHandler} + */ + this.set = function (name, args) { + switch (name) { + case 'mirror': + _transformations.mirror = (img.flipX = args); + } + img.setCoords().canvas.renderAll(); + + return this; + }; +} + +module.exports = ImageTransformationHandler; diff --git a/src/js/components/ImageEditor/RulerHandler.js b/src/js/components/ImageEditor/RulerHandler.js new file mode 100644 index 0000000..47eed1f --- /dev/null +++ b/src/js/components/ImageEditor/RulerHandler.js @@ -0,0 +1,119 @@ +'use strict'; +var util = require('./Util'); +var TextButton = require('./TextButton'); + +function RulerHandler(viewport) { + var _enabled = false; + var _lines = { h: null, v: null }; + var _rect = { h: null, v: null }; + + var Event = { + /** + * Triggers when handlebar is moved + */ + onMove: function () { + var axis = this.axis; + var bounds = viewport.getBounds(); + var pos = util.assertBetween(this[axis], bounds.min[axis], bounds.max[axis]); + this.marker[axis] = this[axis] = pos; + var dim = (this[axis] - bounds[axis]) / (viewport.data.ppmm * 10); + + if (axis === 'left') { + dim = viewport.data.dim.width - dim; + } + + if (axis === 'top') { + dim = viewport.data.dim.height - dim; + } + + this.setText(Math.round(dim * 100) / 100 + ' cm'); + } + }; + + /** + * @return {fabric.Line} + */ + function getLine(axis) { + var bounds = viewport.getBounds(); + return new fabric.Line([ + 0, 0, + axis === 'h' ? bounds.width : 0, + axis === 'v' ? bounds.height : 0 + ], { + evented: false, + strokeDashArray: [5, 5], + stroke: '#000', + strokeWidth: 1, + top: axis === 'h' ? bounds.top + bounds.height / 2 : bounds.top, + left: axis === 'v' ? bounds.left + bounds.width / 2 : bounds.left + }); + } + + /** + * @return {RulerHandler} + */ + this.clear = function () { + [].concat(_rect.h, _rect.v, _lines.h, _lines.v).forEach(function (o) { + viewport.canvas.remove(o); + }); + return this; + }; + + /** + * @param {Boolean} enabled + * @return {RulerHandler} + */ + this.enable = function (enabled) { + _enabled = enabled; + this.clear(); + + if (!_enabled) { + viewport.canvas.renderAll(); + return; + } + + var bounds = viewport.getBounds(); + + _lines = { h: getLine('h'), v: getLine('v') }; + + viewport.canvas.add(_lines.h); + viewport.canvas.add(_lines.v); + + var textSettings = { + width: 75, + height: 20, + color: '#3f3', + }; + + _rect.h = new TextButton('H', textSettings).set({ + axis: 'left', + marker: _lines.v, + top: Math.ceil(bounds.top + bounds.height + 5), + left: _lines.v.left, + originX: 'center', + lockMovementY: true, + }).addTo(viewport.canvas).on('moving', Event.onMove).trigger('moving'); + + _rect.v = new TextButton('V', textSettings).set({ + axis: 'top', + marker: _lines.h, + top: _lines.h.top, + left: Math.ceil(bounds.left + bounds.width + 5), + originY: 'center', + lockMovementX: true, + }).addTo(viewport.canvas).on('moving', Event.onMove).trigger('moving'); + + viewport.canvas.renderAll(); + + return this; + }; + + /** + * @return {RulerHandler} + */ + this.reset = function () { + return this.enable(_enabled); + }; +} + +module.exports = RulerHandler; diff --git a/src/js/components/ImageEditor/TextButton.js b/src/js/components/ImageEditor/TextButton.js new file mode 100644 index 0000000..761ff0d --- /dev/null +++ b/src/js/components/ImageEditor/TextButton.js @@ -0,0 +1,120 @@ +'use strict'; + +var defaults = { + activeBackground: '#000', + background: '#000', + color: '#fff', + fontFamily: 'breuer', + fontSize: 12, + padding: 10, +}; + +/** + * @param {String} text + * @param {Object} settings + */ +function TextButton(text, settings) { + fabric.Group.call(this); + + var _callback = function () {}; + + var _rect = null; + var _text = null; + + settings = $.extend({}, defaults, settings); + + settings.text = Array.isArray(text) ? text[0] : text; + settings.textActive = Array.isArray(text) ? text[1] : text; + + var _rectSettings = { + fill: settings.background, + width: settings.width, + height: settings.height, + originX: 'center', + originY: 'center', + }; + + var _textSettings = { + fill: settings.color, + fontFamily: settings.fontFamily, + fontSize: settings.fontSize, + originX: 'center', + originY: 'center', + }; + + // If width or height is omitted, the size for this button needs to be + // calculated when setting Text. + // An initial value for width and height needs to be set, else we won't + // render correctly. + if ((settings.dynamicW = !settings.width)) { + _rectSettings.width = 1; + } + + if ((settings.dynamicH = !settings.height)) { + _rectSettings.height = 1; + } + + // Make sure we're interactive + this.selectable = true; + + this.hoverCursor = 'default'; + + // Initialize text and rectangle + _text = new fabric.Text('', _textSettings); + _rect = new fabric.Rect(_rectSettings); + this.addWithUpdate(_rect) + .addWithUpdate(_text); + + var Event = { + mouseDown: function (e) { + this.__active = !this.__active; + _rect.setFill(this.__active ? settings.activeBackground : settings.background); + this.setText(settings[this.isActive() ? 'textActive' : 'text']); + _callback.call(this, e); + + if (this.__group) { + this.__group.render(); + } + } + }; + + /** + * @return {Boolean} + */ + this.isActive = function () { + return this.__active; + }; + + this.onClick = function (callback) { + _callback = callback || function () {}; + this.on('mousedown', Event.mouseDown); + return this; + }; + + /** + * @param {String} text + * @return {TextButton} + */ + this.setText = function (text) { + _text.setText((text || '').toString()).setCoords(); + + if (settings.dynamicW) { + this.width = _rect.width = _text.width + settings.padding; + } + + if (settings.dynamicH) { + this.height = _rect.height = _text.height + settings.padding; + } + + return this.setCoords(); + }; + + if (settings.text) { + this.setText(settings.text); + } +} + +TextButton.prototype = Object.create(fabric.Group.prototype); +TextButton.prototype.constructor = TextButton; + +module.exports = TextButton; diff --git a/src/js/components/ImageEditor/Util.js b/src/js/components/ImageEditor/Util.js new file mode 100644 index 0000000..aea888f --- /dev/null +++ b/src/js/components/ImageEditor/Util.js @@ -0,0 +1,124 @@ +/* jshint -W097 */ +'use strict'; + +module.exports = { + assertKeyValuePair: function (key, value) { + if (typeof key === 'object') { + return key; + } + + var returnValue = {}; + returnValue[key] = value; + + return returnValue; + }, + + assertBetween: function (value, min, max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; + }, + + /** + * @param {Integer} width + * @param {Integer} height + * @return {Object} + * axis: STRING, + * dim: OBJECT( width: NUMBER, height: NUMBER), + * width: NUMBER, + * height: NUMBER, + * ppmm: NUMBER + */ + calcCrop: function (obj, width, height) { + var cropRatio = width / height; + var objRatio = obj.width / obj.height; + + var returnValue = { + image: obj, + axis: objRatio > cropRatio ? 'x' : 'y', + dim: { width: width, height: height }, + width: obj.width, + height: obj.height + }; + + if (objRatio > cropRatio) { + returnValue.width *= cropRatio / objRatio; + } else { + returnValue.height /= cropRatio / objRatio; + } + + // Pixels per milimeter + returnValue.ppmm = returnValue.width / (returnValue.dim.width * 10); + + return returnValue; + }, + + /** + * @param {fabric.Object} obj + * @param {Object} bounds + * @return {fabric.Object} + */ + setPositionInside: function (obj, bounds) { + ['left', 'top'].forEach(function (v) { + if (obj[v] < bounds[v].min) { + obj.set(v, bounds[v].min).setCoords(); + } + + if (obj[v] > bounds[v].max) { + obj.set(v, bounds[v].max).setCoords(); + } + }); + return obj; + }, + + /** + * @param {Object} obj + * @param {Object} properties + */ + setProperties: function (obj, properties) { + for (var o in properties) { + if (properties.hasOwnProperty(o)) { + Object.defineProperty(obj, o, properties[o]); + } + } + }, + + /** + * Toggles the full screen state for passed DOM object + * @param {HTMLElement} elm + */ + toggleFullScreen: function (elm) { + var isFullScreen = + document.fullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + document.msFullscreenElement; + + if (isFullScreen) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + return; + } + + if(elm.requestFullScreen) { + elm.requestFullScreen(); + } + else if(elm.webkitRequestFullScreen) { + elm.webkitRequestFullScreen(); + } + else if(elm.mozRequestFullScreen) { + elm.mozRequestFullScreen(); + } + } +}; diff --git a/src/js/components/ImageEditor/Viewport.js b/src/js/components/ImageEditor/Viewport.js new file mode 100644 index 0000000..715f2e0 --- /dev/null +++ b/src/js/components/ImageEditor/Viewport.js @@ -0,0 +1,148 @@ +'use strict'; + +var GoreHandler = require('./GoreHandler'); +var RulerHandler = require('./RulerHandler'); +var util = require('./Util'); + +function Viewport(canvas) { + var _beams = []; + var _borderWidth = 2; + var _data = null; + var _rect; + + /** + * Applies viewport to page + */ + function apply() { + canvas.remove(_rect); + + _rect = new fabric.Rect({ + evented: false, + fill: 'transparent', + width: _data.width, + height: _data.height, + stroke: '#fff', + strokeWidth: _borderWidth, + }); + + _rect.width += _borderWidth; + _rect.height += _borderWidth; + _rect.addTo(canvas).center().setCoords(); + + canvas.image + .center() + .drag.enable(calcMinMaxBoundsForRect(_rect.getInsideBoundingRect(), canvas.image)); + canvas.aperture.apply(_data.axis, _rect.getBoundingRect()); + + drawBeams(); + canvas.dragbars.apply(); + + // Apertures sometimes overlaps the viewports bounding rect. + // Solve this by bringing it to the front after apertures are applied. + _rect.bringToFront(); + } + + + /** + * Render the diagonal lines from viewport to edge of the canvas + */ + function drawBeams() { + _beams.forEach(function (o) { + canvas.remove(o); + }); + + _beams = []; + + // TOP LEFT + _beams.push(new fabric.Line( + [_rect.left, _rect.top, _rect.left - _rect.top, -1], { + stroke: '#fff', + strokeWidth: 2, + } + )); + + // TOP RIGHT + _beams.push(_beams[0].clone().set({ flipX: true, left: _rect.left + _rect.width })); + // BOTTOM RIGHT + _beams.push(_beams[1].clone().set({ flipY: true, top: _rect.top + _rect.height })); + // BOTTOM LEFT + _beams.push(_beams[0].clone().set({ flipY: true, top: _rect.top + _rect.height })); + + _beams.forEach(function (o) { + canvas.add(o); + o.bringToFront(); + }); + } + + /** + * Gets an object with min / max values for each axis, based on the bounds + * of passed fabric.Rect + * @param {fabric.Rect} rect + * @return {Object} + */ + function calcMinMaxBoundsForRect(rect, img) { + return { + left: { + min: rect.left + rect.width - img.width, + max: rect.left + }, + top: { + min: rect.top + rect.height - img.height, + max: rect.top + } + }; + } + + this.canvas = canvas; + + this.getBounds = function () { + return _rect.getInsideBoundingRect(); + }; + + /** + * @return {Viewport} + */ + this.reset = function () { + if (_data) { + return this.set(_data.dim.width, _data.dim.height); + } + return this; + }; + + /** + * @param {Number} width + * @param {Number} height + * @return {Viewport} + */ + this.set = function (width, height) { + _data = util.calcCrop(canvas.image, width, height); + apply(); + this.gores.reset(); + this.rulers.reset(); + canvas.aperture.setTransparent(true); + canvas.image.__moved = false; + canvas.topMenu.bringToFront(); + }; + + util.setProperties(this, { + data: { + get: function () { + return _data; + } + }, + gores: { + get: function () { + return this.__goreHandler || + (this.__goreHandler = new GoreHandler(this)); + } + }, + rulers: { + get: function () { + return this.__rulerHandler || + (this.__rulerHandler = new RulerHandler(this)); + } + } + }); +} + +module.exports = Viewport; diff --git a/src/js/components/ImageEditor/fabric.ext/ObjectDefaults.js b/src/js/components/ImageEditor/fabric.ext/ObjectDefaults.js new file mode 100644 index 0000000..fc62591 --- /dev/null +++ b/src/js/components/ImageEditor/fabric.ext/ObjectDefaults.js @@ -0,0 +1,61 @@ +'use strict'; + +var Draggable = require('../Draggable'); +var ImageTransformationHandler = require('../ImageTransformationHandler'); +var util = require('../Util'); + +$.extend(fabric.Object.prototype, { + cornerSize: 0, + hasBorders: false, + hasControls: false, + padding: 0, + selectable: false, + strokeWidth: 0, + addTo: function (canvas) { + canvas.add(this); + return this; + }, +}); + +util.setProperties(fabric.Image.prototype, { + drag: { + get: function () { + return this.__draggable || (this.__draggable = new Draggable(this)); + } + }, + + transformation: { + get: function () { + return this.__ImageTransformationHandler || + (this.__ImageTransformationHandler = new ImageTransformationHandler(this)); + } + } +}); + +$.extend(fabric.Rect.prototype, { + /** + * Gets the inner bounds of this fabric.Rect + * @return {Object} + */ + getInsideBoundingRect: function () { + var bounds = this.getBoundingRect(); + var returnValue = { + left: bounds.left + this.strokeWidth, + top: bounds.top + this.strokeWidth, + width: bounds.width - this.strokeWidth * 2, + height: bounds.height - this.strokeWidth * 2 + }; + + returnValue.min = { + left: returnValue.left, + top: returnValue.top + }; + + returnValue.max = { + left: returnValue.left + returnValue.width, + top: returnValue.top + returnValue.height + }; + + return returnValue; + } +}); diff --git a/src/js/components/ImageEditor/settings/TopMenu.js b/src/js/components/ImageEditor/settings/TopMenu.js new file mode 100644 index 0000000..a9886e3 --- /dev/null +++ b/src/js/components/ImageEditor/settings/TopMenu.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + activeBackground: '#000', + background: '#444', + fontSize: 14, + height: 30, + padding: 20 +}; diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 0000000..32655ca --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,7 @@ +(function () { + 'use strict'; + + require('./components/ImageEditor/fabric.ext/ObjectDefaults'); + //require('./components/ImageEditor/fabric.ext/BaseEventHandlers'); + require('./components/ImageEditor/ImageEditor'); +}());