Get pan direction on panend hammer.js and restrict directions - javascript

I am using a hammer.js library with its jQuery plugin. I started using it as suggested by documentation, so something like this to initiate it on .game-card-mobile divs
/* Create Hammer object for swipable game cards */
var $gameCard = $('.game-card-mobile');
var $gameCardTouch = $gameCard.hammer();
$gameCardTouch.on("panright press panleft", function(ev) {
console.log(ev.type);
});
This allows me to restrict available actions to pan/swipe element to the right, left and press on it, however during one event, lets say panright I would get many entries printed out in console although only one pan was performed. So I then changed initiation to this:
$gameCardTouch.on("panend", function(ev) {
console.log(ev.type);
});
Now it listens to panend action which occur at the end of the action, thus is only returning one printout in console, however it is always panend now, thus I lost restriction to only 3 previous actions and am unable to detect what specific action was performed.
Is there a way to combine these so I would get a print out of panright if users swiped right, panleft if swiped left and press, all just one time once they finish that action?

Just using a combination of the panright,press, and panleft with the panend could work for you.
(function(){
var direction;
var $gameCard = $('.game-card-mobile');
var $gameCardTouch = $gameCard.hammer();
$gameCardTouch.on("panright press panleft", function(ev) {
//Set the direction in here
direction = ev.type;
});
$gameCardTouch.on("panend", function(ev) {
//Use the direction in here
//You know the pan has ended
//and you know which action they were taking
console.log(direction);
//So do what ever here
if(direction === "whatever") ...
});
}());

To expand a bit on what Jack was getting at. Perhaps a simpler solution...
ev.eventObject returns integer values
https://hammerjs.github.io/api/#event-object
In the case of ev.direction:
no movement = 0
left = 2
right = 4
up = 8
down = 16
horizontal = 6
vertical = 24
all = 30
Clean
var mc = new Hammer(body);
mc.on("panend", function(ev) {
if (ev.direction == INT) {
//do something
}
});
Verbosely
var mc = new Hammer(body);
mc.on("panend", function(ev) {
//console.log(ev.direction);
//for left case
if (ev.direction == 2) {
//do something
}
//for right case
if (ev.direction == 4) {
//do something
}
});

