From 88b589b3d60846aa92b4b097b85594c735746430 Mon Sep 17 00:00:00 2001 From: Tom French Date: Fri, 8 Nov 2013 15:54:55 +0000 Subject: [PATCH] Implement zoom for brushes, various zoom fixes, remove canvasBorder. --- dist/all.js | 2540 +++++++++++++++++-- dist/all.require.js | 2842 +++++++++++++++++++--- src/brushes/circle_brush.class.js | 1 + src/brushes/pencil_brush.class.js | 4 + src/brushes/spray_brush.class.js | 13 +- src/canvas.class.js | 21 +- src/mixins/canvas_events.mixin.js | 15 +- src/mixins/object_geometry.mixin.js | 95 +- src/mixins/object_interactivity.mixin.js | 17 +- src/shapes/group.class.js | 24 +- src/shapes/object.class.js | 6 +- src/static_canvas.class.js | 41 +- 12 files changed, 5063 insertions(+), 556 deletions(-) diff --git a/dist/all.js b/dist/all.js index 3ef9f0ae..3179ebb1 100644 --- a/dist/all.js +++ b/dist/all.js @@ -1,4 +1,4 @@ -/* build: `node build.js modules=ALL exclude=gestures` */ +/* build: `node build.js modules=ALL` */ /*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */ var fabric = fabric || { version: "1.3.7" }; @@ -1749,6 +1749,1912 @@ if (!JSON) { } }()); +/* + ---------------------------------------------------- + Event.js : 1.1.1 : 2012/11/19 : MIT License + ---------------------------------------------------- + https://github.com/mudcube/Event.js + ---------------------------------------------------- + 1 : click, dblclick, dbltap + 1+ : tap, longpress, drag, swipe + 2+ : pinch, rotate + : mousewheel, devicemotion, shake + ---------------------------------------------------- + TODO + ---------------------------------------------------- + * switch configuration to 4th argument on addEventListener + * bbox calculation for elements scaled with transform. + ---------------------------------------------------- + NOTES + ---------------------------------------------------- + * When using other libraries that may have built in "Event" namespace, + i.e. Typescript, you can use "eventjs" instead of "Event" for all example calls. + ---------------------------------------------------- + REQUIREMENTS: querySelector, querySelectorAll + ---------------------------------------------------- + * There are two ways to add/remove events with this library. + ---------------------------------------------------- + // Retains "this" attribute as target, and overrides native addEventListener. + target.addEventListener(type, listener, useCapture); + target.removeEventListener(type, listener, useCapture); + + // Attempts to perform as fast as possible. + Event.add(type, listener, configure); + Event.remove(type, listener, configure); + + * You can turn prototyping on/off for individual features. + ---------------------------------------------------- + Event.modifyEventListener = true; // add custom *EventListener commands to HTMLElements. + Event.modifySelectors = true; // add bulk *EventListener commands on NodeLists from querySelectorAll and others. + + * Example of setting up a single listener with a custom configuration. + ---------------------------------------------------- + // optional configuration. + var configure = { + fingers: 2, // listen for specifically two fingers. + snap: 90 // snap to 90 degree intervals. + }; + // adding with addEventListener() + target.addEventListener("swipe", function(event) { + // additional variables can be found on the event object. + console.log(event.velocity, event.angle, event.fingers); + }, configure); + + // adding with Event.add() + Event.add("swipe", function(event, self) { + // additional variables can be found on the self object. + console.log(self.velocity, self.angle, self.fingers); + }, configure); + + * Multiple listeners glued together. + ---------------------------------------------------- + // adding with addEventListener() + target.addEventListener("click swipe", function(event) { }); + + // adding with Event.add() + Event.add(target, "click swipe", function(event, self) { }); + + * Use query selectors to create an event (querySelectorAll) + ---------------------------------------------------- + // adding events to NodeList from querySelectorAll() + document.querySelectorAll("#element a.link").addEventListener("click", callback); + + // adding with Event.add() + Event.add("#element a.link", "click", callback); + + * Listen for selector to become available (querySelector) + ---------------------------------------------------- + Event.add("body", "ready", callback); + // or... + Event.add({ + target: "body", + type: "ready", + timeout: 10000, // set a timeout to stop checking. + interval: 30, // set how often to check for element. + listener: callback + }); + + * Multiple listeners bound to one callback w/ single configuration. + ---------------------------------------------------- + var bindings = Event.add({ + target: target, + type: "click swipe", + snap: 90, // snap to 90 degree intervals. + minFingers: 2, // minimum required fingers to start event. + maxFingers: 4, // maximum fingers in one event. + listener: function(event, self) { + console.log(self.gesture); // will be click or swipe. + console.log(self.x); + console.log(self.y); + console.log(self.identifier); + console.log(self.start); + console.log(self.fingers); // somewhere between "2" and "4". + self.pause(); // disable event. + self.resume(); // enable event. + self.remove(); // remove event. + } + }); + + * Multiple listeners bound to multiple callbacks w/ single configuration. + ---------------------------------------------------- + var bindings = Event.add({ + target: target, + minFingers: 1, + maxFingers: 12, + listeners: { + click: function(event, self) { + self.remove(); // removes this click listener. + }, + swipe: function(event, self) { + binding.remove(); // removes both the click + swipe listeners. + } + } + }); + + * Multiple listeners bound to multiple callbacks w/ multiple configurations. + ---------------------------------------------------- + var binding = Event.add({ + target: target, + listeners: { + longpress: { + fingers: 1, + wait: 500, // milliseconds + listener: function(event, self) { + console.log(self.fingers); // "1" finger. + } + }, + drag: { + fingers: 3, + position: "relative", // "relative", "absolute", "difference", "move" + listener: function(event, self) { + console.log(self.fingers); // "3" fingers. + console.log(self.x); // coordinate is relative to edge of target. + } + } + } + }); + + * Capturing an event and manually forwarding it to a proxy (tiered events). + ---------------------------------------------------- + Event.add(target, "down", function(event, self) { + var x = event.pageX; // local variables that wont change. + var y = event.pageY; + Event.proxy.drag({ + event: event, + target: target, + listener: function(event, self) { + console.log(x - event.pageX); // measure movement. + console.log(y - event.pageY); + } + }); + }); + ---------------------------------------------------- + + * Event proxies. + * type, fingers, state, start, x, y, position, bbox + * rotation, scale, velocity, angle, delay, timeout + ---------------------------------------------------- + // "Click" :: fingers, minFingers, maxFingers. + Event.add(window, "click", function(event, self) { + console.log(self.gesture, self.x, self.y); + }); + // "Double-Click" :: fingers, minFingers, maxFingers. + Event.add(window, "dblclick", function(event, self) { + console.log(self.gesture, self.x, self.y); + }); + // "Drag" :: fingers, maxFingers, position + Event.add(window, "drag", function(event, self) { + console.log(self.gesture, self.fingers, self.state, self.start, self.x, self.y, self.bbox); + }); + // "Gesture" :: fingers, minFingers, maxFingers. + Event.add(window, "gesture", function(event, self) { + console.log(self.gesture, self.fingers, self.state, self.rotation, self.scale); + }); + // "Swipe" :: fingers, minFingers, maxFingers, snap, threshold. + Event.add(window, "swipe", function(event, self) { + console.log(self.gesture, self.fingers, self.velocity, self.angle, self.start, self.x, self.y); + }); + // "Tap" :: fingers, minFingers, maxFingers, timeout. + Event.add(window, "tap", function(event, self) { + console.log(self.gesture, self.fingers); + }); + // "Longpress" :: fingers, minFingers, maxFingers, delay. + Event.add(window, "longpress", function(event, self) { + console.log(self.gesture, self.fingers); + }); + // + Event.add(window, "shake", function(event, self) { + console.log(self.gesture, self.acceleration, self.accelerationIncludingGravity); + }); + // + Event.add(window, "devicemotion", function(event, self) { + console.log(self.gesture, self.acceleration, self.accelerationIncludingGravity); + }); + // + Event.add(window, "wheel", function(event, self) { + console.log(self.gesture, self.state, self.wheelDelta); + }); + + * Stop, prevent and cancel. + ---------------------------------------------------- + Event.stop(event); // stop bubble. + Event.prevent(event); // prevent default. + Event.cancel(event); // stop and prevent. + + * Track for proper command/control-key for Mac/PC. + ---------------------------------------------------- + Event.add(window, "keyup keydown", Event.proxy.metaTracker); + console.log(Event.proxy.metaKey); + + * Test for event features, in this example Drag & Drop file support. + ---------------------------------------------------- + console.log(Event.supports('dragstart') && Event.supports('drop') && !!window.FileReader); + + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(eventjs) === "undefined") + var eventjs = Event; + +Event = (function(root) { + "use strict"; + +// Add custom *EventListener commands to HTMLElements. + root.modifyEventListener = false; + +// Add bulk *EventListener commands on NodeLists from querySelectorAll and others. + root.modifySelectors = false; + +// Event maintenance. + root.add = function(target, type, listener, configure) { + return eventManager(target, type, listener, configure, "add"); + }; + + root.remove = function(target, type, listener, configure) { + return eventManager(target, type, listener, configure, "remove"); + }; + + root.stop = function(event) { + if (event.stopPropagation) + event.stopPropagation(); + event.cancelBubble = true; // <= IE8 + event.bubble = 0; + }; + + root.prevent = function(event) { + if (event.preventDefault) + event.preventDefault(); + event.returnValue = false; // <= IE8 + }; + + root.cancel = function(event) { + root.stop(event); + root.prevent(event); + }; + +// Check whether event is natively supported (via @kangax) + root.supports = function(target, type) { + if (typeof(target) === "string") { + type = target; + target = window; + } + type = "on" + type; + if (type in target) + return true; + if (!target.setAttribute) + target = document.createElement("div"); + if (target.setAttribute && target.removeAttribute) { + target.setAttribute(type, ""); + var isSupported = typeof target[type] === "function"; + if (typeof target[type] !== "undefined") + target[type] = null; + target.removeAttribute(type); + return isSupported; + } + }; + + var clone = function(obj) { + if (!obj || typeof (obj) !== 'object') + return obj; + var temp = new obj.constructor(); + for (var key in obj) { + if (!obj[key] || typeof (obj[key]) !== 'object') { + temp[key] = obj[key]; + } else { // clone sub-object + temp[key] = clone(obj[key]); + } + } + return temp; + }; + +/// Handle custom *EventListener commands. + var eventManager = function(target, type, listener, configure, trigger, fromOverwrite) { + configure = configure || {}; + // Check for element to load on interval (before onload). + if (typeof(target) === "string" && type === "ready") { + var time = (new Date()).getTime(); + var timeout = configure.timeout; + var ms = configure.interval || 1000 / 60; + var interval = window.setInterval(function() { + if ((new Date()).getTime() - time > timeout) { + window.clearInterval(interval); + } + if (document.querySelector(target)) { + window.clearInterval(interval); + listener(); + } + }, ms); + return; + } + // Get DOM element from Query Selector. + if (typeof(target) === "string") { + target = document.querySelectorAll(target); + if (target.length === 0) + return createError("Missing target on listener!"); // No results. + if (target.length === 1) { // Single target. + target = target[0]; + } + } + /// Handle multiple targets. + var event; + var events = {}; + if (target.length > 0) { + for (var n0 = 0, length0 = target.length; n0 < length0; n0++) { + event = eventManager(target[n0], type, listener, clone(configure), trigger); + if (event) + events[n0] = event; + } + return createBatchCommands(events); + } + // Check for multiple events in one string. + if (type.indexOf && type.indexOf(" ") !== -1) + type = type.split(" "); + if (type.indexOf && type.indexOf(",") !== -1) + type = type.split(","); + // Attach or remove multiple events associated with a target. + if (typeof(type) !== "string") { // Has multiple events. + if (typeof(type.length) === "number") { // Handle multiple listeners glued together. + for (var n1 = 0, length1 = type.length; n1 < length1; n1++) { // Array [type] + event = eventManager(target, type[n1], listener, clone(configure), trigger); + if (event) + events[type[n1]] = event; + } + } else { // Handle multiple listeners. + for (var key in type) { // Object {type} + if (typeof(type[key]) === "function") { // without configuration. + event = eventManager(target, key, type[key], clone(configure), trigger); + } else { // with configuration. + event = eventManager(target, key, type[key].listener, clone(type[key]), trigger); + } + if (event) + events[key] = event; + } + } + return createBatchCommands(events); + } + // Ensure listener is a function. + if (typeof(listener) !== "function") + return createError("Listener is not a function!"); + // Generate a unique wrapper identifier. + var useCapture = configure.useCapture || false; + var id = normalize(type) + getID(target) + "." + getID(listener) + "." + (useCapture ? 1 : 0); + // Handle the event. + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // Fire custom event. + if (trigger === "remove") { // Remove event listener. + if (!wrappers[id]) + return; // Already removed. + wrappers[id].remove(); + delete wrappers[id]; + } else if (trigger === "add") { // Attach event listener. + if (wrappers[id]) + return wrappers[id]; // Already attached. + // Retains "this" orientation. + if (configure.useCall && !root.modifyEventListener) { + var tmp = listener; + listener = function(event, self) { + for (var key in self) + event[key] = self[key]; + return tmp.call(target, event); + }; + } + // Create listener proxy. + configure.gesture = type; + configure.target = target; + configure.listener = listener; + configure.fromOverwrite = fromOverwrite; + // Record wrapper. + wrappers[id] = root.proxy[type](configure); + } + } else { // Fire native event. + type = normalize(type); + if (trigger === "remove") { // Remove event listener. + if (!wrappers[id]) + return; // Already removed. + target[remove](type, listener, useCapture); + delete wrappers[id]; + } else if (trigger === "add") { // Attach event listener. + if (wrappers[id]) + return wrappers[id]; // Already attached. + target[add](type, listener, useCapture); + // Record wrapper. + wrappers[id] = { + type: type, + target: target, + listener: listener, + remove: function() { + root.remove(target, type, listener, configure); + } + }; + } + } + return wrappers[id]; + }; + +/// Perform batch actions on multiple events. + var createBatchCommands = function(events) { + return { + remove: function() { // Remove multiple events. + for (var key in events) { + events[key].remove(); + } + }, + add: function() { // Add multiple events. + for (var key in events) { + events[key].add(); + } + } + }; + }; + +/// Display error message in console. + var createError = function(message) { + if (typeof(console) === "undefined") + return; + if (typeof(console.error) === "undefined") + return; + console.error(message); + }; + +/// Handle naming discrepancies between platforms. + var normalize = (function() { + var translate = {}; + return function(type) { + if (!root.pointerType) { + if (window.navigator.msPointerEnabled) { + root.pointerType = "mspointer"; + translate = { + "mousedown": "MSPointerDown", + "mousemove": "MSPointerMove", + "mouseup": "MSPointerUp" + }; + } else if (root.supports("touchstart")) { + root.pointerType = "touch"; + translate = { + "mousedown": "touchstart", + "mouseup": "touchend", + "mousemove": "touchmove" + }; + } else { + root.pointerType = "mouse"; + } + } + if (translate[type]) + type = translate[type]; + if (!document.addEventListener) { // IE + return "on" + type; + } else { + return type; + } + }; + })(); + +/// Event wrappers to keep track of all events placed in the window. + var wrappers = {}; + var counter = 0; + var getID = function(object) { + if (object === window) + return "#window"; + if (object === document) + return "#document"; + if (!object) + return createError("Missing target on listener!"); + if (!object.uniqueID) + object.uniqueID = "id" + counter++; + return object.uniqueID; + }; + +/// Detect platforms native *EventListener command. + var add = document.addEventListener ? "addEventListener" : "attachEvent"; + var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; + + /* + Pointer.js + ------------------------ + Modified from; https://github.com/borismus/pointer.js + */ + + root.createPointerEvent = function(event, self, preventRecord) { + var eventName = self.gesture; + var target = self.target; + var pts = event.changedTouches || root.proxy.getCoords(event); + if (pts.length) { + var pt = pts[0]; + self.pointers = preventRecord ? [] : pts; + self.pageX = pt.pageX; + self.pageY = pt.pageY; + self.x = self.pageX; + self.y = self.pageY; + } + /// + var newEvent = document.createEvent("Event"); + newEvent.initEvent(eventName, true, true); + newEvent.originalEvent = event; + for (var k in self) { + if (k === "target") + continue; + newEvent[k] = self[k]; + } + target.dispatchEvent(newEvent); + }; + +/// Allows *EventListener to use custom event proxies. + if (root.modifyEventListener && window.HTMLElement) + (function() { + var augmentEventListener = function(proto) { + var recall = function(trigger) { // overwrite native *EventListener's + var handle = trigger + "EventListener"; + var handler = proto[handle]; + proto[handle] = function(type, listener, useCapture) { + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // capture custom events. + var configure = useCapture; + if (typeof(useCapture) === "object") { + configure.useCall = true; + } else { // convert to configuration object. + configure = { + useCall: true, + useCapture: useCapture + }; + } + eventManager(this, type, listener, configure, trigger, true); + handler.call(this, type, listener, useCapture); + } else { // use native function. + handler.call(this, normalize(type), listener, useCapture); + } + }; + }; + recall("add"); + recall("remove"); + }; + // NOTE: overwriting HTMLElement doesn't do anything in Firefox. + if (navigator.userAgent.match(/Firefox/)) { + // TODO: fix Firefox for the general case. + augmentEventListener(HTMLDivElement.prototype); + augmentEventListener(HTMLCanvasElement.prototype); + } else { + augmentEventListener(HTMLElement.prototype); + } + augmentEventListener(document); + augmentEventListener(window); + })(); + +/// Allows querySelectorAll and other NodeLists to perform *EventListener commands in bulk. + if (root.modifySelectors) + (function() { + var proto = NodeList.prototype; + proto.removeEventListener = function(type, listener, useCapture) { + for (var n = 0, length = this.length; n < length; n++) { + this[n].removeEventListener(type, listener, useCapture); + } + }; + proto.addEventListener = function(type, listener, useCapture) { + for (var n = 0, length = this.length; n < length; n++) { + this[n].addEventListener(type, listener, useCapture); + } + }; + })(); + + return root; + +})(Event); +/* + ---------------------------------------------------- + Event.proxy : 0.4.2 : 2012/07/29 : MIT License + ---------------------------------------------------- + https://github.com/mudcube/Event.js + ---------------------------------------------------- + Pointer Gestures + ---------------------------------------------------- + 1 : click, dblclick, dbltap + 1+ : tap, taphold, drag, swipe + 2+ : pinch, rotate + ---------------------------------------------------- + Gyroscope Gestures + ---------------------------------------------------- + * shake + ---------------------------------------------------- + Fixes issues with + ---------------------------------------------------- + * mousewheel-Firefox uses DOMMouseScroll and does not return wheelDelta. + * devicemotion-Fixes issue where event.acceleration is not returned. + ---------------------------------------------------- + Ideas for the future + ---------------------------------------------------- + * Keyboard, GamePad, and other input abstractions. + * Event batching - i.e. for every x fingers down a new gesture is created. + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + /* + Create a new pointer gesture instance. + */ + + root.pointerSetup = function(conf, self) { + /// Configure. + conf.doc = conf.target.ownerDocument || conf.target; // Associated document. + conf.minFingers = conf.minFingers || conf.fingers || 1; // Minimum required fingers. + conf.maxFingers = conf.maxFingers || conf.fingers || Infinity; // Maximum allowed fingers. + conf.position = conf.position || "relative"; // Determines what coordinate system points are returned. + delete conf.fingers; //- + /// Convenience data. + self = self || {}; + self.gesture = conf.gesture; + self.target = conf.target; + self.pointerType = Event.pointerType; + /// + if (Event.modifyEventListener && conf.fromOverwrite) + conf.listener = Event.createPointerEvent; + /// Convenience commands. + var fingers = 0; + var type = self.gesture.indexOf("pointer") === 0 && Event.modifyEventListener ? "pointer" : "mouse"; + self.listener = conf.listener; + self.proxy = function(listener) { + self.defaultListener = conf.listener; + conf.listener = listener; + listener(conf.event, self); + }; + self.remove = function() { + if (conf.onPointerDown) + Event.remove(conf.target, type + "down", conf.onPointerDown); + if (conf.onPointerMove) + Event.remove(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp) + Event.remove(conf.doc, type + "up", conf.onPointerUp); + }; + self.resume = function(opt) { + if (conf.onPointerMove && (!opt || opt.move)) + Event.add(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp && (!opt || opt.move)) + Event.add(conf.doc, type + "up", conf.onPointerUp); + conf.fingers = fingers; + }; + self.pause = function(opt) { + fingers = conf.fingers; + if (conf.onPointerMove && (!opt || opt.move)) + Event.remove(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp && (!opt || opt.up)) + Event.remove(conf.doc, type + "up", conf.onPointerUp); + conf.fingers = 0; + }; + /// + return self; + }; + + /* + Begin proxied pointer command. + */ + + root.pointerStart = function(event, self, conf) { + var addTouchStart = function(touch, sid) { + var bbox = conf.bbox; + var pt = track[sid] = {}; + /// + switch (conf.position) { + case "absolute": // Absolute from within window. + pt.offsetX = 0; + pt.offsetY = 0; + break; + case "difference": // Relative from origin. + pt.offsetX = touch.pageX; + pt.offsetY = touch.pageY; + break; + case "move": // Move target element. + pt.offsetX = touch.pageX - bbox.x1; + pt.offsetY = touch.pageY - bbox.y1; + break; + default: // Relative from within target. + pt.offsetX = bbox.x1; + pt.offsetY = bbox.y1; + break; + } + /// + if (conf.position === "relative") { + var x = (touch.pageX + bbox.scrollLeft - pt.offsetX) * bbox.scaleX; + var y = (touch.pageY + bbox.scrollTop - pt.offsetY) * bbox.scaleY; + } else { + var x = (touch.pageX - pt.offsetX); + var y = (touch.pageY - pt.offsetY); + } + /// + pt.rotation = 0; + pt.scale = 1; + pt.startTime = pt.moveTime = (new Date).getTime(); + pt.move = {x: x, y: y}; + pt.start = {x: x, y: y}; + /// + conf.fingers++; + }; + /// + conf.event = event; + if (self.defaultListener) { + conf.listener = self.defaultListener; + delete self.defaultListener; + } + /// + var isTouchStart = !conf.fingers; + var track = conf.tracker; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Adding touch events to tracking. + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; // Touch ID. + // Track the current state of the touches. + if (conf.fingers) { + if (conf.fingers >= conf.maxFingers) { + var ids = []; + for (var sid in conf.tracker) + ids.push(sid); + self.identifier = ids.join(","); + return isTouchStart; + } + var fingers = 0; // Finger ID. + for (var rid in track) { + // Replace removed finger. + if (track[rid].up) { + delete track[rid]; + addTouchStart(touch, sid); + conf.cancel = true; + break; + } + fingers++; + } + // Add additional finger. + if (track[sid]) + continue; + addTouchStart(touch, sid); + } else { // Start tracking fingers. + track = conf.tracker = {}; + self.bbox = conf.bbox = root.getBoundingBox(conf.target); + conf.fingers = 0; + conf.cancel = false; + addTouchStart(touch, sid); + } + } + /// + var ids = []; + for (var sid in conf.tracker) + ids.push(sid); + self.identifier = ids.join(","); + /// + return isTouchStart; + }; + + /* + End proxied pointer command. + */ + + root.pointerEnd = function(event, self, conf, onPointerUp) { + // Record changed touches have ended (iOS changedTouches is not reliable). + var touches = event.touches || []; + var length = touches.length; + var exists = {}; + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var sid = touch.identifier; + exists[sid || Infinity] = true; + } + for (var sid in conf.tracker) { + var track = conf.tracker[sid]; + if (exists[sid] || track.up) + continue; + if (onPointerUp) { // add changedTouches to mouse. + onPointerUp({ + pageX: track.pageX, + pageY: track.pageY, + changedTouches: [{ + pageX: track.pageX, + pageY: track.pageY, + identifier: sid === "Infinity" ? Infinity : sid + }] + }, "up"); + } + track.up = true; + conf.fingers--; + } + /* // This should work but fails in Safari on iOS4 so not using it. + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Record changed touches have ended (this should work). + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var track = conf.tracker[sid]; + if (track && !track.up) { + if (onPointerUp) { // add changedTouches to mouse. + onPointerUp({ + changedTouches: [{ + pageX: track.pageX, + pageY: track.pageY, + identifier: sid === "Infinity" ? Infinity : sid + }] + }, "up"); + } + track.up = true; + conf.fingers --; + } + } */ + // Wait for all fingers to be released. + if (conf.fingers !== 0) + return false; + // Record total number of fingers gesture used. + var ids = []; + conf.gestureFingers = 0; + for (var sid in conf.tracker) { + conf.gestureFingers++; + ids.push(sid); + } + self.identifier = ids.join(","); + // Our pointer gesture has ended. + return true; + }; + + /* + Returns mouse coords in an array to match event.*Touches + ------------------------------------------------------------ + var touch = event.changedTouches || root.getCoords(event); + */ + + root.getCoords = function(event) { + if (typeof(event.pageX) !== "undefined") { // Desktop browsers. + root.getCoords = function(event) { + return Array({ + type: "mouse", + x: event.pageX, + y: event.pageY, + pageX: event.pageX, + pageY: event.pageY, + identifier: Infinity + }); + }; + } else { // Internet Explorer <= 8.0 + root.getCoords = function(event) { + event = event || window.event; + return Array({ + type: "mouse", + x: event.clientX + document.documentElement.scrollLeft, + y: event.clientY + document.documentElement.scrollTop, + pageX: event.clientX + document.documentElement.scrollLeft, + pageY: event.clientY + document.documentElement.scrollTop, + identifier: Infinity + }); + }; + } + return root.getCoords(event); + }; + + /* + Returns single coords in an object. + ------------------------------------------------------------ + var mouse = root.getCoord(event); + */ + + root.getCoord = function(event) { + if ("ontouchstart" in window) { // Mobile browsers. + var pX = 0; + var pY = 0; + root.getCoord = function(event) { + var touches = event.changedTouches; + if (touches.length) { // ontouchstart + ontouchmove + return { + x: pX = touches[0].pageX, + y: pY = touches[0].pageY + }; + } else { // ontouchend + return { + x: pX, + y: pY + }; + } + }; + } else if (typeof(event.pageX) !== "undefined" && typeof(event.pageY) !== "undefined") { // Desktop browsers. + root.getCoord = function(event) { + return { + x: event.pageX, + y: event.pageY + }; + }; + } else { // Internet Explorer <=8.0 + root.getCoord = function(event) { + event = event || window.event; + return { + x: event.clientX + document.documentElement.scrollLeft, + y: event.clientY + document.documentElement.scrollTop + }; + }; + } + return root.getCoord(event); + }; + + /* + Get target scale and position in space. + */ + + root.getBoundingBox = function(o) { + if (o === window || o === document) + o = document.body; + /// + var bbox = { + x1: 0, + y1: 0, + x2: 0, + y2: 0, + scrollLeft: 0, + scrollTop: 0 + }; + /// + if (o === document.body) { + bbox.height = window.innerHeight; + bbox.width = window.innerWidth; + } else { + bbox.height = o.offsetHeight; + bbox.width = o.offsetWidth; + } + /// Get the scale of the element. + bbox.scaleX = o.width / bbox.width || 1; + bbox.scaleY = o.height / bbox.height || 1; + /// Get the offset of element. + var tmp = o; + while (tmp !== null) { + bbox.x1 += tmp.offsetLeft; + bbox.y1 += tmp.offsetTop; + tmp = tmp.offsetParent; + } + ; + /// Get the scroll of container element. + var tmp = o.parentNode; + while (tmp !== null) { + if (tmp === document.body) + break; + if (tmp.scrollTop === undefined) + break; + bbox.scrollLeft += tmp.scrollLeft; + bbox.scrollTop += tmp.scrollTop; + tmp = tmp.parentNode; + } + ; + /// Record the extent of box. + bbox.x2 = bbox.x1 + bbox.width; + bbox.y2 = bbox.y1 + bbox.height; + /// + return bbox; + }; + + /* + Keep track of metaKey, the proper ctrlKey for users platform. + */ + + (function() { + var agent = navigator.userAgent.toLowerCase(); + var mac = agent.indexOf("macintosh") !== -1; + if (mac && agent.indexOf("khtml") !== -1) { // chrome, safari. + var watch = {91: true, 93: true}; + } else if (mac && agent.indexOf("firefox") !== -1) { // mac firefox. + var watch = {224: true}; + } else { // windows, linux, or mac opera. + var watch = {17: true}; + } + root.isMetaKey = function(event) { + return !!watch[event.keyCode]; + }; + root.metaTracker = function(event) { + if (watch[event.keyCode]) { + root.metaKey = event.type === "keydown"; + } + }; + })(); + + return root; + +})(Event.proxy); +/* + "Click" event proxy. + ---------------------------------------------------- + Event.add(window, "click", function(event, self) {}); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.click = function(conf) { + conf.maxFingers = conf.maxFingers || conf.fingers || 1; + // Setting up local variables. + var EVENT; + // Tracking the events. + conf.onPointerDown = function(event) { + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function(event) { + EVENT = event; + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + if (EVENT.cancelBubble && ++EVENT.bubble > 1) + return; + var pointers = EVENT.changedTouches || root.getCoords(EVENT); + var pointer = pointers[0]; + var bbox = conf.bbox; + var newbbox = root.getBoundingBox(conf.target); + if (conf.position === "relative") { + var ax = (pointer.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; + var ay = (pointer.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; + } else { + var ax = (pointer.pageX - bbox.x1); + var ay = (pointer.pageY - bbox.y1); + } + if (ax > 0 && ax < bbox.width && // Within target coordinates. + ay > 0 && ay < bbox.height && + bbox.scrollTop === newbbox.scrollTop) { + conf.listener(EVENT, self); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + self.state = "click"; + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.click = root.click; + + return root; + +})(Event.proxy); +/* + "Double-Click" aka "Double-Tap" event proxy. + ---------------------------------------------------- + Event.add(window, "dblclick", function(event, self) {}); + ---------------------------------------------------- + Touch an target twice for <= 700ms, with less than 25 pixel drift. + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.dbltap = + root.dblclick = function(conf) { + conf.maxFingers = conf.maxFingers || conf.fingers || 1; + // Setting up local variables. + var delay = 700; // in milliseconds + var time0, time1, timeout; + var pointer0, pointer1; + // Tracking the events. + conf.onPointerDown = function(event) { + var pointers = event.changedTouches || root.getCoords(event); + if (time0 && !time1) { // Click #2 + pointer1 = pointers[0]; + time1 = (new Date).getTime() - time0; + } else { // Click #1 + pointer0 = pointers[0]; + time0 = (new Date).getTime(); + time1 = 0; + clearTimeout(timeout); + timeout = setTimeout(function() { + time0 = 0; + }, delay); + } + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function(event) { + if (time0 && !time1) { + var pointers = event.changedTouches || root.getCoords(event); + pointer1 = pointers[0]; + } + var bbox = conf.bbox; + if (conf.position === "relative") { + var ax = (pointer1.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; + var ay = (pointer1.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; + } else { + var ax = (pointer1.pageX - bbox.x1); + var ay = (pointer1.pageY - bbox.y1); + } + if (!(ax > 0 && ax < bbox.width && // Within target coordinates.. + ay > 0 && ay < bbox.height && + Math.abs(pointer1.pageX - pointer0.pageX) <= 25 && // Within drift deviance. + Math.abs(pointer1.pageY - pointer0.pageY) <= 25)) { + // Cancel out this listener. + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + clearTimeout(timeout); + time0 = time1 = 0; + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + if (time0 && time1) { + if (time1 <= delay && !(event.cancelBubble && ++event.bubble > 1)) { + self.state = conf.gesture; + conf.listener(event, self); + } + clearTimeout(timeout); + time0 = time1 = 0; + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + self.state = "dblclick"; + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.dbltap = root.dbltap; + Event.Gesture._gestureHandlers.dblclick = root.dblclick; + + return root; + +})(Event.proxy); +/* + "Drag" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: maxFingers, position. + ---------------------------------------------------- + Event.add(window, "drag", function(event, self) { + console.log(self.gesture, self.state, self.start, self.x, self.y, self.bbox); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.dragElement = function(that, event) { + root.drag({ + event: event, + target: that, + position: "move", + listener: function(event, self) { + that.style.left = self.x + "px"; + that.style.top = self.y + "px"; + Event.prevent(event); + } + }); + }; + + root.drag = function(conf) { + conf.gesture = "drag"; + conf.onPointerDown = function(event) { + if (root.pointerStart(event, self, conf)) { + if (!conf.monitor) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + } + // Process event listener. + conf.onPointerMove(event, "down"); + }; + conf.onPointerMove = function(event, state) { + if (!conf.tracker) + return conf.onPointerDown(event); + var bbox = conf.bbox; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var identifier = touch.identifier || Infinity; + var pt = conf.tracker[identifier]; + // Identifier defined outside of listener. + if (!pt) + continue; + pt.pageX = touch.pageX; + pt.pageY = touch.pageY; + // Record data. + self.state = state || "move"; + self.identifier = identifier; + self.start = pt.start; + self.fingers = conf.fingers; + if (conf.position === "relative") { + self.x = (pt.pageX + bbox.scrollLeft - pt.offsetX) * bbox.scaleX; + self.y = (pt.pageY + bbox.scrollTop - pt.offsetY) * bbox.scaleY; + } else { + self.x = (pt.pageX - pt.offsetX); + self.y = (pt.pageY - pt.offsetY); + } + /// + conf.listener(event, self); + } + }; + conf.onPointerUp = function(event) { + // Remove tracking for touch. + if (root.pointerEnd(event, self, conf, conf.onPointerMove)) { + if (!conf.monitor) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + if (conf.event) { + conf.onPointerDown(conf.event); + } else { // + Event.add(conf.target, "mousedown", conf.onPointerDown); + if (conf.monitor) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + } + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.drag = root.drag; + + return root; + +})(Event.proxy); +/* + "Gesture" event proxy (2+ fingers). + ---------------------------------------------------- + CONFIGURE: minFingers, maxFingers. + ---------------------------------------------------- + Event.add(window, "gesture", function(event, self) { + console.log(self.rotation, self.scale, self.fingers, self.state); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + var RAD_DEG = Math.PI / 180; + + root.gesture = function(conf) { + conf.minFingers = conf.minFingers || conf.fingers || 2; + // Tracking the events. + conf.onPointerDown = function(event) { + var fingers = conf.fingers; + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + // Record gesture start. + if (conf.fingers === conf.minFingers && fingers !== conf.fingers) { + self.fingers = conf.minFingers; + self.scale = 1; + self.rotation = 0; + self.state = "start"; + var sids = ""; //- FIXME(mud): can generate duplicate IDs. + for (var key in conf.tracker) + sids += key; + self.identifier = parseInt(sids); + conf.listener(event, self); + } + }; + /// + conf.onPointerMove = function(event, state) { + var bbox = conf.bbox; + var points = conf.tracker; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Update tracker coordinates. + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var pt = points[sid]; + // Check whether "pt" is used by another gesture. + if (!pt) + continue; + // Find the actual coordinates. + if (conf.position === "relative") { + pt.move.x = (touch.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; + pt.move.y = (touch.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; + } else { + pt.move.x = (touch.pageX - bbox.x1); + pt.move.y = (touch.pageY - bbox.y1); + } + } + /// + if (conf.fingers < conf.minFingers) + return; + /// + var touches = []; + var scale = 0; + var rotation = 0; + /// Calculate centroid of gesture. + var centroidx = 0; + var centroidy = 0; + var length = 0; + for (var sid in points) { + var touch = points[sid]; + if (touch.up) + continue; + centroidx += touch.move.x; + centroidy += touch.move.y; + length++; + } + centroidx /= length; + centroidy /= length; + /// + for (var sid in points) { + var touch = points[sid]; + if (touch.up) + continue; + var start = touch.start; + if (!start.distance) { + var dx = start.x - centroidx; + var dy = start.y - centroidy; + start.distance = Math.sqrt(dx * dx + dy * dy); + start.angle = Math.atan2(dx, dy) / RAD_DEG; + } + // Calculate scale. + var dx = touch.move.x - centroidx; + var dy = touch.move.y - centroidy; + var distance = Math.sqrt(dx * dx + dy * dy); + scale += distance / start.distance; + // Calculate rotation. + var angle = Math.atan2(dx, dy) / RAD_DEG; + var rotate = (start.angle - angle + 360) % 360 - 180; + touch.DEG2 = touch.DEG1; // Previous degree. + touch.DEG1 = rotate > 0 ? rotate : -rotate; // Current degree. + if (typeof(touch.DEG2) !== "undefined") { + if (rotate > 0) { + touch.rotation += touch.DEG1 - touch.DEG2; + } else { + touch.rotation -= touch.DEG1 - touch.DEG2; + } + rotation += touch.rotation; + } + // Attach current points to self. + touches.push(touch.move); + } + /// + self.touches = touches; + self.fingers = conf.fingers; + self.scale = scale / conf.fingers; + self.rotation = rotation / conf.fingers; + self.state = "change"; + conf.listener(event, self); + }; + conf.onPointerUp = function(event) { + // Remove tracking for touch. + var fingers = conf.fingers; + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + // Check whether fingers has dropped below minFingers. + if (fingers === conf.minFingers && conf.fingers < conf.minFingers) { + self.fingers = conf.fingers; + self.state = "end"; + conf.listener(event, self); + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.gesture = root.gesture; + + return root; + +})(Event.proxy); +/* + "Pointer" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: minFingers, maxFingers. + ---------------------------------------------------- + Event.add(window, "gesture", function(event, self) { + console.log(self.rotation, self.scale, self.fingers, self.state); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.pointerdown = + root.pointermove = + root.pointerup = function(conf) { + if (conf.target.isPointerEmitter) + return; + // Tracking the events. + var isDown = true; + conf.onPointerDown = function(event) { + isDown = false; + self.gesture = "pointerdown"; + conf.listener(event, self); + }; + conf.onPointerMove = function(event) { + self.gesture = "pointermove"; + conf.listener(event, self, isDown); + }; + conf.onPointerUp = function(event) { + isDown = true; + self.gesture = "pointerup"; + conf.listener(event, self, true); + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + Event.add(conf.target, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + // Return this object. + conf.target.isPointerEmitter = true; + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.pointerdown = root.pointerdown; + Event.Gesture._gestureHandlers.pointermove = root.pointermove; + Event.Gesture._gestureHandlers.pointerup = root.pointerup; + + return root; + +})(Event.proxy); +/* + "Device Motion" and "Shake" event proxy. + ---------------------------------------------------- + http://developer.android.com/reference/android/hardware/SensorEvent.html#values + ---------------------------------------------------- + Event.add(window, "shake", function(event, self) {}); + Event.add(window, "devicemotion", function(event, self) { + console.log(self.acceleration, self.accelerationIncludingGravity); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.shake = function(conf) { + // Externally accessible data. + var self = { + gesture: "devicemotion", + acceleration: {}, + accelerationIncludingGravity: {}, + target: conf.target, + listener: conf.listener, + remove: function() { + window.removeEventListener('devicemotion', onDeviceMotion, false); + } + }; + // Setting up local variables. + var threshold = 4; // Gravitational threshold. + var timeout = 1000; // Timeout between shake events. + var timeframe = 200; // Time between shakes. + var shakes = 3; // Minimum shakes to trigger event. + var lastShake = (new Date).getTime(); + var gravity = {x: 0, y: 0, z: 0}; + var delta = { + x: {count: 0, value: 0}, + y: {count: 0, value: 0}, + z: {count: 0, value: 0} + }; + // Tracking the events. + var onDeviceMotion = function(e) { + var alpha = 0.8; // Low pass filter. + var o = e.accelerationIncludingGravity; + gravity.x = alpha * gravity.x + (1 - alpha) * o.x; + gravity.y = alpha * gravity.y + (1 - alpha) * o.y; + gravity.z = alpha * gravity.z + (1 - alpha) * o.z; + self.accelerationIncludingGravity = gravity; + self.acceleration.x = o.x - gravity.x; + self.acceleration.y = o.y - gravity.y; + self.acceleration.z = o.z - gravity.z; + /// + if (conf.gesture === "devicemotion") { + conf.listener(e, self); + return; + } + var data = "xyz"; + var now = (new Date).getTime(); + for (var n = 0, length = data.length; n < length; n++) { + var letter = data[n]; + var ACCELERATION = self.acceleration[letter]; + var DELTA = delta[letter]; + var abs = Math.abs(ACCELERATION); + /// Check whether another shake event was recently registered. + if (now - lastShake < timeout) + continue; + /// Check whether delta surpasses threshold. + if (abs > threshold) { + var idx = now * ACCELERATION / abs; + var span = Math.abs(idx + DELTA.value); + // Check whether last delta was registered within timeframe. + if (DELTA.value && span < timeframe) { + DELTA.value = idx; + DELTA.count++; + // Check whether delta count has enough shakes. + if (DELTA.count === shakes) { + conf.listener(e, self); + // Reset tracking. + lastShake = now; + DELTA.value = 0; + DELTA.count = 0; + } + } else { + // Track first shake. + DELTA.value = idx; + DELTA.count = 1; + } + } + } + }; + // Attach events. + if (!window.addEventListener) + return; + window.addEventListener('devicemotion', onDeviceMotion, false); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.shake = root.shake; + + return root; + +})(Event.proxy); +/* + "Swipe" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: snap, threshold, maxFingers. + ---------------------------------------------------- + Event.add(window, "swipe", function(event, self) { + console.log(self.velocity, self.angle); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + var RAD_DEG = Math.PI / 180; + + root.swipe = function(conf) { + conf.snap = conf.snap || 90; // angle snap. + conf.threshold = conf.threshold || 1; // velocity threshold. + // Tracking the events. + conf.onPointerDown = function(event) { + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function(event) { + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var o = conf.tracker[sid]; + // Identifier defined outside of listener. + if (!o) + continue; + o.move.x = touch.pageX; + o.move.y = touch.pageY; + o.moveTime = (new Date).getTime(); + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + /// + var velocity1; + var velocity2 + var degree1; + var degree2; + /// Calculate centroid of gesture. + var start = {x: 0, y: 0}; + var endx = 0; + var endy = 0; + var length = 0; + /// + for (var sid in conf.tracker) { + var touch = conf.tracker[sid]; + var xdist = touch.move.x - touch.start.x; + var ydist = touch.move.y - touch.start.y; + + endx += touch.move.x; + endy += touch.move.y; + start.x += touch.start.x; + start.y += touch.start.y; + length++; + + + var distance = Math.sqrt(xdist * xdist + ydist * ydist); + var ms = touch.moveTime - touch.startTime; + var degree2 = Math.atan2(xdist, ydist) / RAD_DEG + 180; + var velocity2 = ms ? distance / ms : 0; + if (typeof(degree1) === "undefined") { + degree1 = degree2; + velocity1 = velocity2; + } else if (Math.abs(degree2 - degree1) <= 20) { + degree1 = (degree1 + degree2) / 2; + velocity1 = (velocity1 + velocity2) / 2; + } else { + return; + } + } + /// + if (velocity1 > conf.threshold) { + start.x /= length; + start.y /= length; + self.start = start; + self.x = endx / length; + self.y = endy / length; + self.angle = -((((degree1 / conf.snap + 0.5) >> 0) * conf.snap || 360) - 360); + self.velocity = velocity1; + self.fingers = conf.gestureFingers; + self.state = "swipe"; + conf.listener(event, self); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.swipe = root.swipe; + + return root; + +})(Event.proxy); +/* + "Tap" and "Longpress" event proxy. + ---------------------------------------------------- + CONFIGURE: delay (longpress), timeout (tap). + ---------------------------------------------------- + Event.add(window, "tap", function(event, self) { + console.log(self.fingers); + }); + ---------------------------------------------------- + multi-finger tap // touch an target for <= 250ms. + multi-finger longpress // touch an target for >= 500ms + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.tap = + root.longpress = function(conf) { + conf.delay = conf.delay || 500; + conf.timeout = conf.timeout || 250; + // Setting up local variables. + var timestamp, timeout; + // Tracking the events. + conf.onPointerDown = function(event) { + if (root.pointerStart(event, self, conf)) { + timestamp = (new Date).getTime(); + // Initialize event listeners. + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + // Make sure this is a "longpress" event. + if (conf.gesture !== "longpress") + return; + timeout = setTimeout(function() { + if (event.cancelBubble && ++event.bubble > 1) + return; + // Make sure no fingers have been changed. + var fingers = 0; + for (var key in conf.tracker) { + if (conf.tracker[key].end === true) + return; + if (conf.cancel) + return; + fingers++; + } + // Send callback. + self.state = "start"; + self.fingers = fingers; + conf.listener(event, self); + }, conf.delay); + } + }; + conf.onPointerMove = function(event) { + var bbox = conf.bbox; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var identifier = touch.identifier || Infinity; + var pt = conf.tracker[identifier]; + if (!pt) + continue; + if (conf.position === "relative") { + var x = (touch.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; + var y = (touch.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; + } else { + var x = (touch.pageX - bbox.x1); + var y = (touch.pageY - bbox.y1); + } + if (!(x > 0 && x < bbox.width && // Within target coordinates.. + y > 0 && y < bbox.height && + Math.abs(x - pt.start.x) <= 25 && // Within drift deviance. + Math.abs(y - pt.start.y) <= 25)) { + // Cancel out this listener. + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + conf.cancel = true; + return; + } + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + clearTimeout(timeout); + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + if (event.cancelBubble && ++event.bubble > 1) + return; + // Callback release on longpress. + if (conf.gesture === "longpress") { + if (self.state === "start") { + self.state = "end"; + conf.listener(event, self); + } + return; + } + // Cancel event due to movement. + if (conf.cancel) + return; + // Ensure delay is within margins. + if ((new Date).getTime() - timestamp > conf.timeout) + return; + // Send callback. + self.state = "tap"; + self.fingers = conf.gestureFingers; + conf.listener(event, self); + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.tap = root.tap; + Event.Gesture._gestureHandlers.longpress = root.longpress; + + return root; + +})(Event.proxy); +/* + "Mouse Wheel" event proxy. + ---------------------------------------------------- + Event.add(window, "wheel", function(event, self) { + console.log(self.state, self.wheelDelta); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.wheel = function(conf) { + // Configure event listener. + var interval; + var timeout = conf.timeout || 150; + var count = 0; + // Externally accessible data. + var self = { + gesture: "wheel", + state: "start", + wheelDelta: 0, + target: conf.target, + listener: conf.listener, + remove: function() { + conf.target[remove](type, onMouseWheel, false); + } + }; + // Tracking the events. + var onMouseWheel = function(event) { + event = event || window.event; + self.state = count++ ? "change" : "start"; + self.wheelDelta = event.detail ? event.detail * -20 : event.wheelDelta; + conf.listener(event, self); + clearTimeout(interval); + interval = setTimeout(function() { + count = 0; + self.state = "end"; + self.wheelDelta = 0; + conf.listener(event, self); + }, timeout); + }; + // Attach events. + var add = document.addEventListener ? "addEventListener" : "attachEvent"; + var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; + var type = Event.supports("mousewheel") ? "mousewheel" : "DOMMouseScroll"; + conf.target[add](type, onMouseWheel, false); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.wheel = root.wheel; + + return root; + +})(Event.proxy); + + /** * Wrapper around `console.log` (when available) * @param {Any} values Values to log @@ -2097,7 +4003,46 @@ fabric.Collection = { return new fabric.Point(rx, ry).addEquals(origin); } + + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + function transformPoint(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[1] * p.y, + t[2] * p.x + t[3] * p.y + ); + } + return new fabric.Point( + t[0] * p.x + t[1] * p.y + t[4], + t[2] * p.x + t[3] * p.y + t[5] + ); + } + /** + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + function invertTransform(t) { + var r = t.slice(), + a = 1 / (t[0] * t[3] - t[1] * t[2]); + r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; + var o = transformPoint({x: t[4], y: t[5]}, r); + r[4] = -o.x; + r[5] = -o.y; + return r + } + /** * A wrapper around Number#toFixed, which contrary to native method returns number, not string. * @static @@ -2535,6 +4480,8 @@ fabric.Collection = { fabric.util.degreesToRadians = degreesToRadians; fabric.util.radiansToDegrees = radiansToDegrees; fabric.util.rotatePoint = rotatePoint; + fabric.util.transformPoint = transformPoint; + fabric.util.invertTransform = invertTransform; fabric.util.toFixed = toFixed; fabric.util.getRandomInt = getRandomInt; fabric.util.falseFunction = falseFunction; @@ -6711,6 +8658,13 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ allowTouchScrolling: false, + /** + * The transformation (in the format of Canvas transform) which focuses the viewport + * @type Array + * @default + */ + viewportTransform: [1, 0, 0, 1, 0, 0], + /** * Callback; invoked right before object is about to be scaled/rotated * @param {fabric.Object} target Object that's about to be scaled/rotated @@ -6999,6 +8953,73 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ return this; }, + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom: function () { + return sqrt(this.viewportTransform[0] * this.viewportTransform[3]); + }, + + /** + * Returns point at center of viewport + * @return {fabric.Point} the top left corner of the viewport + */ + getViewportCenter: function () { + var wh = fabric.util.transformPoint( + new fabric.Point(this.getWidth(), this.getHeight()), + this.viewportTransform + ), + x = this.viewportTransform[4], + y = this.viewportTransform[5]; + + return new fabric.Point(this.getWidth()/2 + x, this.getHeight()/2 + y); + }, + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom: function (value) { + // TODO: just change the scale, preserve other transformations + this.viewportTransform[0] = value; + this.viewportTransform[3] = value; + this.renderAll(); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].setCoords(); + } + return this; + }, + + /** + * Centers viewport of this canvas instance on given point + * @param {Numer} x value for center of viewport + * @param {Numer} y value for center of viewport + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportCenter: function (x, y) { + var wh = fabric.util.transformPoint( + new fabric.Point(this.getWidth(), this.getHeight()), + this.viewportTransform + ); + this.viewportTransform[4] = x - wh.x/2; + this.viewportTransform[5] = y - wh.y/2; + this.renderAll(); + return this; + }, + + /** + * Centers viewport of this canvas instance + * @return {fabric.Canvas} instance + * @chainable true + */ + centerViewport: function () { + return this.setViewportCenter(this.getWidth()/2, this.getHeight()/2); + }, + /** * Returns <canvas> element corresponding to this instance * @return {HTMLCanvasElement} @@ -7050,8 +9071,14 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ _onObjectAdded: function(obj) { this.stateful && obj.setupState(); - obj.setCoords(); obj.canvas = this; + obj.setCoords(); + if (obj._objects) { + for (var i = 0, len = obj._objects.length; i < len; i++) { + obj._objects[i].canvas = this; + obj._objects[i].setCoords(); + } + } this.fire('object:added', { target: obj }); obj.fire('added'); }, @@ -7959,6 +9986,10 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype * @private */ _getSVGPathData: function() { + var ivt = fabric.util.invertTransform(this.canvas.viewportTransform); + for (var i = 0, len = this._points.length; i < len; i++) { + this._points[i] = fabric.util.transformPoint(this._points[i], ivt); + } this.box = this.getPathBoundingBox(this._points); return this.convertPointsToSVGPath( this._points, this.box.minx, this.box.maxx, this.box.miny, this.box.maxy); @@ -8168,6 +10199,7 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric circles.push(circle); } var group = new fabric.Group(circles); + group.canvas = this.canvas; this.canvas.add(group); this.canvas.fire('path:created', { path: group }); @@ -8314,6 +10346,8 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric } var group = new fabric.Group(rects); + group.canvas = this.canvas; + this.canvas.add(group); this.canvas.fire('path:created', { path: group }); @@ -8349,9 +10383,13 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric var ctx = this.canvas.contextTop; ctx.fillStyle = this.color; ctx.save(); + var ivt = fabric.util.invertTransform(this.canvas.viewportTransform); for (var i = 0, len = this.sprayChunkPoints.length; i < len; i++) { var point = this.sprayChunkPoints[i]; + var tpoint = fabric.util.transformPoint({x: point.x, y: point.y}, ivt); + point.x = tpoint.x; + point.y = tpoint.y; if (typeof point.opacity !== 'undefined') { ctx.globalAlpha = point.opacity; } @@ -8367,6 +10405,7 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric this.sprayChunkPoints = [ ]; var x, y, width, radius = this.width / 2; + var vpt = this.canvas.viewportTransform; for (var i = 0; i < this.density; i++) { @@ -8382,8 +10421,10 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric else { width = this.dotWidth; } - - var point = { x: x, y: y, width: width }; + + var point = new fabric.Point(x, y); + point = fabric.util.transformPoint(point, vpt); + point.width = width if (this.randomOpacity) { point.opacity = fabric.util.getRandomInt(0, 100) / 100; @@ -8705,7 +10746,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @return {Boolean} true if point is contained within an area of given object */ containsPoint: function (e, target) { - var pointer = this.getPointer(e), + var pointer = this.getPointer(e, true), xy = this._normalizePointer(target, pointer); // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html @@ -8719,7 +10760,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _normalizePointer: function (object, pointer) { var activeGroup = this.getActiveGroup(), x = pointer.x, - y = pointer.y; + y = pointer.y, + lt; var isObjectInGroup = ( activeGroup && @@ -8728,8 +10770,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab ); if (isObjectInGroup) { - x -= activeGroup.left; - y -= activeGroup.top; + lt = new fabric.Point(activeGroup.left, activeGroup.top); + lt = fabric.util.transformPoint(lt, this.viewportTransform, true); + x -= lt.x; + y -= lt.y; } return { x: x, y: y }; }, @@ -8838,7 +10882,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var action = 'drag', corner, - pointer = getPointer(e, target.canvas.upperCanvasEl); + pointer = fabric.util.transformPoint( + getPointer(e, this.upperCanvasEl), + fabric.util.invertTransform(this.viewportTransform) + ); corner = target._findTargetCorner(e, this._offset); if (corner) { @@ -8954,6 +11001,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target); var group = new fabric.Group( isActiveLower ? [ target, this._activeObject ] : [ this._activeObject, target ]); + group.canvas = this; this.setActiveGroup(group); this._activeObject = null; @@ -9233,6 +11281,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } else if (group.length > 1) { group = new fabric.Group(group.reverse()); + group.canvas = this; this.setActiveGroup(group); group.saveCoords(); this.fire('selection:created', { target: group }); @@ -9249,7 +11298,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this.skipTargetFind) return; var target, - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); if (this.controlsAboveOverlay && this.lastRenderedObjectWithControlsAboveOverlay && @@ -9289,7 +11338,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } for (var j = 0, len = possibleTargets.length; j < len; j++) { - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); var isTransparent = this.isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); if (!isTransparent) { target = possibleTargets[j]; @@ -9306,8 +11355,18 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e * @return {Object} object with "x" and "y" number values */ - getPointer: function (e) { - var pointer = getPointer(e, this.upperCanvasEl); + getPointer: function (e, ignoreZoom, upperCanvasEl) { + if (!upperCanvasEl) { + upperCanvasEl = this.upperCanvasEl; + } + var pointer = getPointer(e, upperCanvasEl); + if (!ignoreZoom) { + pointer = fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + } + return { x: pointer.x - this._offset.left, y: pointer.y - this._offset.top @@ -9753,7 +11812,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } else { - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); } render = this._shouldRender(target, pointer); @@ -9801,7 +11860,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this.clipTo) { fabric.util.clipContext(this, this.contextTop); } - this.freeDrawingBrush.onMouseDown(this.getPointer(e)); + this.freeDrawingBrush.onMouseDown(this.getPointer(e, true)); this.fire('mouse:down', { e: e }); }, @@ -9827,7 +11886,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this._currentTransform) return; var target = this.findTarget(e), - pointer = this.getPointer(e), + pointer = this.getPointer(e, true), corner, render; @@ -9928,8 +11987,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this.isDrawingMode) { if (this._isCurrentlyDrawing) { - pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseMove(pointer); + this.freeDrawingBrush.onMouseMove(this.getPointer(e, true)); } this.upperCanvasEl.style.cursor = this.freeDrawingCursor; this.fire('mouse:move', { e: e }); @@ -9940,10 +11998,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab // We initially clicked in an empty area, so we draw a box for multiple selection. if (groupSelector) { - pointer = getPointer(e, this.upperCanvasEl); + pointer = this.getPointer(e, true); - groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; - groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; + groupSelector.left = pointer.x - groupSelector.ex; + groupSelector.top = pointer.y - groupSelector.ey; this.renderTop(); } else if (!this._currentTransform) { @@ -9968,7 +12026,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } else { // object is being transformed (scaled/rotated/moved/etc.) - pointer = getPointer(e, this.upperCanvasEl); + pointer = fabric.util.transformPoint( + getPointer(e, this.upperCanvasEl), + fabric.util.invertTransform(this.viewportTransform) + ); var x = pointer.x, y = pointer.y, @@ -10523,6 +12584,83 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }); +(function() { + + var degreesToRadians = fabric.util.degreesToRadians, + radiansToDegrees = fabric.util.radiansToDegrees; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * Method that defines actions when an Event.js gesture is detected on an object. Currently only supports + * 2 finger gestures. + * + * @param e Event object by Event.js + * @param self Event proxy object by Event.js + */ + __onTransformGesture: function(e, self) { + + if (this.isDrawingMode || e.touches.length !== 2 || 'gesture' !== self.gesture) { + return; + } + + var target = this.findTarget(e); + if ('undefined' !== typeof target) { + this.onBeforeScaleRotate(target); + this._rotateObjectByAngle(self.rotation); + this._scaleObjectBy(self.scale); + } + + this.fire('touch:gesture', {target: target, e: e, self: self}); + }, + + /** + * Scales an object by a factor + * @param s {Number} The scale factor to apply to the current scale level + * @param by {String} Either 'x' or 'y' - specifies dimension constraint by which to scale an object. + * When not provided, an object is scaled by both dimensions equally + */ + _scaleObjectBy: function(s, by) { + var t = this._currentTransform, + target = t.target; + + var lockScalingX = target.get('lockScalingX'), + lockScalingY = target.get('lockScalingY'); + + if (lockScalingX && lockScalingY) return; + + target._scaling = true; + + if (!by) { + if (!lockScalingX) { + target.set('scaleX', t.scaleX * s); + } + if (!lockScalingY) { + target.set('scaleY', t.scaleY * s); + } + } + else if (by === 'x' && !target.get('lockUniScaling')) { + lockScalingX || target.set('scaleX', t.scaleX * s); + } + else if (by === 'y' && !target.get('lockUniScaling')) { + lockScalingY || target.set('scaleY', t.scaleY * s); + } + }, + + /** + * Rotates object by an angle + * @param curAngle {Number} the angle of rotation in degrees + */ + _rotateObjectByAngle: function(curAngle) { + var t = this._currentTransform; + + if (t.target.get('lockRotation')) return; + t.target.angle = radiansToDegrees(degreesToRadians(curAngle) + t.theta); + } + }); +})(); + + (function(global) { "use strict"; @@ -11255,6 +13393,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {Boolean} fromLeft When true, context is transformed to object's top/left corner. This is used when rendering text on Node */ transform: function(ctx, fromLeft) { + if (this.group) { + this.group.transform(ctx, fromLeft); + } ctx.globalAlpha = this.opacity; var center = fromLeft ? this._getLeftTopCoords() : this.getCenterPoint(); @@ -11537,6 +13678,16 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ctx.save(); var m = this.transformMatrix; + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + if (m && !this.group) { ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); } @@ -11571,8 +13722,23 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this._render(ctx, noTransform); this.clipTo && ctx.restore(); this._removeShadow(ctx); + ctx.restore(); + ctx.save(); if (this.active && !noTransform) { + var center; + if (this.group) { + center = fabric.util.transformPoint(this.group.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.group.angle)); + } + center = fabric.util.transformPoint(this.getCenterPoint(), v, null != this.group); + if (this.group) { + center.x *= this.group.scaleX; + center.y *= this.group.scaleY; + } + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -12558,10 +14724,21 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, padding = this.padding, - theta = degreesToRadians(this.angle); + theta = degreesToRadians(this.angle), + vpt; + if (this.canvas) { + vpt = this.canvas.viewportTransform; + } + if (!vpt) { // TODO + vpt = [1, 0, 0, 1, 0, 0]; + }; - this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; - this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; + var f = function (p) { + return fabric.util.transformPoint(p, vpt); + } + + this.currentWidth = (this.width + strokeWidth) * this.scaleX; + this.currentHeight = (this.height + strokeWidth) * this.scaleY; // If width is negative, make postive. Fixes path selection issue if (this.currentWidth < 0) { @@ -12578,45 +14755,35 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var offsetX = Math.cos(_angle + theta) * _hypotenuse, offsetY = Math.sin(_angle + theta) * _hypotenuse, sinTh = Math.sin(theta), - cosTh = Math.cos(theta); + cosTh = Math.cos(theta), + coords = this.getCenterPoint(), + wh = new fabric.Point(this.currentWidth, this.currentHeight); + var _tl = new fabric.Point(coords.x - offsetX, coords.y - offsetY); + var _tr = new fabric.Point(_tl.x + (wh.x * cosTh), _tl.y + (wh.x * sinTh)); + var _bl = new fabric.Point(_tl.x - (wh.y * sinTh), _tl.y + (wh.y * cosTh)); + var _mt = new fabric.Point(_tl.x + (wh.x/2 * cosTh), _tl.y + (wh.x/2 * sinTh)); + var tl = f(_tl); + var tr = f(_tr); + var br = f(new fabric.Point(_tr.x - (wh.y * sinTh), _tr.y + (wh.y * cosTh))); + var bl = f(_bl); + var ml = f(new fabric.Point(_tl.x - (wh.y/2 * sinTh), _tl.y + (wh.y/2 * cosTh))); + var mt = f(_mt); + var mr = f(new fabric.Point(_tr.x - (wh.y/2 * sinTh), _tr.y + (wh.y/2 * cosTh))); + var mb = f(new fabric.Point(_bl.x + (wh.x/2 * cosTh), _bl.y + (wh.x/2 * sinTh))); + var mtr = f(new fabric.Point(_mt.x, _mt.y)); - var coords = this.getCenterPoint(); - var tl = { - x: coords.x - offsetX, - y: coords.y - offsetY - }; - var tr = { - x: tl.x + (this.currentWidth * cosTh), - y: tl.y + (this.currentWidth * sinTh) - }; - var br = { - x: tr.x - (this.currentHeight * sinTh), - y: tr.y + (this.currentHeight * cosTh) - }; - var bl = { - x: tl.x - (this.currentHeight * sinTh), - y: tl.y + (this.currentHeight * cosTh) - }; - var ml = { - x: tl.x - (this.currentHeight/2 * sinTh), - y: tl.y + (this.currentHeight/2 * cosTh) - }; - var mt = { - x: tl.x + (this.currentWidth/2 * cosTh), - y: tl.y + (this.currentWidth/2 * sinTh) - }; - var mr = { - x: tr.x - (this.currentHeight/2 * sinTh), - y: tr.y + (this.currentHeight/2 * cosTh) - }; - var mb = { - x: bl.x + (this.currentWidth/2 * cosTh), - y: bl.y + (this.currentWidth/2 * sinTh) - }; - var mtr = { - x: mt.x, - y: mt.y - }; + // padding + var padX = Math.cos(_angle + theta) * this.padding * Math.sqrt(2), + padY = Math.sin(_angle + theta) * this.padding * Math.sqrt(2); + tl = tl.add(new fabric.Point(-padX, -padY)); + tr = tr.add(new fabric.Point(padY, -padX)); + br = br.add(new fabric.Point(padX, padY)); + bl = bl.add(new fabric.Point(-padY, padX)); + ml = ml.add(new fabric.Point((-padX - padY) / 2, (-padY + padX) / 2)); + mt = mt.add(new fabric.Point((padY - padX) / 2, -(padY + padX) / 2)); + mr = mr.add(new fabric.Point((padY + padX) / 2, (padY - padX) / 2)); + mb = mb.add(new fabric.Point((padX - padY) / 2, (padX + padY) / 2)); + mtr = mtr.add(new fabric.Point((padY - padX) / 2, -(padY + padX) / 2)); // debugging @@ -12714,9 +14881,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _findTargetCorner: function(e, offset) { if (!this.hasControls || !this.active) return false; - var pointer = getPointer(e, this.canvas.upperCanvasEl), - ex = pointer.x - offset.left, - ey = pointer.y - offset.top, + var pointer = this.canvas.getPointer(e, true), + ex = pointer.x, + ey = pointer.y, xPoints, lines; @@ -12965,25 +15132,32 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot scaleY = 1 / this._constrainScale(this.scaleY); ctx.lineWidth = 1 / this.borderScaleFactor; - - ctx.scale(scaleX, scaleY); - - var w = this.getWidth(), - h = this.getHeight(); + + var vpt = this.canvas.viewportTransform, + wh = fabric.util.transformPoint(new fabric.Point(this.getWidth(), this.getHeight()), vpt, true), + sxy = fabric.util.transformPoint(new fabric.Point(scaleX, scaleY), vpt, true), + w = wh.x, + h = wh.y, + sx= sxy.x, + sy= sxy.y; + if (this.group) { + w = w * this.group.scaleX; + h = h * this.group.scaleY; + } ctx.strokeRect( - ~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) - 0.5, // offset needed to make lines look sharper - ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) - 0.5, - ~~(w + padding2 + strokeWidth * this.scaleX) + 1, // double offset needed to make lines look sharper - ~~(h + padding2 + strokeWidth * this.scaleY) + 1 + ~~(-(w / 2) - padding - strokeWidth / 2 * sx) - 0.5, // offset needed to make lines look sharper + ~~(-(h / 2) - padding - strokeWidth / 2 * sy) - 0.5, + ~~(w + padding2 + strokeWidth * sx) + 1, // double offset needed to make lines look sharper + ~~(h + padding2 + strokeWidth * sy) + 1 ); if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) { var rotateHeight = ( this.flipY - ? h + (strokeWidth * this.scaleY) + (padding * 2) - : -h - (strokeWidth * this.scaleY) - (padding * 2) + ? h + (strokeWidth * sx) + (padding * 2) + : -h - (strokeWidth * sy) - (padding * 2) ) / 2; ctx.beginPath(); @@ -12999,7 +15173,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * Draws corners of an object's bounding box. - * Requires public properties: width, height, scaleX, scaleY + * Requires public properties: width, height * Requires public options: cornerSize, padding * @param {CanvasRenderingContext2D} ctx Context to draw on * @return {fabric.Object} thisArg @@ -13011,99 +15185,95 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot var size = this.cornerSize, size2 = size / 2, strokeWidth2 = ~~(this.strokeWidth / 2), // half strokeWidth rounded down - left = -(this.width / 2), - top = -(this.height / 2), + wh = fabric.util.transformPoint(new fabric.Point(this.getWidth(), this.getHeight()), this.canvas.viewportTransform, true), + width = wh.x, + height = wh.y, + left = -(width / 2), + top = -(height / 2), _left, _top, - sizeX = size / this.scaleX, - sizeY = size / this.scaleY, - paddingX = this.padding / this.scaleX, - paddingY = this.padding / this.scaleY, - scaleOffsetY = size2 / this.scaleY, - scaleOffsetX = size2 / this.scaleX, - scaleOffsetSizeX = (size2 - size) / this.scaleX, - scaleOffsetSizeY = (size2 - size) / this.scaleY, - height = this.height, - width = this.width, + padding = this.padding, + scaleOffset = size2, + scaleOffsetSize = size2 - size, methodName = this.transparentCorners ? 'strokeRect' : 'fillRect', transparent = this.transparentCorners, isVML = typeof G_vmlCanvasManager !== 'undefined'; ctx.save(); - ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); + ctx.lineWidth = 1; ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; ctx.strokeStyle = ctx.fillStyle = this.cornerColor; // top-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; + _left = left - scaleOffset - strokeWidth2 - padding; + _top = top - scaleOffset - strokeWidth2 - padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // top-right - _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; + _left = left + width - scaleOffset + strokeWidth2 + padding; + _top = top - scaleOffset - strokeWidth2 - padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // bottom-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + _left = left - scaleOffset - strokeWidth2 - padding; + _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // bottom-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + _left = left + width + scaleOffsetSize + strokeWidth2 + padding; + _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); if (!this.get('lockUniScaling')) { // middle-top - _left = left + width/2 - scaleOffsetX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; + _left = left + width/2 - scaleOffset; + _top = top - scaleOffset - strokeWidth2 - padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // middle-bottom - _left = left + width/2 - scaleOffsetX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + _left = left + width/2 - scaleOffset; + _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // middle-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height/2 - scaleOffsetY; + _left = left + width + scaleOffsetSize + strokeWidth2 + padding; + _top = top + height/2 - scaleOffset; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // middle-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height/2 - scaleOffsetY; + _left = left - scaleOffset - strokeWidth2 - padding; + _top = top + height/2 - scaleOffset; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); } // middle-top-rotate if (this.hasRotatingPoint) { - _left = left + width/2 - scaleOffsetX; + _left = left + width/2 - scaleOffset; _top = this.flipY ? - (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) - : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); + (top + height + (this.rotatingPointOffset) - size2 + strokeWidth2 + padding) + : (top - (this.rotatingPointOffset) - size2 - strokeWidth2 - padding); - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); } ctx.restore(); @@ -15173,6 +17343,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.save(); var m = this.transformMatrix; + + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + if (m) { ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } @@ -15206,8 +17386,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._renderStroke(ctx); this.clipTo && ctx.restore(); this._removeShadow(ctx); + ctx.restore(); + ctx.save(); if (!noTransform && this.active) { + var center; + center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -15541,6 +17727,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.save(); var m = this.transformMatrix; + + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + if (m) { ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } @@ -15554,8 +17750,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } this.clipTo && ctx.restore(); this._removeShadow(ctx); + ctx.restore(); + ctx.save(); if (this.active) { + var center; + center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -15718,7 +17920,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot extend = fabric.util.object.extend, min = fabric.util.array.min, max = fabric.util.array.max, - invoke = fabric.util.array.invoke; + invoke = fabric.util.array.invoke, + degreesToRadians = fabric.util.degreesToRadians; if (fabric.Group) { return; @@ -15764,6 +17967,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._objects = objects || []; for (var i = this._objects.length; i--; ) { this._objects[i].group = this; + this._objects[i].setCoords(); } this.originalState = { }; @@ -15927,9 +18131,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (!this.visible) return; ctx.save(); - this.transform(ctx); + var v = this.canvas.viewportTransform; - var groupScaleFactor = Math.max(this.scaleX, this.scaleY); + var sxy = fabric.util.transformPoint( + new fabric.Point(this.scaleX, this.scaleY), + v, true), + groupScaleFactor = Math.max(sxy.x, sxy.y); this.clipTo && fabric.util.clipContext(this, ctx); @@ -15943,22 +18150,21 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot // do not render if object is not visible if (!object.visible) continue; - object.borderScaleFactor = groupScaleFactor; object.hasRotatingPoint = false; - object.render(ctx); - object.borderScaleFactor = originalScaleFactor; object.hasRotatingPoint = originalHasRotatingPoint; } this.clipTo && ctx.restore(); - if (!noTransform && this.active) { + if (this.active && !noTransform) { + var center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } ctx.restore(); - this.setCoords(); }, /** @@ -16106,7 +18312,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _calcBounds: function() { var aX = [], aY = [], - minX, minY, maxX, maxY, o, width, height, + minX, minY, maxX, maxY, o, width, height, minXY, maxXY, i = 0, len = this._objects.length; @@ -16118,20 +18324,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot aY.push(o.oCoords[prop].y); } } + + var ivt = fabric.util.invertTransform(canvas.viewportTransform); - minX = min(aX); - maxX = max(aX); - minY = min(aY); - maxY = max(aY); + minXY = new fabric.Point(min(aX), min(aY)); + maxXY = new fabric.Point(max(aX), max(aY)); - width = (maxX - minX) || 0; - height = (maxY - minY) || 0; + minXY = fabric.util.transformPoint(minXY, ivt); + maxXY = fabric.util.transformPoint(maxXY, ivt); - this.width = width; - this.height = height; - - this.left = (minX + width / 2) || 0; - this.top = (minY + height / 2) || 0; + this.width = (maxXY.x - minXY.x) || 0; + this.height = (maxXY.y - minXY.y) || 0; + + this.left = (minXY.x + maxXY.x) / 2 || 0; + this.top = (minXY.y + maxXY.y) / 2 || 0; }, /* _TO_SVG_START_ */ @@ -16315,8 +18521,17 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.save(); var m = this.transformMatrix; + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } var isInPathGroup = this.group && this.group.type !== 'group'; + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + // this._resetWidthHeight(); if (isInPathGroup) { ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2); @@ -16338,8 +18553,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._renderStroke(ctx); this.clipTo && ctx.restore(); ctx.restore(); + ctx.restore(); + ctx.save(); if (this.active && !noTransform) { + var center; + if (this.group) { + center = fabric.util.transformPoint(this.group.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.group.angle)); + } + center = fabric.util.transformPoint(this.getCenterPoint(), v, null != this.group); + if (this.group) { + center.x *= this.group.scaleX; + center.y *= this.group.scaleY; + } + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -18602,8 +20832,22 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag if (!this.visible) return; ctx.save(); + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); this._render(ctx); + ctx.restore(); + ctx.save(); if (!noTransform && this.active) { + var center; + center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } diff --git a/dist/all.require.js b/dist/all.require.js index dcc1e18c..d832fe79 100644 --- a/dist/all.require.js +++ b/dist/all.require.js @@ -1,4 +1,4 @@ -/* build: `node build.js modules=ALL exclude=gestures` */ +/* build: `node build.js modules=ALL` */ /*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */ var fabric = fabric || { version: "1.3.7" }; @@ -1749,6 +1749,1912 @@ if (!JSON) { } }()); +/* + ---------------------------------------------------- + Event.js : 1.1.1 : 2012/11/19 : MIT License + ---------------------------------------------------- + https://github.com/mudcube/Event.js + ---------------------------------------------------- + 1 : click, dblclick, dbltap + 1+ : tap, longpress, drag, swipe + 2+ : pinch, rotate + : mousewheel, devicemotion, shake + ---------------------------------------------------- + TODO + ---------------------------------------------------- + * switch configuration to 4th argument on addEventListener + * bbox calculation for elements scaled with transform. + ---------------------------------------------------- + NOTES + ---------------------------------------------------- + * When using other libraries that may have built in "Event" namespace, + i.e. Typescript, you can use "eventjs" instead of "Event" for all example calls. + ---------------------------------------------------- + REQUIREMENTS: querySelector, querySelectorAll + ---------------------------------------------------- + * There are two ways to add/remove events with this library. + ---------------------------------------------------- + // Retains "this" attribute as target, and overrides native addEventListener. + target.addEventListener(type, listener, useCapture); + target.removeEventListener(type, listener, useCapture); + + // Attempts to perform as fast as possible. + Event.add(type, listener, configure); + Event.remove(type, listener, configure); + + * You can turn prototyping on/off for individual features. + ---------------------------------------------------- + Event.modifyEventListener = true; // add custom *EventListener commands to HTMLElements. + Event.modifySelectors = true; // add bulk *EventListener commands on NodeLists from querySelectorAll and others. + + * Example of setting up a single listener with a custom configuration. + ---------------------------------------------------- + // optional configuration. + var configure = { + fingers: 2, // listen for specifically two fingers. + snap: 90 // snap to 90 degree intervals. + }; + // adding with addEventListener() + target.addEventListener("swipe", function(event) { + // additional variables can be found on the event object. + console.log(event.velocity, event.angle, event.fingers); + }, configure); + + // adding with Event.add() + Event.add("swipe", function(event, self) { + // additional variables can be found on the self object. + console.log(self.velocity, self.angle, self.fingers); + }, configure); + + * Multiple listeners glued together. + ---------------------------------------------------- + // adding with addEventListener() + target.addEventListener("click swipe", function(event) { }); + + // adding with Event.add() + Event.add(target, "click swipe", function(event, self) { }); + + * Use query selectors to create an event (querySelectorAll) + ---------------------------------------------------- + // adding events to NodeList from querySelectorAll() + document.querySelectorAll("#element a.link").addEventListener("click", callback); + + // adding with Event.add() + Event.add("#element a.link", "click", callback); + + * Listen for selector to become available (querySelector) + ---------------------------------------------------- + Event.add("body", "ready", callback); + // or... + Event.add({ + target: "body", + type: "ready", + timeout: 10000, // set a timeout to stop checking. + interval: 30, // set how often to check for element. + listener: callback + }); + + * Multiple listeners bound to one callback w/ single configuration. + ---------------------------------------------------- + var bindings = Event.add({ + target: target, + type: "click swipe", + snap: 90, // snap to 90 degree intervals. + minFingers: 2, // minimum required fingers to start event. + maxFingers: 4, // maximum fingers in one event. + listener: function(event, self) { + console.log(self.gesture); // will be click or swipe. + console.log(self.x); + console.log(self.y); + console.log(self.identifier); + console.log(self.start); + console.log(self.fingers); // somewhere between "2" and "4". + self.pause(); // disable event. + self.resume(); // enable event. + self.remove(); // remove event. + } + }); + + * Multiple listeners bound to multiple callbacks w/ single configuration. + ---------------------------------------------------- + var bindings = Event.add({ + target: target, + minFingers: 1, + maxFingers: 12, + listeners: { + click: function(event, self) { + self.remove(); // removes this click listener. + }, + swipe: function(event, self) { + binding.remove(); // removes both the click + swipe listeners. + } + } + }); + + * Multiple listeners bound to multiple callbacks w/ multiple configurations. + ---------------------------------------------------- + var binding = Event.add({ + target: target, + listeners: { + longpress: { + fingers: 1, + wait: 500, // milliseconds + listener: function(event, self) { + console.log(self.fingers); // "1" finger. + } + }, + drag: { + fingers: 3, + position: "relative", // "relative", "absolute", "difference", "move" + listener: function(event, self) { + console.log(self.fingers); // "3" fingers. + console.log(self.x); // coordinate is relative to edge of target. + } + } + } + }); + + * Capturing an event and manually forwarding it to a proxy (tiered events). + ---------------------------------------------------- + Event.add(target, "down", function(event, self) { + var x = event.pageX; // local variables that wont change. + var y = event.pageY; + Event.proxy.drag({ + event: event, + target: target, + listener: function(event, self) { + console.log(x - event.pageX); // measure movement. + console.log(y - event.pageY); + } + }); + }); + ---------------------------------------------------- + + * Event proxies. + * type, fingers, state, start, x, y, position, bbox + * rotation, scale, velocity, angle, delay, timeout + ---------------------------------------------------- + // "Click" :: fingers, minFingers, maxFingers. + Event.add(window, "click", function(event, self) { + console.log(self.gesture, self.x, self.y); + }); + // "Double-Click" :: fingers, minFingers, maxFingers. + Event.add(window, "dblclick", function(event, self) { + console.log(self.gesture, self.x, self.y); + }); + // "Drag" :: fingers, maxFingers, position + Event.add(window, "drag", function(event, self) { + console.log(self.gesture, self.fingers, self.state, self.start, self.x, self.y, self.bbox); + }); + // "Gesture" :: fingers, minFingers, maxFingers. + Event.add(window, "gesture", function(event, self) { + console.log(self.gesture, self.fingers, self.state, self.rotation, self.scale); + }); + // "Swipe" :: fingers, minFingers, maxFingers, snap, threshold. + Event.add(window, "swipe", function(event, self) { + console.log(self.gesture, self.fingers, self.velocity, self.angle, self.start, self.x, self.y); + }); + // "Tap" :: fingers, minFingers, maxFingers, timeout. + Event.add(window, "tap", function(event, self) { + console.log(self.gesture, self.fingers); + }); + // "Longpress" :: fingers, minFingers, maxFingers, delay. + Event.add(window, "longpress", function(event, self) { + console.log(self.gesture, self.fingers); + }); + // + Event.add(window, "shake", function(event, self) { + console.log(self.gesture, self.acceleration, self.accelerationIncludingGravity); + }); + // + Event.add(window, "devicemotion", function(event, self) { + console.log(self.gesture, self.acceleration, self.accelerationIncludingGravity); + }); + // + Event.add(window, "wheel", function(event, self) { + console.log(self.gesture, self.state, self.wheelDelta); + }); + + * Stop, prevent and cancel. + ---------------------------------------------------- + Event.stop(event); // stop bubble. + Event.prevent(event); // prevent default. + Event.cancel(event); // stop and prevent. + + * Track for proper command/control-key for Mac/PC. + ---------------------------------------------------- + Event.add(window, "keyup keydown", Event.proxy.metaTracker); + console.log(Event.proxy.metaKey); + + * Test for event features, in this example Drag & Drop file support. + ---------------------------------------------------- + console.log(Event.supports('dragstart') && Event.supports('drop') && !!window.FileReader); + + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(eventjs) === "undefined") + var eventjs = Event; + +Event = (function(root) { + "use strict"; + +// Add custom *EventListener commands to HTMLElements. + root.modifyEventListener = false; + +// Add bulk *EventListener commands on NodeLists from querySelectorAll and others. + root.modifySelectors = false; + +// Event maintenance. + root.add = function(target, type, listener, configure) { + return eventManager(target, type, listener, configure, "add"); + }; + + root.remove = function(target, type, listener, configure) { + return eventManager(target, type, listener, configure, "remove"); + }; + + root.stop = function(event) { + if (event.stopPropagation) + event.stopPropagation(); + event.cancelBubble = true; // <= IE8 + event.bubble = 0; + }; + + root.prevent = function(event) { + if (event.preventDefault) + event.preventDefault(); + event.returnValue = false; // <= IE8 + }; + + root.cancel = function(event) { + root.stop(event); + root.prevent(event); + }; + +// Check whether event is natively supported (via @kangax) + root.supports = function(target, type) { + if (typeof(target) === "string") { + type = target; + target = window; + } + type = "on" + type; + if (type in target) + return true; + if (!target.setAttribute) + target = document.createElement("div"); + if (target.setAttribute && target.removeAttribute) { + target.setAttribute(type, ""); + var isSupported = typeof target[type] === "function"; + if (typeof target[type] !== "undefined") + target[type] = null; + target.removeAttribute(type); + return isSupported; + } + }; + + var clone = function(obj) { + if (!obj || typeof (obj) !== 'object') + return obj; + var temp = new obj.constructor(); + for (var key in obj) { + if (!obj[key] || typeof (obj[key]) !== 'object') { + temp[key] = obj[key]; + } else { // clone sub-object + temp[key] = clone(obj[key]); + } + } + return temp; + }; + +/// Handle custom *EventListener commands. + var eventManager = function(target, type, listener, configure, trigger, fromOverwrite) { + configure = configure || {}; + // Check for element to load on interval (before onload). + if (typeof(target) === "string" && type === "ready") { + var time = (new Date()).getTime(); + var timeout = configure.timeout; + var ms = configure.interval || 1000 / 60; + var interval = window.setInterval(function() { + if ((new Date()).getTime() - time > timeout) { + window.clearInterval(interval); + } + if (document.querySelector(target)) { + window.clearInterval(interval); + listener(); + } + }, ms); + return; + } + // Get DOM element from Query Selector. + if (typeof(target) === "string") { + target = document.querySelectorAll(target); + if (target.length === 0) + return createError("Missing target on listener!"); // No results. + if (target.length === 1) { // Single target. + target = target[0]; + } + } + /// Handle multiple targets. + var event; + var events = {}; + if (target.length > 0) { + for (var n0 = 0, length0 = target.length; n0 < length0; n0++) { + event = eventManager(target[n0], type, listener, clone(configure), trigger); + if (event) + events[n0] = event; + } + return createBatchCommands(events); + } + // Check for multiple events in one string. + if (type.indexOf && type.indexOf(" ") !== -1) + type = type.split(" "); + if (type.indexOf && type.indexOf(",") !== -1) + type = type.split(","); + // Attach or remove multiple events associated with a target. + if (typeof(type) !== "string") { // Has multiple events. + if (typeof(type.length) === "number") { // Handle multiple listeners glued together. + for (var n1 = 0, length1 = type.length; n1 < length1; n1++) { // Array [type] + event = eventManager(target, type[n1], listener, clone(configure), trigger); + if (event) + events[type[n1]] = event; + } + } else { // Handle multiple listeners. + for (var key in type) { // Object {type} + if (typeof(type[key]) === "function") { // without configuration. + event = eventManager(target, key, type[key], clone(configure), trigger); + } else { // with configuration. + event = eventManager(target, key, type[key].listener, clone(type[key]), trigger); + } + if (event) + events[key] = event; + } + } + return createBatchCommands(events); + } + // Ensure listener is a function. + if (typeof(listener) !== "function") + return createError("Listener is not a function!"); + // Generate a unique wrapper identifier. + var useCapture = configure.useCapture || false; + var id = normalize(type) + getID(target) + "." + getID(listener) + "." + (useCapture ? 1 : 0); + // Handle the event. + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // Fire custom event. + if (trigger === "remove") { // Remove event listener. + if (!wrappers[id]) + return; // Already removed. + wrappers[id].remove(); + delete wrappers[id]; + } else if (trigger === "add") { // Attach event listener. + if (wrappers[id]) + return wrappers[id]; // Already attached. + // Retains "this" orientation. + if (configure.useCall && !root.modifyEventListener) { + var tmp = listener; + listener = function(event, self) { + for (var key in self) + event[key] = self[key]; + return tmp.call(target, event); + }; + } + // Create listener proxy. + configure.gesture = type; + configure.target = target; + configure.listener = listener; + configure.fromOverwrite = fromOverwrite; + // Record wrapper. + wrappers[id] = root.proxy[type](configure); + } + } else { // Fire native event. + type = normalize(type); + if (trigger === "remove") { // Remove event listener. + if (!wrappers[id]) + return; // Already removed. + target[remove](type, listener, useCapture); + delete wrappers[id]; + } else if (trigger === "add") { // Attach event listener. + if (wrappers[id]) + return wrappers[id]; // Already attached. + target[add](type, listener, useCapture); + // Record wrapper. + wrappers[id] = { + type: type, + target: target, + listener: listener, + remove: function() { + root.remove(target, type, listener, configure); + } + }; + } + } + return wrappers[id]; + }; + +/// Perform batch actions on multiple events. + var createBatchCommands = function(events) { + return { + remove: function() { // Remove multiple events. + for (var key in events) { + events[key].remove(); + } + }, + add: function() { // Add multiple events. + for (var key in events) { + events[key].add(); + } + } + }; + }; + +/// Display error message in console. + var createError = function(message) { + if (typeof(console) === "undefined") + return; + if (typeof(console.error) === "undefined") + return; + console.error(message); + }; + +/// Handle naming discrepancies between platforms. + var normalize = (function() { + var translate = {}; + return function(type) { + if (!root.pointerType) { + if (window.navigator.msPointerEnabled) { + root.pointerType = "mspointer"; + translate = { + "mousedown": "MSPointerDown", + "mousemove": "MSPointerMove", + "mouseup": "MSPointerUp" + }; + } else if (root.supports("touchstart")) { + root.pointerType = "touch"; + translate = { + "mousedown": "touchstart", + "mouseup": "touchend", + "mousemove": "touchmove" + }; + } else { + root.pointerType = "mouse"; + } + } + if (translate[type]) + type = translate[type]; + if (!document.addEventListener) { // IE + return "on" + type; + } else { + return type; + } + }; + })(); + +/// Event wrappers to keep track of all events placed in the window. + var wrappers = {}; + var counter = 0; + var getID = function(object) { + if (object === window) + return "#window"; + if (object === document) + return "#document"; + if (!object) + return createError("Missing target on listener!"); + if (!object.uniqueID) + object.uniqueID = "id" + counter++; + return object.uniqueID; + }; + +/// Detect platforms native *EventListener command. + var add = document.addEventListener ? "addEventListener" : "attachEvent"; + var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; + + /* + Pointer.js + ------------------------ + Modified from; https://github.com/borismus/pointer.js + */ + + root.createPointerEvent = function(event, self, preventRecord) { + var eventName = self.gesture; + var target = self.target; + var pts = event.changedTouches || root.proxy.getCoords(event); + if (pts.length) { + var pt = pts[0]; + self.pointers = preventRecord ? [] : pts; + self.pageX = pt.pageX; + self.pageY = pt.pageY; + self.x = self.pageX; + self.y = self.pageY; + } + /// + var newEvent = document.createEvent("Event"); + newEvent.initEvent(eventName, true, true); + newEvent.originalEvent = event; + for (var k in self) { + if (k === "target") + continue; + newEvent[k] = self[k]; + } + target.dispatchEvent(newEvent); + }; + +/// Allows *EventListener to use custom event proxies. + if (root.modifyEventListener && window.HTMLElement) + (function() { + var augmentEventListener = function(proto) { + var recall = function(trigger) { // overwrite native *EventListener's + var handle = trigger + "EventListener"; + var handler = proto[handle]; + proto[handle] = function(type, listener, useCapture) { + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // capture custom events. + var configure = useCapture; + if (typeof(useCapture) === "object") { + configure.useCall = true; + } else { // convert to configuration object. + configure = { + useCall: true, + useCapture: useCapture + }; + } + eventManager(this, type, listener, configure, trigger, true); + handler.call(this, type, listener, useCapture); + } else { // use native function. + handler.call(this, normalize(type), listener, useCapture); + } + }; + }; + recall("add"); + recall("remove"); + }; + // NOTE: overwriting HTMLElement doesn't do anything in Firefox. + if (navigator.userAgent.match(/Firefox/)) { + // TODO: fix Firefox for the general case. + augmentEventListener(HTMLDivElement.prototype); + augmentEventListener(HTMLCanvasElement.prototype); + } else { + augmentEventListener(HTMLElement.prototype); + } + augmentEventListener(document); + augmentEventListener(window); + })(); + +/// Allows querySelectorAll and other NodeLists to perform *EventListener commands in bulk. + if (root.modifySelectors) + (function() { + var proto = NodeList.prototype; + proto.removeEventListener = function(type, listener, useCapture) { + for (var n = 0, length = this.length; n < length; n++) { + this[n].removeEventListener(type, listener, useCapture); + } + }; + proto.addEventListener = function(type, listener, useCapture) { + for (var n = 0, length = this.length; n < length; n++) { + this[n].addEventListener(type, listener, useCapture); + } + }; + })(); + + return root; + +})(Event); +/* + ---------------------------------------------------- + Event.proxy : 0.4.2 : 2012/07/29 : MIT License + ---------------------------------------------------- + https://github.com/mudcube/Event.js + ---------------------------------------------------- + Pointer Gestures + ---------------------------------------------------- + 1 : click, dblclick, dbltap + 1+ : tap, taphold, drag, swipe + 2+ : pinch, rotate + ---------------------------------------------------- + Gyroscope Gestures + ---------------------------------------------------- + * shake + ---------------------------------------------------- + Fixes issues with + ---------------------------------------------------- + * mousewheel-Firefox uses DOMMouseScroll and does not return wheelDelta. + * devicemotion-Fixes issue where event.acceleration is not returned. + ---------------------------------------------------- + Ideas for the future + ---------------------------------------------------- + * Keyboard, GamePad, and other input abstractions. + * Event batching - i.e. for every x fingers down a new gesture is created. + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + /* + Create a new pointer gesture instance. + */ + + root.pointerSetup = function(conf, self) { + /// Configure. + conf.doc = conf.target.ownerDocument || conf.target; // Associated document. + conf.minFingers = conf.minFingers || conf.fingers || 1; // Minimum required fingers. + conf.maxFingers = conf.maxFingers || conf.fingers || Infinity; // Maximum allowed fingers. + conf.position = conf.position || "relative"; // Determines what coordinate system points are returned. + delete conf.fingers; //- + /// Convenience data. + self = self || {}; + self.gesture = conf.gesture; + self.target = conf.target; + self.pointerType = Event.pointerType; + /// + if (Event.modifyEventListener && conf.fromOverwrite) + conf.listener = Event.createPointerEvent; + /// Convenience commands. + var fingers = 0; + var type = self.gesture.indexOf("pointer") === 0 && Event.modifyEventListener ? "pointer" : "mouse"; + self.listener = conf.listener; + self.proxy = function(listener) { + self.defaultListener = conf.listener; + conf.listener = listener; + listener(conf.event, self); + }; + self.remove = function() { + if (conf.onPointerDown) + Event.remove(conf.target, type + "down", conf.onPointerDown); + if (conf.onPointerMove) + Event.remove(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp) + Event.remove(conf.doc, type + "up", conf.onPointerUp); + }; + self.resume = function(opt) { + if (conf.onPointerMove && (!opt || opt.move)) + Event.add(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp && (!opt || opt.move)) + Event.add(conf.doc, type + "up", conf.onPointerUp); + conf.fingers = fingers; + }; + self.pause = function(opt) { + fingers = conf.fingers; + if (conf.onPointerMove && (!opt || opt.move)) + Event.remove(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp && (!opt || opt.up)) + Event.remove(conf.doc, type + "up", conf.onPointerUp); + conf.fingers = 0; + }; + /// + return self; + }; + + /* + Begin proxied pointer command. + */ + + root.pointerStart = function(event, self, conf) { + var addTouchStart = function(touch, sid) { + var bbox = conf.bbox; + var pt = track[sid] = {}; + /// + switch (conf.position) { + case "absolute": // Absolute from within window. + pt.offsetX = 0; + pt.offsetY = 0; + break; + case "difference": // Relative from origin. + pt.offsetX = touch.pageX; + pt.offsetY = touch.pageY; + break; + case "move": // Move target element. + pt.offsetX = touch.pageX - bbox.x1; + pt.offsetY = touch.pageY - bbox.y1; + break; + default: // Relative from within target. + pt.offsetX = bbox.x1; + pt.offsetY = bbox.y1; + break; + } + /// + if (conf.position === "relative") { + var x = (touch.pageX + bbox.scrollLeft - pt.offsetX) * bbox.scaleX; + var y = (touch.pageY + bbox.scrollTop - pt.offsetY) * bbox.scaleY; + } else { + var x = (touch.pageX - pt.offsetX); + var y = (touch.pageY - pt.offsetY); + } + /// + pt.rotation = 0; + pt.scale = 1; + pt.startTime = pt.moveTime = (new Date).getTime(); + pt.move = {x: x, y: y}; + pt.start = {x: x, y: y}; + /// + conf.fingers++; + }; + /// + conf.event = event; + if (self.defaultListener) { + conf.listener = self.defaultListener; + delete self.defaultListener; + } + /// + var isTouchStart = !conf.fingers; + var track = conf.tracker; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Adding touch events to tracking. + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; // Touch ID. + // Track the current state of the touches. + if (conf.fingers) { + if (conf.fingers >= conf.maxFingers) { + var ids = []; + for (var sid in conf.tracker) + ids.push(sid); + self.identifier = ids.join(","); + return isTouchStart; + } + var fingers = 0; // Finger ID. + for (var rid in track) { + // Replace removed finger. + if (track[rid].up) { + delete track[rid]; + addTouchStart(touch, sid); + conf.cancel = true; + break; + } + fingers++; + } + // Add additional finger. + if (track[sid]) + continue; + addTouchStart(touch, sid); + } else { // Start tracking fingers. + track = conf.tracker = {}; + self.bbox = conf.bbox = root.getBoundingBox(conf.target); + conf.fingers = 0; + conf.cancel = false; + addTouchStart(touch, sid); + } + } + /// + var ids = []; + for (var sid in conf.tracker) + ids.push(sid); + self.identifier = ids.join(","); + /// + return isTouchStart; + }; + + /* + End proxied pointer command. + */ + + root.pointerEnd = function(event, self, conf, onPointerUp) { + // Record changed touches have ended (iOS changedTouches is not reliable). + var touches = event.touches || []; + var length = touches.length; + var exists = {}; + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var sid = touch.identifier; + exists[sid || Infinity] = true; + } + for (var sid in conf.tracker) { + var track = conf.tracker[sid]; + if (exists[sid] || track.up) + continue; + if (onPointerUp) { // add changedTouches to mouse. + onPointerUp({ + pageX: track.pageX, + pageY: track.pageY, + changedTouches: [{ + pageX: track.pageX, + pageY: track.pageY, + identifier: sid === "Infinity" ? Infinity : sid + }] + }, "up"); + } + track.up = true; + conf.fingers--; + } + /* // This should work but fails in Safari on iOS4 so not using it. + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Record changed touches have ended (this should work). + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var track = conf.tracker[sid]; + if (track && !track.up) { + if (onPointerUp) { // add changedTouches to mouse. + onPointerUp({ + changedTouches: [{ + pageX: track.pageX, + pageY: track.pageY, + identifier: sid === "Infinity" ? Infinity : sid + }] + }, "up"); + } + track.up = true; + conf.fingers --; + } + } */ + // Wait for all fingers to be released. + if (conf.fingers !== 0) + return false; + // Record total number of fingers gesture used. + var ids = []; + conf.gestureFingers = 0; + for (var sid in conf.tracker) { + conf.gestureFingers++; + ids.push(sid); + } + self.identifier = ids.join(","); + // Our pointer gesture has ended. + return true; + }; + + /* + Returns mouse coords in an array to match event.*Touches + ------------------------------------------------------------ + var touch = event.changedTouches || root.getCoords(event); + */ + + root.getCoords = function(event) { + if (typeof(event.pageX) !== "undefined") { // Desktop browsers. + root.getCoords = function(event) { + return Array({ + type: "mouse", + x: event.pageX, + y: event.pageY, + pageX: event.pageX, + pageY: event.pageY, + identifier: Infinity + }); + }; + } else { // Internet Explorer <= 8.0 + root.getCoords = function(event) { + event = event || window.event; + return Array({ + type: "mouse", + x: event.clientX + document.documentElement.scrollLeft, + y: event.clientY + document.documentElement.scrollTop, + pageX: event.clientX + document.documentElement.scrollLeft, + pageY: event.clientY + document.documentElement.scrollTop, + identifier: Infinity + }); + }; + } + return root.getCoords(event); + }; + + /* + Returns single coords in an object. + ------------------------------------------------------------ + var mouse = root.getCoord(event); + */ + + root.getCoord = function(event) { + if ("ontouchstart" in window) { // Mobile browsers. + var pX = 0; + var pY = 0; + root.getCoord = function(event) { + var touches = event.changedTouches; + if (touches.length) { // ontouchstart + ontouchmove + return { + x: pX = touches[0].pageX, + y: pY = touches[0].pageY + }; + } else { // ontouchend + return { + x: pX, + y: pY + }; + } + }; + } else if (typeof(event.pageX) !== "undefined" && typeof(event.pageY) !== "undefined") { // Desktop browsers. + root.getCoord = function(event) { + return { + x: event.pageX, + y: event.pageY + }; + }; + } else { // Internet Explorer <=8.0 + root.getCoord = function(event) { + event = event || window.event; + return { + x: event.clientX + document.documentElement.scrollLeft, + y: event.clientY + document.documentElement.scrollTop + }; + }; + } + return root.getCoord(event); + }; + + /* + Get target scale and position in space. + */ + + root.getBoundingBox = function(o) { + if (o === window || o === document) + o = document.body; + /// + var bbox = { + x1: 0, + y1: 0, + x2: 0, + y2: 0, + scrollLeft: 0, + scrollTop: 0 + }; + /// + if (o === document.body) { + bbox.height = window.innerHeight; + bbox.width = window.innerWidth; + } else { + bbox.height = o.offsetHeight; + bbox.width = o.offsetWidth; + } + /// Get the scale of the element. + bbox.scaleX = o.width / bbox.width || 1; + bbox.scaleY = o.height / bbox.height || 1; + /// Get the offset of element. + var tmp = o; + while (tmp !== null) { + bbox.x1 += tmp.offsetLeft; + bbox.y1 += tmp.offsetTop; + tmp = tmp.offsetParent; + } + ; + /// Get the scroll of container element. + var tmp = o.parentNode; + while (tmp !== null) { + if (tmp === document.body) + break; + if (tmp.scrollTop === undefined) + break; + bbox.scrollLeft += tmp.scrollLeft; + bbox.scrollTop += tmp.scrollTop; + tmp = tmp.parentNode; + } + ; + /// Record the extent of box. + bbox.x2 = bbox.x1 + bbox.width; + bbox.y2 = bbox.y1 + bbox.height; + /// + return bbox; + }; + + /* + Keep track of metaKey, the proper ctrlKey for users platform. + */ + + (function() { + var agent = navigator.userAgent.toLowerCase(); + var mac = agent.indexOf("macintosh") !== -1; + if (mac && agent.indexOf("khtml") !== -1) { // chrome, safari. + var watch = {91: true, 93: true}; + } else if (mac && agent.indexOf("firefox") !== -1) { // mac firefox. + var watch = {224: true}; + } else { // windows, linux, or mac opera. + var watch = {17: true}; + } + root.isMetaKey = function(event) { + return !!watch[event.keyCode]; + }; + root.metaTracker = function(event) { + if (watch[event.keyCode]) { + root.metaKey = event.type === "keydown"; + } + }; + })(); + + return root; + +})(Event.proxy); +/* + "Click" event proxy. + ---------------------------------------------------- + Event.add(window, "click", function(event, self) {}); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.click = function(conf) { + conf.maxFingers = conf.maxFingers || conf.fingers || 1; + // Setting up local variables. + var EVENT; + // Tracking the events. + conf.onPointerDown = function(event) { + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function(event) { + EVENT = event; + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + if (EVENT.cancelBubble && ++EVENT.bubble > 1) + return; + var pointers = EVENT.changedTouches || root.getCoords(EVENT); + var pointer = pointers[0]; + var bbox = conf.bbox; + var newbbox = root.getBoundingBox(conf.target); + if (conf.position === "relative") { + var ax = (pointer.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; + var ay = (pointer.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; + } else { + var ax = (pointer.pageX - bbox.x1); + var ay = (pointer.pageY - bbox.y1); + } + if (ax > 0 && ax < bbox.width && // Within target coordinates. + ay > 0 && ay < bbox.height && + bbox.scrollTop === newbbox.scrollTop) { + conf.listener(EVENT, self); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + self.state = "click"; + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.click = root.click; + + return root; + +})(Event.proxy); +/* + "Double-Click" aka "Double-Tap" event proxy. + ---------------------------------------------------- + Event.add(window, "dblclick", function(event, self) {}); + ---------------------------------------------------- + Touch an target twice for <= 700ms, with less than 25 pixel drift. + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.dbltap = + root.dblclick = function(conf) { + conf.maxFingers = conf.maxFingers || conf.fingers || 1; + // Setting up local variables. + var delay = 700; // in milliseconds + var time0, time1, timeout; + var pointer0, pointer1; + // Tracking the events. + conf.onPointerDown = function(event) { + var pointers = event.changedTouches || root.getCoords(event); + if (time0 && !time1) { // Click #2 + pointer1 = pointers[0]; + time1 = (new Date).getTime() - time0; + } else { // Click #1 + pointer0 = pointers[0]; + time0 = (new Date).getTime(); + time1 = 0; + clearTimeout(timeout); + timeout = setTimeout(function() { + time0 = 0; + }, delay); + } + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function(event) { + if (time0 && !time1) { + var pointers = event.changedTouches || root.getCoords(event); + pointer1 = pointers[0]; + } + var bbox = conf.bbox; + if (conf.position === "relative") { + var ax = (pointer1.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; + var ay = (pointer1.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; + } else { + var ax = (pointer1.pageX - bbox.x1); + var ay = (pointer1.pageY - bbox.y1); + } + if (!(ax > 0 && ax < bbox.width && // Within target coordinates.. + ay > 0 && ay < bbox.height && + Math.abs(pointer1.pageX - pointer0.pageX) <= 25 && // Within drift deviance. + Math.abs(pointer1.pageY - pointer0.pageY) <= 25)) { + // Cancel out this listener. + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + clearTimeout(timeout); + time0 = time1 = 0; + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + if (time0 && time1) { + if (time1 <= delay && !(event.cancelBubble && ++event.bubble > 1)) { + self.state = conf.gesture; + conf.listener(event, self); + } + clearTimeout(timeout); + time0 = time1 = 0; + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + self.state = "dblclick"; + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.dbltap = root.dbltap; + Event.Gesture._gestureHandlers.dblclick = root.dblclick; + + return root; + +})(Event.proxy); +/* + "Drag" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: maxFingers, position. + ---------------------------------------------------- + Event.add(window, "drag", function(event, self) { + console.log(self.gesture, self.state, self.start, self.x, self.y, self.bbox); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.dragElement = function(that, event) { + root.drag({ + event: event, + target: that, + position: "move", + listener: function(event, self) { + that.style.left = self.x + "px"; + that.style.top = self.y + "px"; + Event.prevent(event); + } + }); + }; + + root.drag = function(conf) { + conf.gesture = "drag"; + conf.onPointerDown = function(event) { + if (root.pointerStart(event, self, conf)) { + if (!conf.monitor) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + } + // Process event listener. + conf.onPointerMove(event, "down"); + }; + conf.onPointerMove = function(event, state) { + if (!conf.tracker) + return conf.onPointerDown(event); + var bbox = conf.bbox; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var identifier = touch.identifier || Infinity; + var pt = conf.tracker[identifier]; + // Identifier defined outside of listener. + if (!pt) + continue; + pt.pageX = touch.pageX; + pt.pageY = touch.pageY; + // Record data. + self.state = state || "move"; + self.identifier = identifier; + self.start = pt.start; + self.fingers = conf.fingers; + if (conf.position === "relative") { + self.x = (pt.pageX + bbox.scrollLeft - pt.offsetX) * bbox.scaleX; + self.y = (pt.pageY + bbox.scrollTop - pt.offsetY) * bbox.scaleY; + } else { + self.x = (pt.pageX - pt.offsetX); + self.y = (pt.pageY - pt.offsetY); + } + /// + conf.listener(event, self); + } + }; + conf.onPointerUp = function(event) { + // Remove tracking for touch. + if (root.pointerEnd(event, self, conf, conf.onPointerMove)) { + if (!conf.monitor) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + if (conf.event) { + conf.onPointerDown(conf.event); + } else { // + Event.add(conf.target, "mousedown", conf.onPointerDown); + if (conf.monitor) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + } + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.drag = root.drag; + + return root; + +})(Event.proxy); +/* + "Gesture" event proxy (2+ fingers). + ---------------------------------------------------- + CONFIGURE: minFingers, maxFingers. + ---------------------------------------------------- + Event.add(window, "gesture", function(event, self) { + console.log(self.rotation, self.scale, self.fingers, self.state); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + var RAD_DEG = Math.PI / 180; + + root.gesture = function(conf) { + conf.minFingers = conf.minFingers || conf.fingers || 2; + // Tracking the events. + conf.onPointerDown = function(event) { + var fingers = conf.fingers; + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + // Record gesture start. + if (conf.fingers === conf.minFingers && fingers !== conf.fingers) { + self.fingers = conf.minFingers; + self.scale = 1; + self.rotation = 0; + self.state = "start"; + var sids = ""; //- FIXME(mud): can generate duplicate IDs. + for (var key in conf.tracker) + sids += key; + self.identifier = parseInt(sids); + conf.listener(event, self); + } + }; + /// + conf.onPointerMove = function(event, state) { + var bbox = conf.bbox; + var points = conf.tracker; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Update tracker coordinates. + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var pt = points[sid]; + // Check whether "pt" is used by another gesture. + if (!pt) + continue; + // Find the actual coordinates. + if (conf.position === "relative") { + pt.move.x = (touch.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; + pt.move.y = (touch.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; + } else { + pt.move.x = (touch.pageX - bbox.x1); + pt.move.y = (touch.pageY - bbox.y1); + } + } + /// + if (conf.fingers < conf.minFingers) + return; + /// + var touches = []; + var scale = 0; + var rotation = 0; + /// Calculate centroid of gesture. + var centroidx = 0; + var centroidy = 0; + var length = 0; + for (var sid in points) { + var touch = points[sid]; + if (touch.up) + continue; + centroidx += touch.move.x; + centroidy += touch.move.y; + length++; + } + centroidx /= length; + centroidy /= length; + /// + for (var sid in points) { + var touch = points[sid]; + if (touch.up) + continue; + var start = touch.start; + if (!start.distance) { + var dx = start.x - centroidx; + var dy = start.y - centroidy; + start.distance = Math.sqrt(dx * dx + dy * dy); + start.angle = Math.atan2(dx, dy) / RAD_DEG; + } + // Calculate scale. + var dx = touch.move.x - centroidx; + var dy = touch.move.y - centroidy; + var distance = Math.sqrt(dx * dx + dy * dy); + scale += distance / start.distance; + // Calculate rotation. + var angle = Math.atan2(dx, dy) / RAD_DEG; + var rotate = (start.angle - angle + 360) % 360 - 180; + touch.DEG2 = touch.DEG1; // Previous degree. + touch.DEG1 = rotate > 0 ? rotate : -rotate; // Current degree. + if (typeof(touch.DEG2) !== "undefined") { + if (rotate > 0) { + touch.rotation += touch.DEG1 - touch.DEG2; + } else { + touch.rotation -= touch.DEG1 - touch.DEG2; + } + rotation += touch.rotation; + } + // Attach current points to self. + touches.push(touch.move); + } + /// + self.touches = touches; + self.fingers = conf.fingers; + self.scale = scale / conf.fingers; + self.rotation = rotation / conf.fingers; + self.state = "change"; + conf.listener(event, self); + }; + conf.onPointerUp = function(event) { + // Remove tracking for touch. + var fingers = conf.fingers; + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + // Check whether fingers has dropped below minFingers. + if (fingers === conf.minFingers && conf.fingers < conf.minFingers) { + self.fingers = conf.fingers; + self.state = "end"; + conf.listener(event, self); + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.gesture = root.gesture; + + return root; + +})(Event.proxy); +/* + "Pointer" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: minFingers, maxFingers. + ---------------------------------------------------- + Event.add(window, "gesture", function(event, self) { + console.log(self.rotation, self.scale, self.fingers, self.state); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.pointerdown = + root.pointermove = + root.pointerup = function(conf) { + if (conf.target.isPointerEmitter) + return; + // Tracking the events. + var isDown = true; + conf.onPointerDown = function(event) { + isDown = false; + self.gesture = "pointerdown"; + conf.listener(event, self); + }; + conf.onPointerMove = function(event) { + self.gesture = "pointermove"; + conf.listener(event, self, isDown); + }; + conf.onPointerUp = function(event) { + isDown = true; + self.gesture = "pointerup"; + conf.listener(event, self, true); + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + Event.add(conf.target, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + // Return this object. + conf.target.isPointerEmitter = true; + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.pointerdown = root.pointerdown; + Event.Gesture._gestureHandlers.pointermove = root.pointermove; + Event.Gesture._gestureHandlers.pointerup = root.pointerup; + + return root; + +})(Event.proxy); +/* + "Device Motion" and "Shake" event proxy. + ---------------------------------------------------- + http://developer.android.com/reference/android/hardware/SensorEvent.html#values + ---------------------------------------------------- + Event.add(window, "shake", function(event, self) {}); + Event.add(window, "devicemotion", function(event, self) { + console.log(self.acceleration, self.accelerationIncludingGravity); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.shake = function(conf) { + // Externally accessible data. + var self = { + gesture: "devicemotion", + acceleration: {}, + accelerationIncludingGravity: {}, + target: conf.target, + listener: conf.listener, + remove: function() { + window.removeEventListener('devicemotion', onDeviceMotion, false); + } + }; + // Setting up local variables. + var threshold = 4; // Gravitational threshold. + var timeout = 1000; // Timeout between shake events. + var timeframe = 200; // Time between shakes. + var shakes = 3; // Minimum shakes to trigger event. + var lastShake = (new Date).getTime(); + var gravity = {x: 0, y: 0, z: 0}; + var delta = { + x: {count: 0, value: 0}, + y: {count: 0, value: 0}, + z: {count: 0, value: 0} + }; + // Tracking the events. + var onDeviceMotion = function(e) { + var alpha = 0.8; // Low pass filter. + var o = e.accelerationIncludingGravity; + gravity.x = alpha * gravity.x + (1 - alpha) * o.x; + gravity.y = alpha * gravity.y + (1 - alpha) * o.y; + gravity.z = alpha * gravity.z + (1 - alpha) * o.z; + self.accelerationIncludingGravity = gravity; + self.acceleration.x = o.x - gravity.x; + self.acceleration.y = o.y - gravity.y; + self.acceleration.z = o.z - gravity.z; + /// + if (conf.gesture === "devicemotion") { + conf.listener(e, self); + return; + } + var data = "xyz"; + var now = (new Date).getTime(); + for (var n = 0, length = data.length; n < length; n++) { + var letter = data[n]; + var ACCELERATION = self.acceleration[letter]; + var DELTA = delta[letter]; + var abs = Math.abs(ACCELERATION); + /// Check whether another shake event was recently registered. + if (now - lastShake < timeout) + continue; + /// Check whether delta surpasses threshold. + if (abs > threshold) { + var idx = now * ACCELERATION / abs; + var span = Math.abs(idx + DELTA.value); + // Check whether last delta was registered within timeframe. + if (DELTA.value && span < timeframe) { + DELTA.value = idx; + DELTA.count++; + // Check whether delta count has enough shakes. + if (DELTA.count === shakes) { + conf.listener(e, self); + // Reset tracking. + lastShake = now; + DELTA.value = 0; + DELTA.count = 0; + } + } else { + // Track first shake. + DELTA.value = idx; + DELTA.count = 1; + } + } + } + }; + // Attach events. + if (!window.addEventListener) + return; + window.addEventListener('devicemotion', onDeviceMotion, false); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.shake = root.shake; + + return root; + +})(Event.proxy); +/* + "Swipe" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: snap, threshold, maxFingers. + ---------------------------------------------------- + Event.add(window, "swipe", function(event, self) { + console.log(self.velocity, self.angle); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + var RAD_DEG = Math.PI / 180; + + root.swipe = function(conf) { + conf.snap = conf.snap || 90; // angle snap. + conf.threshold = conf.threshold || 1; // velocity threshold. + // Tracking the events. + conf.onPointerDown = function(event) { + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function(event) { + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var o = conf.tracker[sid]; + // Identifier defined outside of listener. + if (!o) + continue; + o.move.x = touch.pageX; + o.move.y = touch.pageY; + o.moveTime = (new Date).getTime(); + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + /// + var velocity1; + var velocity2 + var degree1; + var degree2; + /// Calculate centroid of gesture. + var start = {x: 0, y: 0}; + var endx = 0; + var endy = 0; + var length = 0; + /// + for (var sid in conf.tracker) { + var touch = conf.tracker[sid]; + var xdist = touch.move.x - touch.start.x; + var ydist = touch.move.y - touch.start.y; + + endx += touch.move.x; + endy += touch.move.y; + start.x += touch.start.x; + start.y += touch.start.y; + length++; + + + var distance = Math.sqrt(xdist * xdist + ydist * ydist); + var ms = touch.moveTime - touch.startTime; + var degree2 = Math.atan2(xdist, ydist) / RAD_DEG + 180; + var velocity2 = ms ? distance / ms : 0; + if (typeof(degree1) === "undefined") { + degree1 = degree2; + velocity1 = velocity2; + } else if (Math.abs(degree2 - degree1) <= 20) { + degree1 = (degree1 + degree2) / 2; + velocity1 = (velocity1 + velocity2) / 2; + } else { + return; + } + } + /// + if (velocity1 > conf.threshold) { + start.x /= length; + start.y /= length; + self.start = start; + self.x = endx / length; + self.y = endy / length; + self.angle = -((((degree1 / conf.snap + 0.5) >> 0) * conf.snap || 360) - 360); + self.velocity = velocity1; + self.fingers = conf.gestureFingers; + self.state = "swipe"; + conf.listener(event, self); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.swipe = root.swipe; + + return root; + +})(Event.proxy); +/* + "Tap" and "Longpress" event proxy. + ---------------------------------------------------- + CONFIGURE: delay (longpress), timeout (tap). + ---------------------------------------------------- + Event.add(window, "tap", function(event, self) { + console.log(self.fingers); + }); + ---------------------------------------------------- + multi-finger tap // touch an target for <= 250ms. + multi-finger longpress // touch an target for >= 500ms + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.tap = + root.longpress = function(conf) { + conf.delay = conf.delay || 500; + conf.timeout = conf.timeout || 250; + // Setting up local variables. + var timestamp, timeout; + // Tracking the events. + conf.onPointerDown = function(event) { + if (root.pointerStart(event, self, conf)) { + timestamp = (new Date).getTime(); + // Initialize event listeners. + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + // Make sure this is a "longpress" event. + if (conf.gesture !== "longpress") + return; + timeout = setTimeout(function() { + if (event.cancelBubble && ++event.bubble > 1) + return; + // Make sure no fingers have been changed. + var fingers = 0; + for (var key in conf.tracker) { + if (conf.tracker[key].end === true) + return; + if (conf.cancel) + return; + fingers++; + } + // Send callback. + self.state = "start"; + self.fingers = fingers; + conf.listener(event, self); + }, conf.delay); + } + }; + conf.onPointerMove = function(event) { + var bbox = conf.bbox; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i++) { + var touch = touches[i]; + var identifier = touch.identifier || Infinity; + var pt = conf.tracker[identifier]; + if (!pt) + continue; + if (conf.position === "relative") { + var x = (touch.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; + var y = (touch.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; + } else { + var x = (touch.pageX - bbox.x1); + var y = (touch.pageY - bbox.y1); + } + if (!(x > 0 && x < bbox.width && // Within target coordinates.. + y > 0 && y < bbox.height && + Math.abs(x - pt.start.x) <= 25 && // Within drift deviance. + Math.abs(y - pt.start.y) <= 25)) { + // Cancel out this listener. + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + conf.cancel = true; + return; + } + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + clearTimeout(timeout); + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + if (event.cancelBubble && ++event.bubble > 1) + return; + // Callback release on longpress. + if (conf.gesture === "longpress") { + if (self.state === "start") { + self.state = "end"; + conf.listener(event, self); + } + return; + } + // Cancel event due to movement. + if (conf.cancel) + return; + // Ensure delay is within margins. + if ((new Date).getTime() - timestamp > conf.timeout) + return; + // Send callback. + self.state = "tap"; + self.fingers = conf.gestureFingers; + conf.listener(event, self); + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.tap = root.tap; + Event.Gesture._gestureHandlers.longpress = root.longpress; + + return root; + +})(Event.proxy); +/* + "Mouse Wheel" event proxy. + ---------------------------------------------------- + Event.add(window, "wheel", function(event, self) { + console.log(self.state, self.wheelDelta); + }); + */ + +if (typeof(Event) === "undefined") + var Event = {}; +if (typeof(Event.proxy) === "undefined") + Event.proxy = {}; + +Event.proxy = (function(root) { + "use strict"; + + root.wheel = function(conf) { + // Configure event listener. + var interval; + var timeout = conf.timeout || 150; + var count = 0; + // Externally accessible data. + var self = { + gesture: "wheel", + state: "start", + wheelDelta: 0, + target: conf.target, + listener: conf.listener, + remove: function() { + conf.target[remove](type, onMouseWheel, false); + } + }; + // Tracking the events. + var onMouseWheel = function(event) { + event = event || window.event; + self.state = count++ ? "change" : "start"; + self.wheelDelta = event.detail ? event.detail * -20 : event.wheelDelta; + conf.listener(event, self); + clearTimeout(interval); + interval = setTimeout(function() { + count = 0; + self.state = "end"; + self.wheelDelta = 0; + conf.listener(event, self); + }, timeout); + }; + // Attach events. + var add = document.addEventListener ? "addEventListener" : "attachEvent"; + var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; + var type = Event.supports("mousewheel") ? "mousewheel" : "DOMMouseScroll"; + conf.target[add](type, onMouseWheel, false); + // Return this object. + return self; + }; + + Event.Gesture = Event.Gesture || {}; + Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; + Event.Gesture._gestureHandlers.wheel = root.wheel; + + return root; + +})(Event.proxy); + + /** * Wrapper around `console.log` (when available) * @param {Any} values Values to log @@ -2097,7 +4003,46 @@ fabric.Collection = { return new fabric.Point(rx, ry).addEquals(origin); } + + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + function transformPoint(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[1] * p.y, + t[2] * p.x + t[3] * p.y + ); + } + return new fabric.Point( + t[0] * p.x + t[1] * p.y + t[4], + t[2] * p.x + t[3] * p.y + t[5] + ); + } + /** + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + function invertTransform(t) { + var r = t.slice(), + a = 1 / (t[0] * t[3] - t[1] * t[2]); + r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; + var o = transformPoint({x: t[4], y: t[5]}, r); + r[4] = -o.x; + r[5] = -o.y; + return r + } + /** * A wrapper around Number#toFixed, which contrary to native method returns number, not string. * @static @@ -2535,6 +4480,8 @@ fabric.Collection = { fabric.util.degreesToRadians = degreesToRadians; fabric.util.radiansToDegrees = radiansToDegrees; fabric.util.rotatePoint = rotatePoint; + fabric.util.transformPoint = transformPoint; + fabric.util.invertTransform = invertTransform; fabric.util.toFixed = toFixed; fabric.util.getRandomInt = getRandomInt; fabric.util.falseFunction = falseFunction; @@ -5280,167 +7227,167 @@ fabric.util.string = { })(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Intersection) { - fabric.warn('fabric.Intersection is already defined'); - return; - } - - /** - * Intersection class - * @class fabric.Intersection - * @memberOf fabric - * @constructor - */ - function Intersection(status) { - this.status = status; - this.points = []; - } - - fabric.Intersection = Intersection; - - fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { - - /** - * Appends a point to intersection - * @param {fabric.Point} point - */ - appendPoint: function (point) { - this.points.push(point); - }, - - /** - * Appends points to intersection - * @param {Array} points - */ - appendPoints: function (points) { - this.points = this.points.concat(points); - } - }; - - /** - * Checks if one line intersects another - * @static - * @param {fabric.Point} a1 - * @param {fabric.Point} a2 - * @param {fabric.Point} b1 - * @param {fabric.Point} b2 - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { - var result, - ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), - ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), - u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); - if (u_b !== 0) { - var ua = ua_t / u_b, - ub = ub_t / u_b; - if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { - result = new Intersection("Intersection"); - result.points.push(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); - } - else { - result = new Intersection(); - } - } - else { - if (ua_t === 0 || ub_t === 0) { - result = new Intersection("Coincident"); - } - else { - result = new Intersection("Parallel"); - } - } - return result; - }; - - /** - * Checks if line intersects polygon - * @static - * @param {fabric.Point} a1 - * @param {fabric.Point} a2 - * @param {Array} points - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectLinePolygon = function(a1,a2,points){ - var result = new Intersection(), - length = points.length; - - for (var i = 0; i < length; i++) { - var b1 = points[i], - b2 = points[(i+1) % length], - inter = Intersection.intersectLineLine(a1, a2, b1, b2); - - result.appendPoints(inter.points); - } - if (result.points.length > 0) { - result.status = "Intersection"; - } - return result; - }; - - /** - * Checks if polygon intersects another polygon - * @static - * @param {Array} points1 - * @param {Array} points2 - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { - var result = new Intersection(), - length = points1.length; - - for (var i = 0; i < length; i++) { - var a1 = points1[i], - a2 = points1[(i+1) % length], - inter = Intersection.intersectLinePolygon(a1, a2, points2); - - result.appendPoints(inter.points); - } - if (result.points.length > 0) { - result.status = "Intersection"; - } - return result; - }; - - /** - * Checks if polygon intersects rectangle - * @static - * @param {Array} points - * @param {Number} r1 - * @param {Number} r2 - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { - var min = r1.min(r2), - max = r1.max(r2), - topRight = new fabric.Point(max.x, min.y), - bottomLeft = new fabric.Point(min.x, max.y), - inter1 = Intersection.intersectLinePolygon(min, topRight, points), - inter2 = Intersection.intersectLinePolygon(topRight, max, points), - inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), - inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), - result = new Intersection(); - - result.appendPoints(inter1.points); - result.appendPoints(inter2.points); - result.appendPoints(inter3.points); - result.appendPoints(inter4.points); - - if (result.points.length > 0) { - result.status = "Intersection"; - } - return result; - }; - -})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * Intersection class + * @class fabric.Intersection + * @memberOf fabric + * @constructor + */ + function Intersection(status) { + this.status = status; + this.points = []; + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { + + /** + * Appends a point to intersection + * @param {fabric.Point} point + */ + appendPoint: function (point) { + this.points.push(point); + }, + + /** + * Appends points to intersection + * @param {Array} points + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + } + }; + + /** + * Checks if one line intersects another + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {fabric.Point} b1 + * @param {fabric.Point} b2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { + var result, + ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (u_b !== 0) { + var ua = ua_t / u_b, + ub = ub_t / u_b; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection("Intersection"); + result.points.push(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection(); + } + } + else { + if (ua_t === 0 || ub_t === 0) { + result = new Intersection("Coincident"); + } + else { + result = new Intersection("Parallel"); + } + } + return result; + }; + + /** + * Checks if line intersects polygon + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {Array} points + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLinePolygon = function(a1,a2,points){ + var result = new Intersection(), + length = points.length; + + for (var i = 0; i < length; i++) { + var b1 = points[i], + b2 = points[(i+1) % length], + inter = Intersection.intersectLineLine(a1, a2, b1, b2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * Checks if polygon intersects another polygon + * @static + * @param {Array} points1 + * @param {Array} points2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { + var result = new Intersection(), + length = points1.length; + + for (var i = 0; i < length; i++) { + var a1 = points1[i], + a2 = points1[(i+1) % length], + inter = Intersection.intersectLinePolygon(a1, a2, points2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * Checks if polygon intersects rectangle + * @static + * @param {Array} points + * @param {Number} r1 + * @param {Number} r2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { + var min = r1.min(r2), + max = r1.max(r2), + topRight = new fabric.Point(max.x, min.y), + bottomLeft = new fabric.Point(min.x, max.y), + inter1 = Intersection.intersectLinePolygon(min, topRight, points), + inter2 = Intersection.intersectLinePolygon(topRight, max, points), + inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), + inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), + result = new Intersection(); + + result.appendPoints(inter1.points); + result.appendPoints(inter2.points); + result.appendPoints(inter3.points); + result.appendPoints(inter4.points); + + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + +})(typeof exports !== 'undefined' ? exports : this); (function(global) { @@ -6711,6 +8658,20 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ allowTouchScrolling: false, + /** + * The transformation (in the format of Canvas transform) which focuses the viewport + * @type Array + * @default + */ + viewportTransform: [1, 0, 0, 1, 0, 0], + + /** + * Color of canvas border + * @type String + * @default + */ + canvasBorderColor: '', + /** * Callback; invoked right before object is about to be scaled/rotated * @param {fabric.Object} target Object that's about to be scaled/rotated @@ -6999,6 +8960,68 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ return this; }, + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom: function () { + return sqrt(this.viewportTransform[0] * this.viewportTransform[3]); + }, + + /** + * Returns point at center of viewport + * @return {fabric.Point} the top left corner of the viewport + */ + getViewportCenter: function () { + var wh = fabric.util.transformPoint( + new fabric.Point(this.getWidth(), this.getHeight()), + this.viewportTransform + ), + x = this.viewportTransform[4], + y = this.viewportTransform[5]; + + return new fabric.Point(this.getWidth()/2 + x, this.getHeight()/2 + y); + }, + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom: function (value) { + // TODO: just change the scale, preserve other transformations + this.viewportTransform[0] = value; + this.viewportTransform[3] = value; + return this; + }, + + /** + * Centers viewport of this canvas instance on given point + * @param {Numer} x value for center of viewport + * @param {Numer} y value for center of viewport + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportCenter: function (x, y) { + var wh = fabric.util.transformPoint( + new fabric.Point(this.getWidth(), this.getHeight()), + this.viewportTransform + ); + this.viewportTransform[4] = x - wh.x/2; + this.viewportTransform[5] = y - wh.y/2; + return this; + }, + + /** + * Centers viewport of this canvas instance + * @return {fabric.Canvas} instance + * @chainable true + */ + centerViewport: function () { + return this.setViewportCenter(this.getWidth()/2, this.getHeight()/2); + }, + /** * Returns <canvas> element corresponding to this instance * @return {HTMLCanvasElement} @@ -7050,8 +9073,14 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ _onObjectAdded: function(obj) { this.stateful && obj.setupState(); - obj.setCoords(); obj.canvas = this; + obj.setCoords(); + if (obj._objects) { + for (var i = 0, len = obj._objects; i < len; i++) { + obj._objects[i].canvas = this; + obj._objects[i].setCoords(); + } + } this.fire('object:added', { target: obj }); obj.fire('added'); }, @@ -7153,6 +9182,10 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ if (typeof this.backgroundImage === 'object') { this._drawBackroundImage(canvasToDrawOn); } + + if (this.canvasBorderColor) { + this._drawCanvasBorder(canvasToDrawOn); + } var activeGroup = this.getActiveGroup(); for (var i = 0, length = this._objects.length; i < length; ++i) { @@ -7209,6 +9242,23 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ canvasToDrawOn.restore(); }, + /** + * @private + * @param {CanvasRenderingContext2D} canvasToDrawOn Context to render on + */ + _drawCanvasBorder: function(canvasToDrawOn) { + var xy = fabric.util.transformPoint(new fabric.Point(0, 0), this.viewportTransform), + wh = fabric.util.transformPoint( + new fabric.Point(this.getWidth(), this.getHeight()), + this.viewportTransform, true + ); + canvasToDrawOn.save(); + canvasToDrawOn.lineWidth = 1; + canvasToDrawOn.strokeStyle = this.canvasBorderColor; + canvasToDrawOn.strokeRect(xy.x - 1.5, xy.y - 1.5, wh.x + 2, wh.y + 2); + canvasToDrawOn.restore(); + }, + /** * Method to render only the top canvas. * Also used to render the group selection box. @@ -7959,6 +10009,10 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype * @private */ _getSVGPathData: function() { + var ivt = fabric.util.invertTransform(this.canvas.viewportTransform); + for (var i = 0, len = this._points.length; i < len; i++) { + this._points[i] = fabric.util.transformPoint(this._points[i], ivt); + } this.box = this.getPathBoundingBox(this._points); return this.convertPointsToSVGPath( this._points, this.box.minx, this.box.maxx, this.box.miny, this.box.maxy); @@ -8168,6 +10222,7 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric circles.push(circle); } var group = new fabric.Group(circles); + group.canvas = this.canvas; this.canvas.add(group); this.canvas.fire('path:created', { path: group }); @@ -8367,6 +10422,7 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric this.sprayChunkPoints = [ ]; var x, y, width, radius = this.width / 2; + var vpt = this.canvas.viewportTransform; for (var i = 0; i < this.density; i++) { @@ -8382,8 +10438,10 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric else { width = this.dotWidth; } - - var point = { x: x, y: y, width: width }; + + var point = fabric.point(x, y); + point = fabric.util.transformPoint(point, vpt); + point.width = width if (this.randomOpacity) { point.opacity = fabric.util.getRandomInt(0, 100) / 100; @@ -8705,7 +10763,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @return {Boolean} true if point is contained within an area of given object */ containsPoint: function (e, target) { - var pointer = this.getPointer(e), + var pointer = this.getPointer(e, true), xy = this._normalizePointer(target, pointer); // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html @@ -8719,7 +10777,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _normalizePointer: function (object, pointer) { var activeGroup = this.getActiveGroup(), x = pointer.x, - y = pointer.y; + y = pointer.y, + lt; var isObjectInGroup = ( activeGroup && @@ -8728,8 +10787,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab ); if (isObjectInGroup) { - x -= activeGroup.left; - y -= activeGroup.top; + lt = new fabric.Point(activeGroup.left, activeGroup.top); + lt = fabric.util.transformPoint(lt, this.viewportTransform, true); + x -= lt.x; + y -= lt.y; } return { x: x, y: y }; }, @@ -8838,7 +10899,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var action = 'drag', corner, - pointer = getPointer(e, target.canvas.upperCanvasEl); + pointer = getPointer(e, target.canvas.UpperCanvasEl); corner = target._findTargetCorner(e, this._offset); if (corner) { @@ -8954,6 +11015,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target); var group = new fabric.Group( isActiveLower ? [ target, this._activeObject ] : [ this._activeObject, target ]); + group.canvas = this; this.setActiveGroup(group); this._activeObject = null; @@ -9233,6 +11295,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } else if (group.length > 1) { group = new fabric.Group(group.reverse()); + group.canvas = this; this.setActiveGroup(group); group.saveCoords(); this.fire('selection:created', { target: group }); @@ -9249,7 +11312,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this.skipTargetFind) return; var target, - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); if (this.controlsAboveOverlay && this.lastRenderedObjectWithControlsAboveOverlay && @@ -9289,7 +11352,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } for (var j = 0, len = possibleTargets.length; j < len; j++) { - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); var isTransparent = this.isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); if (!isTransparent) { target = possibleTargets[j]; @@ -9306,8 +11369,18 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e * @return {Object} object with "x" and "y" number values */ - getPointer: function (e) { - var pointer = getPointer(e, this.upperCanvasEl); + getPointer: function (e, ignoreZoom, upperCanvasEl) { + if (!upperCanvasEl) { + upperCanvasEl = this.upperCanvasEl; + } + var pointer = getPointer(e, upperCanvasEl); + if (!ignoreZoom) { + pointer = fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + } + return { x: pointer.x - this._offset.left, y: pointer.y - this._offset.top @@ -9753,7 +11826,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } else { - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); } render = this._shouldRender(target, pointer); @@ -9801,7 +11874,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this.clipTo) { fabric.util.clipContext(this, this.contextTop); } - this.freeDrawingBrush.onMouseDown(this.getPointer(e)); + this.freeDrawingBrush.onMouseDown(this.getPointer(e, true)); this.fire('mouse:down', { e: e }); }, @@ -9827,7 +11900,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this._currentTransform) return; var target = this.findTarget(e), - pointer = this.getPointer(e), + pointer = this.getPointer(e, true), corner, render; @@ -9928,8 +12001,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this.isDrawingMode) { if (this._isCurrentlyDrawing) { - pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseMove(pointer); + this.freeDrawingBrush.onMouseMove(this.getPointer(e, true)); } this.upperCanvasEl.style.cursor = this.freeDrawingCursor; this.fire('mouse:move', { e: e }); @@ -9940,10 +12012,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab // We initially clicked in an empty area, so we draw a box for multiple selection. if (groupSelector) { - pointer = getPointer(e, this.upperCanvasEl); + pointer = this.getPointer(e, true); - groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; - groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; + groupSelector.left = pointer.x - groupSelector.ex; + groupSelector.top = pointer.y - groupSelector.ey; this.renderTop(); } else if (!this._currentTransform) { @@ -9968,7 +12040,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } else { // object is being transformed (scaled/rotated/moved/etc.) - pointer = getPointer(e, this.upperCanvasEl); + pointer = this.getPointer(e); var x = pointer.x, y = pointer.y, @@ -10523,6 +12595,83 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }); +(function() { + + var degreesToRadians = fabric.util.degreesToRadians, + radiansToDegrees = fabric.util.radiansToDegrees; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * Method that defines actions when an Event.js gesture is detected on an object. Currently only supports + * 2 finger gestures. + * + * @param e Event object by Event.js + * @param self Event proxy object by Event.js + */ + __onTransformGesture: function(e, self) { + + if (this.isDrawingMode || e.touches.length !== 2 || 'gesture' !== self.gesture) { + return; + } + + var target = this.findTarget(e); + if ('undefined' !== typeof target) { + this.onBeforeScaleRotate(target); + this._rotateObjectByAngle(self.rotation); + this._scaleObjectBy(self.scale); + } + + this.fire('touch:gesture', {target: target, e: e, self: self}); + }, + + /** + * Scales an object by a factor + * @param s {Number} The scale factor to apply to the current scale level + * @param by {String} Either 'x' or 'y' - specifies dimension constraint by which to scale an object. + * When not provided, an object is scaled by both dimensions equally + */ + _scaleObjectBy: function(s, by) { + var t = this._currentTransform, + target = t.target; + + var lockScalingX = target.get('lockScalingX'), + lockScalingY = target.get('lockScalingY'); + + if (lockScalingX && lockScalingY) return; + + target._scaling = true; + + if (!by) { + if (!lockScalingX) { + target.set('scaleX', t.scaleX * s); + } + if (!lockScalingY) { + target.set('scaleY', t.scaleY * s); + } + } + else if (by === 'x' && !target.get('lockUniScaling')) { + lockScalingX || target.set('scaleX', t.scaleX * s); + } + else if (by === 'y' && !target.get('lockUniScaling')) { + lockScalingY || target.set('scaleY', t.scaleY * s); + } + }, + + /** + * Rotates object by an angle + * @param curAngle {Number} the angle of rotation in degrees + */ + _rotateObjectByAngle: function(curAngle) { + var t = this._currentTransform; + + if (t.target.get('lockRotation')) return; + t.target.angle = radiansToDegrees(degreesToRadians(curAngle) + t.theta); + } + }); +})(); + + (function(global) { "use strict"; @@ -11537,11 +13686,24 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ctx.save(); var m = this.transformMatrix; + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + if (m && !this.group) { ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); } if (!noTransform) { + if (this.group) { + this.group.transform(ctx); + } this.transform(ctx); } @@ -11571,8 +13733,23 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this._render(ctx, noTransform); this.clipTo && ctx.restore(); this._removeShadow(ctx); + ctx.restore(); + ctx.save(); if (this.active && !noTransform) { + var center; + if (this.group) { + center = fabric.util.transformPoint(this.group.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.group.angle)); + } + center = fabric.util.transformPoint(this.getCenterPoint(), v, null != this.group); + if (this.group) { + center.x *= this.group.scaleX; + center.y *= this.group.scaleY; + } + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -12578,40 +14755,40 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var offsetX = Math.cos(_angle + theta) * _hypotenuse, offsetY = Math.sin(_angle + theta) * _hypotenuse, sinTh = Math.sin(theta), - cosTh = Math.cos(theta); - - var coords = this.getCenterPoint(); + cosTh = Math.cos(theta), + coords = this.getCenterPoint(), + wh = new fabric.Point(this.currentWidth, this.currentHeight); var tl = { x: coords.x - offsetX, y: coords.y - offsetY }; var tr = { - x: tl.x + (this.currentWidth * cosTh), - y: tl.y + (this.currentWidth * sinTh) + x: tl.x + (wh.x * cosTh), + y: tl.y + (wh.x * sinTh) }; var br = { - x: tr.x - (this.currentHeight * sinTh), - y: tr.y + (this.currentHeight * cosTh) + x: tr.x - (wh.y * sinTh), + y: tr.y + (wh.y * cosTh) }; var bl = { - x: tl.x - (this.currentHeight * sinTh), - y: tl.y + (this.currentHeight * cosTh) + x: tl.x - (wh.y * sinTh), + y: tl.y + (wh.y * cosTh) }; var ml = { - x: tl.x - (this.currentHeight/2 * sinTh), - y: tl.y + (this.currentHeight/2 * cosTh) + x: tl.x - (wh.y/2 * sinTh), + y: tl.y + (wh.y/2 * cosTh) }; var mt = { - x: tl.x + (this.currentWidth/2 * cosTh), - y: tl.y + (this.currentWidth/2 * sinTh) + x: tl.x + (wh.x/2 * cosTh), + y: tl.y + (wh.x/2 * sinTh) }; var mr = { - x: tr.x - (this.currentHeight/2 * sinTh), - y: tr.y + (this.currentHeight/2 * cosTh) + x: tr.x - (wh.y/2 * sinTh), + y: tr.y + (wh.y/2 * cosTh) }; var mb = { - x: bl.x + (this.currentWidth/2 * cosTh), - y: bl.y + (this.currentWidth/2 * sinTh) + x: bl.x + (wh.x/2 * cosTh), + y: bl.y + (wh.x/2 * sinTh) }; var mtr = { x: mt.x, @@ -12641,6 +14818,17 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati mtr: mtr }; + var vpt; + if (this.canvas) { + vpt = this.canvas.viewportTransform; + } + if (!vpt) { // TODO + vpt = [1, 0, 0, 1, 0, 0]; + } + for (c in this.oCoords) { + this.oCoords[c] = fabric.util.transformPoint(this.oCoords[c], vpt); + } + // set coordinates of the draggable boxes in the corners used to scale/rotate the image this._setCornerCoords && this._setCornerCoords(); @@ -12965,25 +15153,38 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot scaleY = 1 / this._constrainScale(this.scaleY); ctx.lineWidth = 1 / this.borderScaleFactor; - - ctx.scale(scaleX, scaleY); - - var w = this.getWidth(), - h = this.getHeight(); + + var vpt = this.canvas.viewportTransform; + // debugging + if (!vpt) { + vpt = [1, 0, 0, 1, 0, 0] + console.log("No vpt! interactivity", this.canvas, this.get('canvas'), this); + } + + var wh = fabric.util.transformPoint(new fabric.Point(this.getWidth(), this.getHeight()), vpt, true), + sxy = fabric.util.transformPoint(new fabric.Point(scaleX, scaleY), vpt, true), + w = wh.x, + h = wh.y, + sx= sxy.x, + sy= sxy.y; + if (this.get('group')) { + w = w * this.get('group').scaleX; + h = h * this.get('group').scaleY; + } ctx.strokeRect( - ~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) - 0.5, // offset needed to make lines look sharper - ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) - 0.5, - ~~(w + padding2 + strokeWidth * this.scaleX) + 1, // double offset needed to make lines look sharper - ~~(h + padding2 + strokeWidth * this.scaleY) + 1 + ~~(-(w / 2) - padding - strokeWidth / 2 * sx) - 0.5, // offset needed to make lines look sharper + ~~(-(h / 2) - padding - strokeWidth / 2 * sy) - 0.5, + ~~(w + padding2 + strokeWidth * sx) + 1, // double offset needed to make lines look sharper + ~~(h + padding2 + strokeWidth * sy) + 1 ); if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) { var rotateHeight = ( this.flipY - ? h + (strokeWidth * this.scaleY) + (padding * 2) - : -h - (strokeWidth * this.scaleY) - (padding * 2) + ? h + (strokeWidth * sx) + (padding * 2) + : -h - (strokeWidth * sy) - (padding * 2) ) / 2; ctx.beginPath(); @@ -12999,7 +15200,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * Draws corners of an object's bounding box. - * Requires public properties: width, height, scaleX, scaleY + * Requires public properties: width, height * Requires public options: cornerSize, padding * @param {CanvasRenderingContext2D} ctx Context to draw on * @return {fabric.Object} thisArg @@ -13011,99 +15212,95 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot var size = this.cornerSize, size2 = size / 2, strokeWidth2 = ~~(this.strokeWidth / 2), // half strokeWidth rounded down - left = -(this.width / 2), - top = -(this.height / 2), + wh = fabric.util.transformPoint(new fabric.Point(this.getWidth(), this.getHeight()), this.canvas.viewportTransform, true), + width = wh.x, + height = wh.y, + left = -(width / 2), + top = -(height / 2), _left, _top, - sizeX = size / this.scaleX, - sizeY = size / this.scaleY, - paddingX = this.padding / this.scaleX, - paddingY = this.padding / this.scaleY, - scaleOffsetY = size2 / this.scaleY, - scaleOffsetX = size2 / this.scaleX, - scaleOffsetSizeX = (size2 - size) / this.scaleX, - scaleOffsetSizeY = (size2 - size) / this.scaleY, - height = this.height, - width = this.width, + padding = this.padding, + scaleOffset = size2, + scaleOffsetSize = size2 - size, methodName = this.transparentCorners ? 'strokeRect' : 'fillRect', transparent = this.transparentCorners, isVML = typeof G_vmlCanvasManager !== 'undefined'; ctx.save(); - ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); + ctx.lineWidth = 1; ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; ctx.strokeStyle = ctx.fillStyle = this.cornerColor; // top-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; + _left = left - scaleOffset - strokeWidth2 - padding; + _top = top - scaleOffset - strokeWidth2 - padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // top-right - _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; + _left = left + width - scaleOffset + strokeWidth2 + padding; + _top = top - scaleOffset - strokeWidth2 - padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // bottom-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + _left = left - scaleOffset - strokeWidth2 - padding; + _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // bottom-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + _left = left + width + scaleOffsetSize + strokeWidth2 + padding; + _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); if (!this.get('lockUniScaling')) { // middle-top - _left = left + width/2 - scaleOffsetX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; + _left = left + width/2 - scaleOffset; + _top = top - scaleOffset - strokeWidth2 - padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // middle-bottom - _left = left + width/2 - scaleOffsetX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + _left = left + width/2 - scaleOffset; + _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // middle-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height/2 - scaleOffsetY; + _left = left + width + scaleOffsetSize + strokeWidth2 + padding; + _top = top + height/2 - scaleOffset; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); // middle-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height/2 - scaleOffsetY; + _left = left - scaleOffset - strokeWidth2 - padding; + _top = top + height/2 - scaleOffset; - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); } // middle-top-rotate if (this.hasRotatingPoint) { - _left = left + width/2 - scaleOffsetX; + _left = left + width/2 - scaleOffset; _top = this.flipY ? - (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) - : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); + (top + height + (this.rotatingPointOffset) - size2 + strokeWidth2 + padding) + : (top - (this.rotatingPointOffset) - size2 - strokeWidth2 - padding); - isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); + isVML || transparent || ctx.clearRect(_left, _top, size, size); + ctx[methodName](_left, _top, size, size); } ctx.restore(); @@ -15173,6 +17370,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.save(); var m = this.transformMatrix; + + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + if (m) { ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } @@ -15206,8 +17413,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._renderStroke(ctx); this.clipTo && ctx.restore(); this._removeShadow(ctx); + ctx.restore(); + ctx.save(); if (!noTransform && this.active) { + var center; + center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -15541,6 +17754,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.save(); var m = this.transformMatrix; + + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + if (m) { ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } @@ -15554,8 +17777,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } this.clipTo && ctx.restore(); this._removeShadow(ctx); + ctx.restore(); + ctx.save(); if (this.active) { + var center; + center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -15718,7 +17947,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot extend = fabric.util.object.extend, min = fabric.util.array.min, max = fabric.util.array.max, - invoke = fabric.util.array.invoke; + invoke = fabric.util.array.invoke, + degreesToRadians = fabric.util.degreesToRadians; if (fabric.Group) { return; @@ -15927,9 +18157,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (!this.visible) return; ctx.save(); - this.transform(ctx); + var v = this.canvas.viewportTransform; - var groupScaleFactor = Math.max(this.scaleX, this.scaleY); + var sxy = fabric.util.transformPoint( + new fabric.Point(this.scaleX, this.scaleY), + v, true), + groupScaleFactor = Math.max(sxy.x, sxy.y); this.clipTo && fabric.util.clipContext(this, ctx); @@ -15943,22 +18176,21 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot // do not render if object is not visible if (!object.visible) continue; - object.borderScaleFactor = groupScaleFactor; object.hasRotatingPoint = false; - object.render(ctx); - object.borderScaleFactor = originalScaleFactor; object.hasRotatingPoint = originalHasRotatingPoint; } this.clipTo && ctx.restore(); - if (!noTransform && this.active) { + if (this.active && !noTransform) { + var center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } ctx.restore(); - this.setCoords(); }, /** @@ -16106,9 +18338,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _calcBounds: function() { var aX = [], aY = [], - minX, minY, maxX, maxY, o, width, height, + minX, minY, maxX, maxY, o, width, height, minXY, maxXY, ivt, // TODO: cleanup i = 0, - len = this._objects.length; + len = this._objects.length, + vpt; + if (this.canvas) { + vpt = this.canvas.viewportTransform; + } + if (!vpt) { // TODO: this always happens when new groups are created + vpt = [1, 0, 0, 1, 0, 0]; + } for (; i < len; ++i) { o = this._objects[i]; @@ -16119,19 +18358,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } } - minX = min(aX); - maxX = max(aX); - minY = min(aY); - maxY = max(aY); + minXY = new fabric.Point(min(aX), min(aY)); + maxXY = new fabric.Point(max(aX), max(aY)); - width = (maxX - minX) || 0; - height = (maxY - minY) || 0; + ivt = fabric.util.invertTransform(vpt); + this.width = (maxXY.x - minXY.x) || 0; + this.height = (maxXY.y - minXY.y) || 0; - this.width = width; - this.height = height; + minXY = fabric.util.transformPoint(minXY, ivt); + maxXY = fabric.util.transformPoint(maxXY, ivt); + this.width = (maxXY.x - minXY.x) || 0; + this.height = (maxXY.y - minXY.y) || 0; - this.left = (minX + width / 2) || 0; - this.top = (minY + height / 2) || 0; + this.left = (minXY.x + maxXY.x) / 2 || 0; + this.top = (minXY.y + maxXY.y) / 2 || 0; }, /* _TO_SVG_START_ */ @@ -16315,8 +18555,17 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.save(); var m = this.transformMatrix; + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } var isInPathGroup = this.group && this.group.type !== 'group'; + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + // this._resetWidthHeight(); if (isInPathGroup) { ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2); @@ -16338,8 +18587,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._renderStroke(ctx); this.clipTo && ctx.restore(); ctx.restore(); + ctx.restore(); + ctx.save(); if (this.active && !noTransform) { + var center; + if (this.group) { + center = fabric.util.transformPoint(this.group.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.group.angle)); + } + center = fabric.util.transformPoint(this.getCenterPoint(), v, null != this.group); + if (this.group) { + center.x *= this.group.scaleX; + center.y *= this.group.scaleY; + } + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -18602,8 +20866,22 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag if (!this.visible) return; ctx.save(); + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); this._render(ctx); + ctx.restore(); + ctx.save(); if (!noTransform && this.active) { + var center; + center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } diff --git a/src/brushes/circle_brush.class.js b/src/brushes/circle_brush.class.js index bebebde2..f790b6d1 100644 --- a/src/brushes/circle_brush.class.js +++ b/src/brushes/circle_brush.class.js @@ -76,6 +76,7 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric circles.push(circle); } var group = new fabric.Group(circles); + group.canvas = this.canvas; this.canvas.add(group); this.canvas.fire('path:created', { path: group }); diff --git a/src/brushes/pencil_brush.class.js b/src/brushes/pencil_brush.class.js index 9a42186a..464c5c4a 100644 --- a/src/brushes/pencil_brush.class.js +++ b/src/brushes/pencil_brush.class.js @@ -141,6 +141,10 @@ * @private */ _getSVGPathData: function() { + var ivt = fabric.util.invertTransform(this.canvas.viewportTransform); + for (var i = 0, len = this._points.length; i < len; i++) { + this._points[i] = fabric.util.transformPoint(this._points[i], ivt); + } this.box = this.getPathBoundingBox(this._points); return this.convertPointsToSVGPath( this._points, this.box.minx, this.box.maxx, this.box.miny, this.box.maxy); diff --git a/src/brushes/spray_brush.class.js b/src/brushes/spray_brush.class.js index aac186d2..4ff8fd9a 100644 --- a/src/brushes/spray_brush.class.js +++ b/src/brushes/spray_brush.class.js @@ -110,6 +110,8 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric } var group = new fabric.Group(rects); + group.canvas = this.canvas; + this.canvas.add(group); this.canvas.fire('path:created', { path: group }); @@ -145,9 +147,13 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric var ctx = this.canvas.contextTop; ctx.fillStyle = this.color; ctx.save(); + var ivt = fabric.util.invertTransform(this.canvas.viewportTransform); for (var i = 0, len = this.sprayChunkPoints.length; i < len; i++) { var point = this.sprayChunkPoints[i]; + var tpoint = fabric.util.transformPoint({x: point.x, y: point.y}, ivt); + point.x = tpoint.x; + point.y = tpoint.y; if (typeof point.opacity !== 'undefined') { ctx.globalAlpha = point.opacity; } @@ -163,6 +169,7 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric this.sprayChunkPoints = [ ]; var x, y, width, radius = this.width / 2; + var vpt = this.canvas.viewportTransform; for (var i = 0; i < this.density; i++) { @@ -178,8 +185,10 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric else { width = this.dotWidth; } - - var point = { x: x, y: y, width: width }; + + var point = new fabric.Point(x, y); + point = fabric.util.transformPoint(point, vpt); + point.width = width if (this.randomOpacity) { point.opacity = fabric.util.getRandomInt(0, 100) / 100; diff --git a/src/canvas.class.js b/src/canvas.class.js index a2c12606..04990ac8 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -249,7 +249,7 @@ * @return {Boolean} true if point is contained within an area of given object */ containsPoint: function (e, target) { - var pointer = this.getPointer(e), + var pointer = this.getPointer(e, true), xy = this._normalizePointer(target, pointer); // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html @@ -504,6 +504,7 @@ var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target); var group = new fabric.Group( isActiveLower ? [ target, this._activeObject ] : [ this._activeObject, target ]); + group.canvas = this; this.setActiveGroup(group); this._activeObject = null; @@ -783,6 +784,7 @@ } else if (group.length > 1) { group = new fabric.Group(group.reverse()); + group.canvas = this; this.setActiveGroup(group); group.saveCoords(); this.fire('selection:created', { target: group }); @@ -799,7 +801,7 @@ if (this.skipTargetFind) return; var target, - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); if (this.controlsAboveOverlay && this.lastRenderedObjectWithControlsAboveOverlay && @@ -839,7 +841,7 @@ } } for (var j = 0, len = possibleTargets.length; j < len; j++) { - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); var isTransparent = this.isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); if (!isTransparent) { target = possibleTargets[j]; @@ -856,8 +858,17 @@ * @param {Event} e * @return {Object} object with "x" and "y" number values */ - getPointer: function (e) { - var pointer = getPointer(e, this.upperCanvasEl); + getPointer: function (e, ignoreZoom, upperCanvasEl) { + if (!upperCanvasEl) { + upperCanvasEl = this.upperCanvasEl; + } + var pointer = getPointer(e, upperCanvasEl); + if (!ignoreZoom) { + pointer = fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + } return { x: pointer.x - this._offset.left, diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index 330d2877..077fab1d 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -187,7 +187,7 @@ } } else { - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); } render = this._shouldRender(target, pointer); @@ -235,7 +235,7 @@ if (this.clipTo) { fabric.util.clipContext(this, this.contextTop); } - this.freeDrawingBrush.onMouseDown(this.getPointer(e)); + this.freeDrawingBrush.onMouseDown(this.getPointer(e, true)); this.fire('mouse:down', { e: e }); }, @@ -261,7 +261,7 @@ if (this._currentTransform) return; var target = this.findTarget(e), - pointer = this.getPointer(e), + pointer = this.getPointer(e, true), corner, render; @@ -362,8 +362,7 @@ if (this.isDrawingMode) { if (this._isCurrentlyDrawing) { - pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseMove(pointer); + this.freeDrawingBrush.onMouseMove(this.getPointer(e, true)); } this.upperCanvasEl.style.cursor = this.freeDrawingCursor; this.fire('mouse:move', { e: e }); @@ -374,10 +373,10 @@ // We initially clicked in an empty area, so we draw a box for multiple selection. if (groupSelector) { - pointer = getPointer(e, this.upperCanvasEl); + pointer = this.getPointer(e, true); - groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; - groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; + groupSelector.left = pointer.x - groupSelector.ex; + groupSelector.top = pointer.y - groupSelector.ey; this.renderTop(); } else if (!this._currentTransform) { diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js index 3deff055..c929a437 100644 --- a/src/mixins/object_geometry.mixin.js +++ b/src/mixins/object_geometry.mixin.js @@ -306,10 +306,21 @@ var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, padding = this.padding, - theta = degreesToRadians(this.angle); + theta = degreesToRadians(this.angle), + vpt; + if (this.canvas) { + vpt = this.canvas.viewportTransform; + } + if (!vpt) { // TODO + vpt = [1, 0, 0, 1, 0, 0]; + }; - this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; - this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; + var f = function (p) { + return fabric.util.transformPoint(p, vpt); + } + + this.currentWidth = (this.width + strokeWidth) * this.scaleX; + this.currentHeight = (this.height + strokeWidth) * this.scaleY; // If width is negative, make postive. Fixes path selection issue if (this.currentWidth < 0) { @@ -329,42 +340,32 @@ cosTh = Math.cos(theta), coords = this.getCenterPoint(), wh = new fabric.Point(this.currentWidth, this.currentHeight); - var tl = { - x: coords.x - offsetX, - y: coords.y - offsetY - }; - var tr = { - x: tl.x + (wh.x * cosTh), - y: tl.y + (wh.x * sinTh) - }; - var br = { - x: tr.x - (wh.y * sinTh), - y: tr.y + (wh.y * cosTh) - }; - var bl = { - x: tl.x - (wh.y * sinTh), - y: tl.y + (wh.y * cosTh) - }; - var ml = { - x: tl.x - (wh.y/2 * sinTh), - y: tl.y + (wh.y/2 * cosTh) - }; - var mt = { - x: tl.x + (wh.x/2 * cosTh), - y: tl.y + (wh.x/2 * sinTh) - }; - var mr = { - x: tr.x - (wh.y/2 * sinTh), - y: tr.y + (wh.y/2 * cosTh) - }; - var mb = { - x: bl.x + (wh.x/2 * cosTh), - y: bl.y + (wh.x/2 * sinTh) - }; - var mtr = { - x: mt.x, - y: mt.y - }; + var _tl = new fabric.Point(coords.x - offsetX, coords.y - offsetY); + var _tr = new fabric.Point(_tl.x + (wh.x * cosTh), _tl.y + (wh.x * sinTh)); + var _bl = new fabric.Point(_tl.x - (wh.y * sinTh), _tl.y + (wh.y * cosTh)); + var _mt = new fabric.Point(_tl.x + (wh.x/2 * cosTh), _tl.y + (wh.x/2 * sinTh)); + var tl = f(_tl); + var tr = f(_tr); + var br = f(new fabric.Point(_tr.x - (wh.y * sinTh), _tr.y + (wh.y * cosTh))); + var bl = f(_bl); + var ml = f(new fabric.Point(_tl.x - (wh.y/2 * sinTh), _tl.y + (wh.y/2 * cosTh))); + var mt = f(_mt); + var mr = f(new fabric.Point(_tr.x - (wh.y/2 * sinTh), _tr.y + (wh.y/2 * cosTh))); + var mb = f(new fabric.Point(_bl.x + (wh.x/2 * cosTh), _bl.y + (wh.x/2 * sinTh))); + var mtr = f(new fabric.Point(_mt.x, _mt.y)); + + // padding + var padX = Math.cos(_angle + theta) * this.padding * Math.sqrt(2), + padY = Math.sin(_angle + theta) * this.padding * Math.sqrt(2); + tl = tl.add(new fabric.Point(-padX, -padY)); + tr = tr.add(new fabric.Point(padY, -padX)); + br = br.add(new fabric.Point(padX, padY)); + bl = bl.add(new fabric.Point(-padY, padX)); + ml = ml.add(new fabric.Point((-padX - padY) / 2, (-padY + padX) / 2)); + mt = mt.add(new fabric.Point((padY - padX) / 2, -(padY + padX) / 2)); + mr = mr.add(new fabric.Point((padY + padX) / 2, (padY - padX) / 2)); + mb = mb.add(new fabric.Point((padX - padY) / 2, (padX + padY) / 2)); + mtr = mtr.add(new fabric.Point((padY - padX) / 2, -(padY + padX) / 2)); // debugging @@ -389,22 +390,6 @@ mtr: mtr }; - var tform; - if (typeof this.canvas == 'undefined') { - if (this.type == 'group') { - tform = this._objects[0].canvas.viewportTransform; - } - else { - tform = [1, 0, 0, 1, 0, 0]; - } - } - else { - tform = this.canvas.viewportTransform; - } - for (c in this.oCoords) { - this.oCoords[c] = fabric.util.transformPoint(this.oCoords[c], tform); - } - // set coordinates of the draggable boxes in the corners used to scale/rotate the image this._setCornerCoords && this._setCornerCoords(); diff --git a/src/mixins/object_interactivity.mixin.js b/src/mixins/object_interactivity.mixin.js index 458e91b5..ba5e7e5f 100644 --- a/src/mixins/object_interactivity.mixin.js +++ b/src/mixins/object_interactivity.mixin.js @@ -15,9 +15,9 @@ _findTargetCorner: function(e, offset) { if (!this.hasControls || !this.active) return false; - var pointer = getPointer(e, this.canvas.upperCanvasEl), - ex = pointer.x - offset.left, - ey = pointer.y - offset.top, + var pointer = this.canvas.getPointer(e, true), + ex = pointer.x, + ey = pointer.y, xPoints, lines; @@ -267,15 +267,16 @@ ctx.lineWidth = 1 / this.borderScaleFactor; - var wh = fabric.util.transformPoint(new fabric.Point(this.getWidth(), this.getHeight()), this.canvas.viewportTransform, true), - sxy = fabric.util.transformPoint(new fabric.Point(scaleX, scaleY), this.canvas.viewportTransform, true), + var vpt = this.canvas.viewportTransform, + wh = fabric.util.transformPoint(new fabric.Point(this.getWidth(), this.getHeight()), vpt, true), + sxy = fabric.util.transformPoint(new fabric.Point(scaleX, scaleY), vpt, true), w = wh.x, h = wh.y, sx= sxy.x, sy= sxy.y; - if (this.get('group')) { - w = w * this.get('group').scaleX; - h = h * this.get('group').scaleY; + if (this.group) { + w = w * this.group.scaleX; + h = h * this.group.scaleY; } ctx.strokeRect( diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 89c0fd90..4de2cf60 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -53,6 +53,7 @@ this._objects = objects || []; for (var i = this._objects.length; i--; ) { this._objects[i].group = this; + this._objects[i].setCoords(); } this.originalState = { }; @@ -216,13 +217,7 @@ if (!this.visible) return; ctx.save(); - var v; - if (this.canvas) { - v = this.canvas.viewportTransform; - } - else { - v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution - } + var v = this.canvas.viewportTransform; var sxy = fabric.util.transformPoint( new fabric.Point(this.scaleX, this.scaleY), @@ -403,10 +398,9 @@ _calcBounds: function() { var aX = [], aY = [], - minX, minY, maxX, maxY, o, width, height, minXY, maxXY, ivt, // TODO: cleanup + minX, minY, maxX, maxY, o, width, height, minXY, maxXY, i = 0, - len = this._objects.length, - canvas = this._objects[0].canvas; + len = this._objects.length; for (; i < len; ++i) { o = this._objects[i]; @@ -416,20 +410,18 @@ aY.push(o.oCoords[prop].y); } } + + var ivt = fabric.util.invertTransform(canvas.viewportTransform); minXY = new fabric.Point(min(aX), min(aY)); maxXY = new fabric.Point(max(aX), max(aY)); - // TODO: cleanup - ivt = fabric.util.invertTransform(canvas.viewportTransform); - this.width = (maxXY.x - minXY.x) || 0; - this.height = (maxXY.y - minXY.y) || 0; - // TODO: cleanup minXY = fabric.util.transformPoint(minXY, ivt); maxXY = fabric.util.transformPoint(maxXY, ivt); + this.width = (maxXY.x - minXY.x) || 0; this.height = (maxXY.y - minXY.y) || 0; - + this.left = (minXY.x + maxXY.x) / 2 || 0; this.top = (minXY.y + maxXY.y) / 2 || 0; }, diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 764c72cb..dc6129f3 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -730,6 +730,9 @@ * @param {Boolean} fromLeft When true, context is transformed to object's top/left corner. This is used when rendering text on Node */ transform: function(ctx, fromLeft) { + if (this.group) { + this.group.transform(ctx, fromLeft); + } ctx.globalAlpha = this.opacity; var center = fromLeft ? this._getLeftTopCoords() : this.getCenterPoint(); @@ -1027,9 +1030,6 @@ } if (!noTransform) { - if (this.group) { - this.group.transform(ctx); - } this.transform(ctx); } diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index a7011f98..9e22540b 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -146,13 +146,6 @@ */ viewportTransform: [1, 0, 0, 1, 0, 0], - /** - * Color of canvas border - * @type String - * @default - */ - canvasBorderColor: '', - /** * Callback; invoked right before object is about to be scaled/rotated * @param {fabric.Object} target Object that's about to be scaled/rotated @@ -474,6 +467,10 @@ // TODO: just change the scale, preserve other transformations this.viewportTransform[0] = value; this.viewportTransform[3] = value; + this.renderAll(); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].setCoords(); + } return this; }, @@ -491,6 +488,7 @@ ); this.viewportTransform[4] = x - wh.x/2; this.viewportTransform[5] = y - wh.y/2; + this.renderAll(); return this; }, @@ -554,8 +552,14 @@ */ _onObjectAdded: function(obj) { this.stateful && obj.setupState(); - obj.setCoords(); obj.canvas = this; + obj.setCoords(); + if (obj._objects) { + for (var i = 0, len = obj._objects.length; i < len; i++) { + obj._objects[i].canvas = this; + obj._objects[i].setCoords(); + } + } this.fire('object:added', { target: obj }); obj.fire('added'); }, @@ -657,10 +661,6 @@ if (typeof this.backgroundImage === 'object') { this._drawBackroundImage(canvasToDrawOn); } - - if (this.canvasBorderColor) { - this._drawCanvasBorder(canvasToDrawOn); - } var activeGroup = this.getActiveGroup(); for (var i = 0, length = this._objects.length; i < length; ++i) { @@ -717,23 +717,6 @@ canvasToDrawOn.restore(); }, - /** - * @private - * @param {CanvasRenderingContext2D} canvasToDrawOn Context to render on - */ - _drawCanvasBorder: function(canvasToDrawOn) { - var xy = fabric.util.transformPoint(new fabric.Point(0, 0), this.viewportTransform), - wh = fabric.util.transformPoint( - new fabric.Point(this.getWidth(), this.getHeight()), - this.viewportTransform, true - ); - canvasToDrawOn.save(); - canvasToDrawOn.lineWidth = 1; - canvasToDrawOn.strokeStyle = this.canvasBorderColor; - canvasToDrawOn.strokeRect(xy.x - 1.5, xy.y - 1.5, wh.x + 2, wh.y + 2); - canvasToDrawOn.restore(); - }, - /** * Method to render only the top canvas. * Also used to render the group selection box.