I'm on my phone, so this is just pseudo code, but can't you do something like this:
var eventHandlers = {
panright: function(){},
press: function(){},
panleft:function(){}
}
$gameCardTouch.on("panright press panleft", function(ev) {
if(ev.type === 'panright') {
eventHandlers.panright();
} else if((ev.type === 'panleft'){
eventHandlers.panright();
} else {
eventHandlers.press();
}
This example from the website is similar: http://codepen.io/jtangelder/pen/ABFnd

Was working on a similar problem using Backbone, jQuery, and Hammer. Here's how I solved it (only posting relevant bits). Hopefully someone finds this useful.
var Slideshow = Backbone.View.extend( {
events: {
"pan .slideshow-slide" : "handlePan",
"panstart .slideshow-slide" : "handlePanStart",
"panend .slideshow-slide" : "handlePanEnd",
},
state: {
panHistory: [],
},
handlePanStart: function( evt ) {
// attempt to get the pan x coord
var startX = this.getPanXFromEvent( evt );
// if an x coord couldn't be retrieved, get out
if ( !startX ) {
this.logger.warn( "Pan Start: Unable to find x", evt );
return;
}
this.logger.log( "Pan Start", evt );
// set the pan x array so this is its first and only element
this.state.panHistory = [ startX ];
},
handlePan: function( evt ) {
// cache the pan history
var pans = this.state.panHistory,
// get the x coord from this pan event
lastX = this.getPanXFromEvent( evt );
// track deltas on the x axis during the pan, so we know if the user
// is switching directions between pan start and pan end
if ( lastX ) {
pans.push( lastX );
}
},
handlePanEnd: function( evt ) {
// get the direction of the pan
switch ( this.getDirectionFromPanEvent( evt ) ) {
case Hammer.DIRECTION_LEFT:
// if we panned left and the next slide isn't out of
// range, go to the next slide.. otherwise fall back
// on the switch statement's default
if ( !this.isOutOfRange( this.state.current + 1 ) ) {
this.logger.log( "Pan End: Moving Left", evt );
this.gotoNextSlide();
return;
}
case Hammer.DIRECTION_RIGHT:
// if we panned right and the previous slide isn't out
// of range, go to the previous slide.. otherwise fall
// back on the switch statement's default
if ( !this.isOutOfRange( this.state.current - 1 ) ) {
this.logger.log( "Pan End: Moving Right", evt );
this.gotoPrevSlide();
return;
}
// Snap back to the current slide by default by calling
// gotoSlide on the current index to reset the transform
default:
this.logger.log( "Pan End: Snapping Back", evt );
this.gotoSlide( this.state.current );
}
},
getPanXFromEvent: function( evt ) {
return Utils.getValue( "originalEvent.gesture.center.x", evt );
},
getDirectionFromPanHistory: function( evt ) {
// placeholders for start, end, and last pan x coords
var i, start, last, end,
// cache the pan x array so we don't have to type it 50 times
pans = this.state.panHistory;
// if there aren't enough pans to calculate a delta, return 0
if ( pans.length < 2 ) {
return 0;
}
// get the starting pan x
start = pans[ 0 ];
// set last and end to the last pan x
last = end = pans[ pans.length - 1 ];
// loop backwards through the pans to find a pan x coord different from the ending one
// since there's a chance that identical pan x coords will be stacked in the array
for ( i = pans.length - 2; last === end && i >= 0; i-- ) {
last = pans[ i ];
}
// if the last pan was to the right, and we're farther right
// than we started, move right
return end > last && end > start ? Hammer.DIRECTION_RIGHT
// if the last pan was to the left, and we're farther left
// than we started, move left
: end < last && end < start ? Hammer.DIRECTION_LEFT
// otherwise move nowhere
: 0;
},
} );

Related

Paper.js Background Rasterization Glitches

We are building an image editor of sorts using Paper.js. We have a queue on the side of the Paper.js canvas that allows switching between images. Each time we switch between images, we want to flatten all the annotations (rasterize) onto the image just being edited.
Each time we switch images, this method is called, which rasterizes the current image and annotations to a data URL. (If we revisit this image, a raster from this data URL will be displayed.)
var flattenToDataUrl = function() {
layerAsRaster = paper.project.layers[0].rasterize(); // Layer to Paper.js Raster object
layerAsRaster.visible = false; // Attempt to set not visible
var dataString = layerAsRaster.toDataURL();
return dataString;
};
Then we end up calling this method, which changes out the image we're editing:
var setCanvasImage = function(imageObject) {
if(imageObject != null)
{
imageHeight = imageObject.height;
var imageWidth = imageObject.width;
// Set up HTMLImage
var imageElement = new Image(imageObject.width, imageObject.height);
if(_.has(imageObject, 'imageData')) { // Came as 64 bit data
imageElement.src = 'data:image/png;base64,' + imageObject.imageData;
} else if(_.has(imageObject, 'imageUrl')) { // Came as URL
imageElement.src = imageObject.imageUrl;
}
// Add image to Paper.js canvas
imageElement.onload = function(event) {
// Initialize Paper.js on the canvas
paper.setup(canvas);
raster = new paper.Raster(imageElement, new paper.Point(canvas.width / 2, canvas.height / 2));
setUpNotes();
selectedItems = new paper.Group(); // Since Paper.js has been setup we can now initialize this Group
registerCanvasEvents(); // Panning, zooming, moving selected items, deselecting all selected items
fitToPage();
};
}
};
So, this changes out the image, but when I move my mouse into the canvas after selecting a different image in the queue, it glitches to the image we were just on (with its annotations) until I do something like pan, zoom, etc. Then I see the image I selected and am truly working with.
Removing the flattenToDataUrl() functionality makes the queue work seamlessly. So it seems to me something is fishy there. We are generating a Paper.js Raster object in that method. Rasters seem to automatically add themselves. I attempt to curb this with a call to
layerAsRaster.visible = false;
but to no avail.
What is causing this glitchy behavior and how do I prevent it?
Update
For clarity (hopefully) and completeness, I've decided to post the whole PaperFunctions class we use in conjunction with React, which hosts our <canvas> element. There's a lot of code, and a lot of cleanup to do, especially in registerCanvasEvents(). Bear with this learning beginner. Also it's several hundred lines, so it may be helpful to paste it into your favorite editor. Entry points include setCanvas which is called in componentDidMount of the React class with the <canvas> element, and canvasSetImage which is called from the queue. I agree from bmacnaughton's answer that it's weird to call paper.setup(canvas) every time we load a new image. I'm currently investigating the right solution to this, the right place to put it. setCanvas seems logical but when I drag the image to move it in that setup, it leaves a trail of images in its wake. Anyway, here's PaperFunctions.js:
var JQueryMousewheel = require('jquery-mousewheel')($);
var SimplePanAndZoom = require('./SimplePanAndZoom.js');
var panAndZoom = new SimplePanAndZoom();
var selectedItems;
// We use selection here in two distinct ways.
// An item may be Paper.js selected but not in the selection group.
// This is because we want to show it is selectable.
// A blue bounding box indicates it is selectable.
// A green bounding box indicates it has actually been selected and added to selectedItems.
// Only things in selectedItems are actually operated on.
// So the event handlers in this method basically set up whether or not the item is in selectedItems (and therefore will be operated on for moving, resizing, deleting, etc.).
// That is, the event handlers here are concerned with displaying to the user the status of selection for the item - whether or not it will be operated on when events actually happen on the selectedItems Group.
var registerItemEvents = function(item) {
// Boolean flag for mouseup to know if was drag or click
var dragged;
// For tracking if dragging or clicking is happening
item.on('mousedown', function(e) {
dragged = false;
});
// On click toggle selection
item.on('mouseup', function(event) {
event.stopPropagation(); // Only for item applied to
event.preventDefault();
if(!dragged) {
var justAdded = addIfNotInSelectedItems(item);
if(!justAdded) { // Item was in selection group, remove it
item.remove();
paper.project.activeLayer.addChild(item);
this.selectedColor = paper.project.activeLayer.selectedColor;
//item.selected = false;
}
}
});
// Show as selectable even after has been deselected
item.on('mousemove', function(event) {
this.selected = true;
})
// If not selected, on mouse enter show that it is selectable
item.on('mouseenter', function(event) {
if(!this.selected) {
this.selected = true;
}
});
// If not selected, on mouse leave remove indicator that is selectable
item.on('mouseleave', function(event) {
var isInSelectedItems = selectedItems.getItem(item);
if(this.selected && isInSelectedItems == null) {
this.selected = false;
}
});
// On drag, move item
item.on('mousedrag', function(event) {
dragged = true;
// If user starts dragging automatically select the item
addIfNotInSelectedItems(item);
});
}
var addIfNotInSelectedItems = function(item) {
var isInSelectedItems = selectedItems.getItem(item);
if(isInSelectedItems == null) { // Item not currently in selection group, add it
selectedItems.addChild(item);
item.selectedColor = 'green';
item.selected = true;
return true; // Was added, return true
} else {
return false; // Already in, return false
}
}
var registerCanvasEvents = function() {
if(paper.view != null && canvas != null) {
// Zoom on mousewheel
$(canvas).mousewheel(function(event) {
event.preventDefault();
var mousePosition = new paper.Point(event.offsetX, event.offsetY);
var viewPosition = paper.view.viewToProject(mousePosition);
var returnedValues = panAndZoom.changeZoom(paper.view.zoom, (event.deltaY * -1), paper.view.center, viewPosition, 1.1);
var newZoom = returnedValues[0];
var offset = returnedValues[1];
paper.view.zoom = newZoom;
paper.view.center = paper.view.center.add(offset);
});
// For tracking if dragging or clicking is happening
var dragged;
paper.project.layers[0].on('mousedown', function(e) { // TODO should be layer 0 in long run?
dragged = false;
});
// Pan on mouse drag
/*paper.project.layers[0].on('mousedrag', function(event) { // TODO should be layer 0 in long run?
if(!event.event.ctrlKey && !event.event.altKey && !event.event.shiftKey) { // No keys (that we use) can be pushed
dragged = true; // We're panning, we don't wish to deselect all items as we would do with a click
paper.view.center = panAndZoom.changeCenter(paper.view.center, event.delta.x, event.delta.y, 0.7);
//event.preventDefault();
}
});*/
// Move selected items on mouse drag
selectedItems.on('mousedrag', function(event) {
event.stopPropagation(); // Don't propogate up or it will register as a pan event
event.preventDefault();
dragged = true; // We're panning, we don't wish to deselect all items as we would do with a click
this.translate(new paper.Point(event.delta.x, event.delta.y));
});
// If was a click and not a drag, deselect selected items
paper.project.layers[0].on('mouseup', function(event) {
if(!dragged) {
var removedItems = selectedItems.removeChildren(); // Remove from selection group, which also removes from display
paper.project.activeLayer.addChildren(removedItems); // Return to display
// Reset selection colors for showing selectable
for(var i =0; i < removedItems.length; i++) {
removedItems[i].selectedColor = paper.project.activeLayer.selectedColor;
removedItems[i].selected = false;
}
}
});
// Initial path object, will be reset for new paths after Alt is released
var path = newPath();
var paths = [];
paths.push(path);
// On mousedown add point to start from
paper.project.layers[0].on('mousedown', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(paths[paths.length-1].lastSegment == null) {
//path.add(event.point, event.point);
paths[paths.length-1].add(event.point, event.point);
} else {
//path.add(path.lastSegment.point, path.lastSegment.point);
paths[paths.length-1].add(paths[paths.length-1].lastSegment.point, paths[paths.length-1].lastSegment.point);
}
}
});
// On mousedrag add points to path
paper.project.layers[0].on('mousedrag', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(event.event.shiftKey) { // Use shift key for freeform
//path.add(event.point);
paths[paths.length-1].add(event.point);
} else { // Default of straight line added to path
//path.lastSegment.point = event.point;
paths[paths.length-1].lastSegment.point = event.point;
}
}
}.bind(this));
var tool = new paper.Tool();
var startDragPoint;
// Capture start of drag selection
paper.tool.onMouseDown = function(event) {
if((event.event.ctrlKey && event.event.shiftKey) || (event.event.ctrlKey && event.event.altKey)) {
startDragPoint = new paper.Point(event.point);
}
};
paper.tool.onMouseDrag = function(event) {
// Panning
if(!event.event.ctrlKey && !event.event.altKey && !event.event.shiftKey) { // No keys (that we use) can be pushed
dragged = true; // We're panning, we don't wish to deselect all items as we would do with a click
paper.view.center = panAndZoom.changeCenter(paper.view.center, event.delta.x, event.delta.y, 0.7);
//event.preventDefault();
}
// Show box indicating the area that has been selected
// For moving area and whiting out area
if((event.event.ctrlKey && event.event.shiftKey) || (event.event.ctrlKey && event.event.altKey)) {
dragged = true;
var showSelection = new paper.Path.Rectangle({
from: startDragPoint,
to: event.point,
strokeColor: 'red',
strokeWidth: 1
});
// Stop showing the selected area on drag (new one is created) and up because we're done
showSelection.removeOn({
drag: true,
up: true
});
}
};
// Capture start of drag selection
paper.tool.onMouseUp = function(event) {
if((event.event.ctrlKey && event.event.shiftKey) || (event.event.ctrlKey && event.event.altKey)) {
var endDragPoint = new paper.Point(event.point);
if(event.event.ctrlKey && event.event.shiftKey) { // Whiteout area
whiteoutArea(startDragPoint, endDragPoint);
} else if(event.event.ctrlKey && event.event.altKey) { // Move selected area
selectArea(startDragPoint, endDragPoint);
}
}
};
// Key events
paper.tool.onKeyUp = function(event) {
// Delete selected items on delete key
if(event.key == 'delete') {
selectedItems.removeChildren();
} else if (event.key == 'option') {
registerItemEvents(paths[paths.length-1]);
// Start a new path
paths.push(newPath());
}
}
}
}
// These variables are scoped so that all methods in PaperFunctions can access them
var canvas; // Set by setCanvas
var imageHeight; // Set by setCanvasImage
var raster;
var toolsSetup = false;
var setCanvas = function(canvasElement) {
canvas = canvasElement;
paper.setup(canvas);
};
var setCanvasImage = function(imageObject) {
if(imageObject != null)
{
imageHeight = imageObject.height;
var imageWidth = imageObject.width;
// Set up HTMLImage
var imageElement = new Image(imageObject.width, imageObject.height);
if(_.has(imageObject, 'imageData')) { // Came as 64 bit data
imageElement.src = 'data:image/png;base64,' + imageObject.imageData;
} else if(_.has(imageObject, 'imageUrl')) { // Came as URL
imageElement.src = imageObject.imageUrl;
}
// Add image to Paper.js canvas
imageElement.onload = function(event) {
//canvas.height = $(document).height()-3; // Set canvas height. Why do this here and not in the React component? Because we set the width here too, so we're keeping those together. Perhaps in the future this will be changed when we are responsive to window resizing.
//scalingFactor = canvas.height / imageObject.height; // Determine the ratio
//canvas.width = imageElement.width * scalingFactor; // Scale width based on height; canvas height has been set to the height of the document
// Initialize Paper.js on the canvas
paper.setup(canvas);
raster = new paper.Raster(imageElement, new paper.Point(canvas.width / 2, canvas.height / 2));
//setUpLineAndFreeFormDrawing(); // TODO once we cycle through images will we need to reset this for each new image or can we do this just once?
setUpNotes(); // TODO once we cycle through images will we need to reset this for each new image or can we do this just once?
selectedItems = new paper.Group(); // Since Paper.js has been setup we can now initialize this Group
registerCanvasEvents(); // Panning, zooming, moving selected items, deselecting all selected items
fitToPage();
};
}
};
var fitToPage = function() {
if(paper.view != null && canvas != null) {
// Fit image to page so whole thing is displayed
var scalingFactor = canvas.height / imageHeight; // Constant representation of the ratio of the canvas size to the image size
var zoomFactor = scalingFactor / paper.view.zoom; // Dynamic representation of the zoom needed to return to viewing the whole image in the canvas
// Reset the center point to the center of the canvas
var canvasCenter = new paper.Point(canvas.width/2, canvas.height/2);
paper.view.center = canvasCenter;
// Zoom to fit the whole image in the canvas
var returnedValues = panAndZoom.changeZoom(paper.view.zoom, -1, canvasCenter, canvasCenter, zoomFactor); // Always pass -1 as the delta, not entirely sure why
var newZoom = returnedValues[0];
var offset = returnedValues[1];
paper.view.zoom = newZoom;
paper.view.center = paper.view.center.add(offset);
}
};
var addImage = function(imageDataUrl) {
if(paper.view != null) {
var img = document.createElement("img");
img.src = imageDataUrl;
var presentMomentForId = new Date().getTime() + "-image"; // For purposes of having unique IDs
img.id = presentMomentForId;
img.hidden = true;
document.body.appendChild(img);
var raster = new paper.Raster(presentMomentForId);
registerItemEvents(raster);
}
};
var setUpLineAndFreeFormDrawing = function() {
if(paper.project != null) {
// Initial path object, will be reset for new paths after Alt is released
var path = newPath();
var paths = [];
paths.push(path);
// On mousedown add point to start from
paper.project.layers[0].on('mousedown', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(paths[paths.length-1].lastSegment == null) {
//path.add(event.point, event.point);
paths[paths.length-1].add(event.point, event.point);
} else {
//path.add(path.lastSegment.point, path.lastSegment.point);
paths[paths.length-1].add(paths[paths.length-1].lastSegment.point, paths[paths.length-1].lastSegment.point);
}
}
});
// On mousedrag add points to path
paper.project.layers[0].on('mousedrag', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(event.event.shiftKey) { // Use shift key for freeform
//path.add(event.point);
paths[paths.length-1].add(event.point);
} else { // Default of straight line added to path
//path.lastSegment.point = event.point;
paths[paths.length-1].lastSegment.point = event.point;
}
}
}.bind(this));
// Each time Alt comes up, start a new path
paper.tool.onKeyUp = function(event) {
if(event.key == "option") {
registerItemEvents(paths[paths.length-1]);
// Start a new path
paths.push(newPath());
}
};
}
};
// Establishes default line style
var newPath = function() {
var path = new paper.Path();
path.strokeColor = 'black';
path.strokeWidth = 10;
return path;
};
var note = "";
var setNote = function(newNote) {
note = newNote;
};
var setUpNotes = function() {
if(paper.project != null) {
paper.project.layers[0].on('mousedown', function(event) { // TODO should be layer 0 in long run?
if(event.event.ctrlKey && !event.event.altKey && !event.event.shiftKey) { // Only Ctrl key to add text
// Add text box
var textBox = new paper.PointText(event.point);
textBox.justification = 'left';
textBox.fillColor = 'black';
textBox.fontSize = 60;
textBox.content = note;
registerItemEvents(textBox);
}
});
}
};
var selectArea = function(startDragPoint, endDragPoint) {
var rasterTopLeftCorner = new paper.Point(raster.bounds.topLeft);
var adjustedStartDragPoint = new paper.Point(startDragPoint.x - rasterTopLeftCorner.x, startDragPoint.y - rasterTopLeftCorner.y);
var adjustedEndDragPoint = new paper.Point(endDragPoint.x - rasterTopLeftCorner.x, endDragPoint.y - rasterTopLeftCorner.y);
var boundingRectangleRasterCoordinates = new paper.Rectangle(adjustedStartDragPoint, adjustedEndDragPoint);
var boundingRectangleCanvasCoordinates = new paper.Rectangle(startDragPoint, endDragPoint);
var selectedArea = raster.getSubRaster(boundingRectangleRasterCoordinates);
var whitedOutSelection = new paper.Shape.Rectangle(boundingRectangleCanvasCoordinates);
whitedOutSelection.fillColor = 'white';
whitedOutSelection.insertAbove(raster); // Whiteout just above the image we're working with
registerItemEvents(selectedArea);
}
var whiteoutArea = function(startDragPoint, endDragPoint) {
var whitedOutSelection = new paper.Shape.Rectangle(startDragPoint, endDragPoint);
whitedOutSelection.fillColor = 'white';
whitedOutSelection.insertAbove(raster); // Whiteout just above the image we're working with
}
var flattenToDataUrl = function() {
layerAsRaster = paper.project.layers[0].rasterize(); // TODO should be layer 0 in long run? // Layer to Paper.js Raster object
layerAsRaster.visible = false;
var dataString = layerAsRaster.toDataURL();
return dataString;
};
module.exports = {
setCanvas: setCanvas,
setCanvasImage: setCanvasImage,
fitToPage: fitToPage,
addImage: addImage,
setNote: setNote,
flattenToDataUrl: flattenToDataUrl
};
Additionally, here's the SimplePanAndZoom.js file for clarity. It uses minimal Paper functions, it mainly just does calculations:
// Based on http://matthiasberth.com/articles/stable-zoom-and-pan-in-paperjs/
var SimplePanAndZoom = (function() {
function SimplePanAndZoom() { }
SimplePanAndZoom.prototype.changeZoom = function(oldZoom, delta, centerPoint, offsetPoint, zoomFactor) {
var newZoom = oldZoom;
if (delta < 0) {
newZoom = oldZoom * zoomFactor;
}
if (delta > 0) {
newZoom = oldZoom / zoomFactor;
}
// Zoom towards offsetPoint, not centerPoint (unless they're the same)
var a = null;
if(!centerPoint.equals(offsetPoint)) {
var scalingFactor = oldZoom / newZoom;
var difference = offsetPoint.subtract(centerPoint);
a = offsetPoint.subtract(difference.multiply(scalingFactor)).subtract(centerPoint);
}
return [newZoom, a];
};
SimplePanAndZoom.prototype.changeCenter = function(oldCenter, deltaX, deltaY, factor) {
var offset;
offset = new paper.Point(-deltaX, -deltaY);
offset = offset.multiply(factor);
return oldCenter.add(offset);
};
return SimplePanAndZoom;
})();
module.exports = SimplePanAndZoom;
Thanks.
I'm taking some guesses here but I'll address some problems in the code that will hopefully address the behavior you're seeing.
First, I presume paper.project.layers[0] is paper.project.activeLayer. Once that has been rasterized 1) the raster is added to the layer and setting visible = false does cause it to disappear when an update is done.
Second, when you invoke paper.setup(canvas) in imageElement.onload you create a new paper project. This project starts out as the active project and makes the previous project "disappear". So when you create a raster with raster = new paper.Raster(...) it goes into the new project, not the old project.
So now there is a hidden (.visible = false) raster in the old project (let's call it project1) and a new version of it in project2.
I'm not sure if this is the intended behavior or not, but when you invoke paper.setup(canvas) for what seems to be the second time then paper seems to notice that they both refer to the same canvas and keeps project1 and project2 in sync. So creating the second project clears the first project's children array. And adding new paper.Raster(...) ends up adding the raster to project1 and project2.
Now I can't tell what the next piece of the puzzle is. You'd need to add some information like 1) where the mouse event handlers are setup and what they are attached to, 2) what setUpNotes() does, 3) what registerCanvasEvents() does, and 4) what fitToPage does.
There are a few globals created, imageHeight and raster that probably aren't intentional. And it's not clear why you need to use new Image() at all - paper.Raster() accepts URLs, including data URLs.
I was surprised paper cleared the first project. It's curious.
Version 2:
Let me take a stab at structuring this using layers. I'd suggest you get rid of multiple projects because having mouse event handlers attached to multiple projects that share the same canvas adds too much complexity.
So, in your code initialization: paper.setup(canvas). Do this once and only once.
Setup the initial image in the single layer initially created by paper.
// this will be inserted into the current layer, project.activeLayer
var raster = new paper.Raster(imageURL, paper.view.bounds.center);
When the image in your queue changes do something like:
// make the existing image/layer invisible
paper.project.activeLayer.visible = false;
// add a new layer which is inserted in the project and activated
var layer = new paper.Layer();
// the new layer is activated, create a raster for the image
var raster = new paper.Raster(imageURL, paper.view.bounds.center);
// now do your normal logic for editing, zooming, etc.
It's really a bit more complicated than that because you have a queue of images and you only want to create a layer the first time you visit an image. You could initialize all the rasters at the outset, something like:
var imageURLs = ["url to image1", "url to image2", "etc"];
imageURLs.forEach(function(url) {
new paper.Layer();
paper.project.activeLayer.visible = false;
new paper.Raster(url, paper.view.bounds.center);
});
// make the first layer visible and activate it
paper.project.layers[0].visible = true;
paper.project.layers[0].activate();
The preceeding code gives you a parallel array to the images in your queue so switching images is straightforward - there is no checking to see if that image has been created or not:
function setImage(index) {
paper.project.activeLayer.visible = false;
paper.project.layers[index].activate();
paper.project.layers[index].visible = true;
}
Finally, I would make sure my mouse handling wasn't causing me problems. From the new code you posted it looks like each project had a global tool that handled 'mousedown', 'mousedrag', and 'mouseup' events, another set of handlers for activeLayer for 'mousedown', 'mousedrag', and 'mouseup' events, and also selectedItems has a handler for 'mousedrag'. I can't keep track of what all the different handlers are supposed to do across projects. I'm guessing that these are the root issue with the flickering you saw.
I would likely just use paper.view.on for 'mousedown', 'mousedrag', and 'mouseup' events. When I get an event I would check to see if anything on the layer was hit by using the following:
paper.project.activeLayer.hitTest(event.point);
Being able to set events on the view is new for paper but very useful. There may be a few other tweaks necessary to handle highlighting unselected items. A relatively straightforward way to handle that is to have a group of selected items and a group of unselected items:
unSelectedGroup.on('mouseenter', function() {
unSelectedGroup.selected = true;
});
unSelectedGroup.on('mouseleave', function() {
unSelectedGroup.selected = false;
});
These should be safe across layers when only one layer is visible at a time. I would set up these group handlers when setting up the images, whether all up front or on an as-needed basis. Alternatively, you could also add paper.view.on('mousemove', ...) and handle the 'mouseenter' and 'mouseleave' events yourself using hitTest as shown above, but either approach should work.
I think using a layer-based approach to your images will keep things in sync. There are enough problems with the project-based approach and many different mouse event handlers that you'll be on more stable ground regardless.

CreateJS Tween on keyDown

I ahave a problem implementing TweenJS on keydown. It only tweens once and after that if I press key there is no another tween.
What I have so far is:
var rotate = false;
document.onkeydown = keyDown;
document.onkeyup = keyUp;
function keyDown(e) {
switch(e.keyCode) {
case 32:
if(!rotate) {
rotate = true;
var t = createjs.Tween.get(rect).to({rotation:360},450, createjs.Ease.BackInOut).call(function(){
rotate = false;
});
}
break;
}
}
As I said, if I hit space, the rectangle rotates just as I want but after it finished rotation var rotate is set back to false as I wanted and after another press on space there is no another rotation.
So, my question is How to tween some element on click or keyDown?
This is because you have already rotated the rect to 360, so rotating it again does nothing (it is already at its target).
An easy solution is to toss in a zero-duration to() call before or after the main rotation, which resets the rotation value so it can be tweened again.
createjs.Tween.get(rect)
.to({rotation:0}) // THIS ONE
.to({rotation:360}, 450, createjs.Ease.BackInOut)
.call(function(){
rotate = false;
// You could also reset it here:
// rect.rotation = 0;
});

How do I detect the number of clicks in Internet Explorer 10+

I need to know how to detect the number of clicks on an HTML element. With Firefox and Chrome, I use the "event" object and check its "detail" property. With a "mousedown" handler, I only want to initiate a "drag" on an element (move it around the screen using CSS) on the FIRST click:
if (event.detail>1) return;
But Internet Exploder 11 (I assume the same for 10+) the
event.detail===5
on the first click. IE9 returns the "proper" value of 1.
The only thing I can think of is to use "setInterval()" to periodically (every .5 seconds or so) set a "global" value to =0, then increment that value on each "mousedown" and use that count instead of the "event.detail".
Ridiculous, methinks.
You can see this problem (until I fix it) at:
http://softmoon-webware.com/MasterColorPicker_instructions.php
The actual javascript code in question is in the file (the the very end):
http://softmoon-webware.com/color-pickers/SoftMoon-WebWare/MasterColorPicker2.js
After clicking on the input box on the left (that says try here), you should be able to "drag" the "picker panels" by their handles around the screen. No problem using a real browser, and even though IE9 is about maxxed out with the codebase (yes, it throws stack-overflow errors!) it will (or did before the last update that worked with the "FD-sliders" was implemented that started the stack overflow problems) allow dragging the panels. IE10+ only "highlights" (selects) the text under the curser, won't drag, again because of the "wrong" "event.detail" value. To be fair, nothing I can find on the Microsoft Developer Network pages says what the event.detail specs actually are (just "gives further info about the event..."), and the link to W3C pages don't seem to have specs for the "event.detail" property either.
Am I missing something here?
code extract from file:
for (var i=0, handle, panels=MasterColorPicker.panels; i<panels.length; i++) {
if (panels[i]===MasterColorPicker.mainPanel) continue;
if (panels[i].id==='MasterColorPicker_options') {
handle=panels[i].getElementsByTagName('header')[0]; // ↓ ↓ for drag, the first panel must be the largest and contain the other(s) in its margin
UniDOM.addEventHandler(handle, 'onmousedown', dragPanel, false, [MasterColorPicker.mainPanel, panels[i]]);
UniDOM.addEventHandler(handle, 'onmouseup', returnPanelsOn3, false, [MasterColorPicker.mainPanel, panels[i]]); }
else {
handle=panels[i].getElementsByTagName('h2')[0].getElementsByTagName('span')[0];
UniDOM.addEventHandler(handle, 'onmousedown', dragPanel, false, [panels[i]]);
UniDOM.addEventHandler(handle, 'onmouseup', returnPanelsOn3, false, [panels[i]]); }
UniDOM.addEventHandler(handle, 'oncontextmenu', abortContextMenu); }
UniDOM.addEventHandler(document.getElementById("MasterColorPicker_returnPanelsOn3"), 'onmouseup', returnPanelsOn3, false, panels);
function dragPanel(event, stickyPanels) { console.log("IE sucks: detail: "+event.detail);
event.stopPropagation();
event.preventDefault();
if (event.detail>1 || !MasterColorPicker.enablePanelDrag) return;
var stick=(event.shiftKey || event.button===2) && MasterColorPicker.enableStickyPanels && (UniDOM.MS_exploder!==9),
ttcn= (stick ? 'MCP_thumbtack' : ""),
CSS=getComputedStyle(stickyPanels[0], null),
mOff= (CSS.position==='fixed') ?
{x: (document.body.offsetWidth-event.clientX)-parseInt(CSS.right), y: event.clientY-parseInt(CSS.top)}
: UniDOM.getMouseOffset(stickyPanels[0], event),
dragHandle=event.currentTarget,
move=UniDOM.addEventHandler(document.body, 'onmousemove', function(event) {
var CSS=getComputedStyle(stickyPanels[0], null);
if (CSS.position==='fixed')
var b={w: document.body.offsetWidth, h: document.documentElement.clientHeight || window.innerHeight, x: 0, y: 0},
y=(event.clientY - mOff.y),
x=((b.w-event.clientX) - mOff.x);
else
var b=UniDOM.getElementOffset(stickyPanels[0].offsetParent, MasterColorPicker.dragBounder),
b={y: b.y, x: b.x, w: MasterColorPicker.dragBounder.offsetWidth, h: MasterColorPicker.dragBounder.offsetHeight},
m=UniDOM.getMouseOffset(stickyPanels[0].offsetParent, event),
y=m.y - (parseInt(CSS.marginTop) + mOff.y),
x=(b.w-m.x) - (stickyPanels[0].offsetWidth-mOff.x) + parseInt(CSS.marginRight);
y= (y<-b.y) ? (-b.y) : ( (y>(m=b.h-(stickyPanels[0].offsetHeight+parseInt(CSS.marginTop)+parseInt(CSS.marginBottom)+b.y))) ? m : y );
x= (x<-b.x) ? (-b.x) : ( (x>(m=b.w-(stickyPanels[0].offsetWidth+parseInt(CSS.marginLeft)+parseInt(CSS.marginRight)+b.x))) ? m : x );
for (i=0; i<stickyPanels.length; i++) {
stickyPanels[i].style.top= y + 'px';
stickyPanels[i].style.right= x + 'px'; }
event.stopPropagation();
event.preventDefault(); }
, true),
blockMenu=UniDOM.addEventHandler(document.body, 'oncontextmenu', abortContextMenu, true),
drop=UniDOM.addEventHandler(document.body, 'onmouseup', function(event) {
move.onmousemove.remove(); blockMenu.oncontextmenu.remove(); drop.onmouseup.remove();
event.stopPropagation();
event.preventDefault();
for (var i=0; i<stickyPanels.length; i++) {UniDOM.removeClass(stickyPanels[i], ['dragging', ttcn]);}
UniDOM.removeClass(document.body, ['MCP_drag', ttcn]);
if (stick) dragHandle.removeChild(MasterColorPicker.thumbtackImage);
try {MasterColorPicker.dataTarget.focus();} catch(e) {} }
, true);
for (var i=0; i<stickyPanels.length; i++) {
UniDOM.addClass(stickyPanels[i], ['dragging', ttcn]);
MasterColorPicker.setTopPanel(stickyPanels[i]); }
if (stick) {
mOff.x=stickyPanels[0].offsetWidth-mOff.x;
if (CSS.position==='fixed') {
mOff.y= -(parseInt(CSS.marginTop)-mOff.y);
var currentCN='floating', newCN='scrollable'; }
else {
mOff.y += parseInt(CSS.marginTop);
var currentCN='scrollable', newCN='floating'; }
while (--i>=0) {UniDOM.swapOutClass(stickyPanels[i], currentCN, newCN);}
dragHandle.appendChild(MasterColorPicker.thumbtackImage);
move.onmousemove.wrapper(event); }
UniDOM.addClass(document.body, ['MCP_drag', ttcn]); }
function returnPanelsOn3(event, stickyPanels) {
event.stopPropagation();
event.preventDefault();
if (event.detail!==3 || event.button!==0) return;
MasterColorPicker.returnPanelsHome(stickyPanels); }
function abortContextMenu(event) {event.preventDefault(); event.stopPropagation();}
MasterColorPicker.returnPanelsHome=function(stickyPanels) {
for (var i=0; i<stickyPanels.length; i++) {
stickyPanels[i].style.top= "";
stickyPanels[i].style.right= "";
UniDOM.removeClass(stickyPanels[i], ['scrollable', 'floating']); } }
Just keep a count of how many times your mousedown handler has been called:
var clickCount = 0;
$('#myElement').on('mousedown', function(){
clickCount++;
if (clickCount > 1)
return;
[ ... ]
});

rotate a globe with the mouse

I can determine where on a globe the user has clicked.
When the user first clicks the mouse button down, I can store where they have clicked; lets call it the pin.
Then, as they drag the mouse around, I want to put the pin under the mouse cursor.
Here is some code that seems to roughly keep the pin under the mouse pointer, but doesn't correctly maintain the globe; it flickers and spins randomly. Sometimes it flips the axis entirely so the globe is momentarily back-to-front.
function evtPos(evt) {
if(!scene.ortho) return null;
var x = lerp(scene.ortho[0],scene.ortho[1],evt.clientX/canvas.width),
y = lerp(scene.ortho[3],scene.ortho[2],evt.clientY/canvas.height), // flipped
sqrd = x*x+y*y;
return (sqrd > 1)?
null:
mat4_vec3_multiply(mat4_inverse(scene.mvMatrix),[x,y,Math.sqrt(1-sqrd)]);
}
function onMouseDown(evt) {
pin = evtPos(evt);
}
function onMouseMove(evt,keys,isMouseDown) {
if(!isMouseDown) return;
var pt = evtPos(evt);
if(pin == null) pin = pt;
if(pt == null) return;
var d = vec3_sub(pt,pin),
rotx = Math.atan2(d[1],d[2]),
roty = (d[2] >= 0)?
-Math.atan2(d[0] * Math.cos(rotx),d[2]):
Math.atan2(d[0] * Math.cos(rotx),-d[2]),
rotz = Math.atan2(Math.cos(rotx),Math.sin(rotx)*Math.sin(roty));
scene.mvMatrix = mat4_multiply(scene.mvMatrix,mat4_rotation(rotx,[1,0,0]));
scene.mvMatrix = mat4_multiply(scene.mvMatrix,mat4_rotation(roty,[0,1,0]));
scene.mvMatrix = mat4_multiply(scene.mvMatrix,mat4_rotation(rotz,[0,0,1]));
}
function onMouseUp(evt) {
pin = null;
}
Also, over time, an error seems to build up and the pin drifts further and further from the mouse pointer. I presume I should somehow compute the mvMatrix completely rather than by lots of samll increments each event?
I want the user to be able to drag the globe around to navigate naturally. All code to spin globes that I've found uses fixed speeds e.g. arrow keys, rather than 'pinning' the globe under a mouse pointer. Unity has a function Quaternion.FromToRotation(fromPos,toPos) which seems very promising but the source is not available.
One of the approaches for doing this is the arcBall algorithm. There are even JavaScript implementations available so you don't need to roll your own.

HTML5 photoshop like polygonal lasso selection

Im looking to build a tool to cut out a portion of a photo by letting the user create a closed shape. The user should be able to start drawing lines. From point a to point b, to c, e, d, e, f .... to eventually point a again to close the shape.
I want to use the HTML5 canvas for this. I think this could be a good fit and I'm thinking about using something like flashcanvas as fallback for IE/older browsers?
Is there any tutorial/open source application that I could use to build this sort of thing?
This is the first time I'm going to build an application using HTML5 canvas so are there any pitfalls I should worry about?
I think this is advanced usage of canvas. You have to know the basics, how to draw, how to use layers, how to manipulate pixels. Just ask google for tutorials.
Assuming you know about the previous, I'll give it a try. I've never done that before but I have an idea :
You need 3 canvas :
the one containing your picture (size of your picture)
a layer where the user draw the selection shape (size of your picture, on top of the first canvas)
a result canvas, will contain your cropped picture (same size, this one doesn't need to be displayed)
When the user click on your picture : actually, he clicks on the layer, the layer is cleared and a new line begins.
When he clicks on it another time, the previous started line is drawn and another one begins, etc... You keep doing this until you click on a non-blank pixel (which means you close the shape).
If you want the user to preview the lines, you need another canvas ( explained here http://dev.opera.com/articles/view/html5-canvas-painting/#line )
When the shape is closed, the user has to click inside or outside the shape to determine which part he wants to select. You fill that part with a semi-transparent gray for example ( flood fill explained here http://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool/ )
Now the layer canvas contains a colored shape corresponding to the user selection.
Get the pixel data from your layer and read through the array, every time you find a non-blank pixel at index i, you copy this pixel from your main canvas to the result canvas :
/* First, get pixel data from your 3 canvas into
* layerPixData, resultPixData, picturePixData
*/
// read the entire pixel array
for (var i = 0 ; i < layerPixData.length ; i+=4 ) {
//if the pixel is not blank, ie. it is part of the selected shape
if ( layerPixData[i] != 255 || layerPixData[i+1] != 255 || layerPixData[i+2] != 255 ) {
// copy the data of the picture to the result
resultPixData[i] = picturePixData[i]; //red
resultPixData[i+1] = picturePixData[i+1]; //green
resultPixData[i+2] = picturePixData[i+2]; //blue
resultPixData[i+3] = picturePixData[i+3]; //alpha
// here you can put the pixels of your picture to white if you want
}
}
If you don't know how pixel manipulation works, read this https://developer.mozilla.org/En/HTML/Canvas/Pixel_manipulation_with_canvas
Then, use putImageData to draw the pixels to your result canvas. Job done !
If you want to move lines of your selection, way to go : http://simonsarris.com/blog/225-canvas-selecting-resizing-shape
Here is how you should do that:
The code at the following adds a canvas on top of your page and then by clicking and dragging on that the selection areas would be highlighted. What you need to do after that is to make a screenshot from the underlying page and also a mask layer out of the created image in your canvas and apply that to the screenshot, just like how it is shown in one other answers.
/* sample css code for the canvas
#overlay-canvas {
position: absolute;
top: 0;
left: 0;
background-color: transparent;
opacity: 0.4;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
}
*/
function getHighIndex(selector) {
if (!selector) { selector = "*" };
var elements = document.querySelectorAll(selector) ||
oXmlDom.documentElement.selectNodes(selector);
var ret = 0;
for (var i = 0; i < elements.length; ++i) {
if (deepCss(elements[i],"position") === "static")
continue;
var temp = deepCss(elements[i], "z-index");
if (temp != "auto")
temp = parseInt(temp, 10) || 0;
else
continue;
if (temp > ret)
ret = temp;
}
return ret;
}
maxZIndex = getHighIndex();
$.fn.extend({
lasso: function () {
return this
.mousedown(function (e) {
// left mouse down switches on "capturing mode"
if (e.which === 1 && !$(this).is(".lassoRunning")) {
var point = [e.offsetX, e.offsetY];
$(this).addClass("lassoRunning");
$(this).data("lassoPoints", [point]);
$(this).trigger("lassoStart", [point]);
}
})
.mouseup(function (e) {
// left mouse up ends "capturing mode" + triggers "Done" event
if (e.which === 1 && $(this).is(".lassoRunning")) {
$(this).removeClass("lassoRunning");
$(this).trigger("lassoDone", [$(this).data("lassoPoints")]);
}
})
.mousemove(function (e) {
// mouse move captures co-ordinates + triggers "Point" event
if ($(this).is(".lassoRunning")) {
var point = [e.offsetX, e.offsetY];
$(this).data("lassoPoints").push(point);
$(this).trigger("lassoPoint", [point]);
}
});
}
});
function onLassoSelect() {
// creating canvas for lasso selection
var _canvas = document.createElement('canvas');
_canvas.setAttribute("id", "overlay-canvas");
_canvas.style.zIndex = ++maxZIndex;
_canvas.width = document.width
_canvas.height = document.height
document.body.appendChild(_canvas);
ctx = _canvas.getContext('2d'),
ctx.strokeStyle = '#0000FF';
ctx.lineWidth = 5;
$(_canvas)
.lasso()
.on("lassoStart", function(e, lassoPoint) {
console.log('lasso start');
var pos = lassoPoint;
ctx.beginPath();
ctx.moveTo(pos[0], pos[1]);
console.log(pos);
})
.on("lassoDone", function(e, lassoPoints) {
console.log('lasso done');
var pos = lassoPoints[0];
ctx.lineTo(pos[0], pos[1]);
ctx.fill();
console.log(pos);
})
.bind("lassoPoint", function(e, lassoPoint) {
var pos = lassoPoint;
ctx.lineTo(pos[0], pos[1]);
ctx.fill();
console.log(pos);
});
}

Categories

Resources