Paper.js Subraster Selecting Wrong Area - javascript

I'm working in a Paper.js project where we're essentially doing image editing. There is one large Raster. I'm attempting to use the getSubRaster method to copy a section of the image (raster) that the user can then move around.
After the raster to edit is loaded, selectArea is called to register these listeners:
var selectArea = function() {
if(paper.project != null) {
var startDragPoint;
paper.project.layers[0].on('mousedown', function(event) { // TODO should be layer 0 in long run? // Capture start of drag selection
if(event.event.ctrlKey && event.event.altKey) {
startDragPoint = new paper.Point(event.point.x + imageWidth/2, (event.point.y + imageHeight/2));
//topLeftPointOfSelectionRectangleCanvasCoordinates = new paper.Point(event.point.x, event.point.y);
}
});
paper.project.layers[0].on('mouseup', function(event) { // TODO should be layer 0 in long run? // Capture end of drag selection
if(event.event.ctrlKey && event.event.altKey) {
var endDragPoint = new paper.Point(event.point.x + imageWidth/2, event.point.y + imageHeight/2);
// Don't know which corner user started dragging from, aggregate the data we have into the leftmost and topmost points for constructing a rectangle
var leftmostX;
if(startDragPoint.x < endDragPoint.x) {
leftmostX = startDragPoint.x;
} else {
leftmostX = endDragPoint.x;
}
var width = Math.abs(startDragPoint.x - endDragPoint.x);
var topmostY;
if(startDragPoint.y < endDragPoint.y) {
topmostY = startDragPoint.y;
} else {
topmostY = endDragPoint.y;
}
var height = Math.abs(startDragPoint.y - endDragPoint.y);
var boundingRectangle = new paper.Rectangle(leftmostX, topmostY, width, height);
console.log(boundingRectangle);
console.log(paper.view.center);
var selectedArea = raster.getSubRaster(boundingRectangle);
var selectedAreaAsDataUrl = selectedArea.toDataURL();
var subImage = new Image(width, height);
subImage.src = selectedAreaAsDataUrl;
subImage.onload = function(event) {
var subRaster = new paper.Raster(subImage);
// Make movable
subRaster.onMouseEnter = movableEvents.showSelected;
subRaster.onMouseDrag = movableEvents.dragItem;
subRaster.onMouseLeave = movableEvents.hideSelected;
};
}
});
}
};
The methods are triggered at the right time and the selection box seems to be the right size. It does indeed render a new raster for me that I can move around, but the contents of the raster are not what I selected. They are close to what I selected but not what I selected. Selecting different areas does not seem to yield different results. The content of the generated subraster always seems to be down and to the right of the actual selection.
Note that as I build the points for the bounding selection rectangle I do some translations. This is because of differences in coordinate systems. The coordinate system where I've drawn the rectangle selection has (0,0) in the center of the image and x increases rightward and y increases downward. But for getSubRaster, we are required to provide the pixel coordinates, per the documentation, which start at (0,0) at the top left of the image and increase going rightward and downward. Consequently, as I build the points, I translate the points to the raster/pixel coordinates by adding imageWidth/2 and imageHeight/2`.
So why does this code select the wrong area? Thanks in advance.
EDIT:
Unfortunately I can't share the image I'm working with because it is sensitive company data. But here is some metadata:
Image Width: 4250 pixels
Image Height: 5500 pixels
Canvas Width: 591 pixels
Canvas Height: 766 pixels
My canvas size varies by the size of the browser window, but those are the parameters I've been testing in. I don't think the canvas dimensions are particularly relevant because I'm doing everything in terms of image pixels. When I capture the event.point.x and event.point.y to the best of my knowledge these are image scaled coordinates, but from a different origin - the center rather than the top left. Unfortunately I can't find any documentation on this. Observe how the coordinates work in this sketch.
I've also been working on a sketch to illustrate the problem of this question. To use it, hold Ctrl + Alt and drag a box on the image. This should trigger some logging data and attempt to get a subraster, but I get an operation insecure error, which I think is because of security settings in the image request header. Using the base 64 string instead of the URL doesn't give the security error, but doesn't do anything. Using that string in the sketch produces a super long URL I can't paste here. But to get that you can download the image (or any image) and convert it here, and put that as the img.src.

The problem is that the mouse events all return points relative to 0, 0 of the canvas. And getSubRaster expects the coordinates to be relative to the 0, 0 of the raster item it is extracting from.
The adjustment needs to be eventpoint - raster.bounds.topLeft. It doesn't really have anything to do with the image width or height. You want to adjust the event points so they are relative to 0, 0 of the raster, and 0, 0 is raster.bounds.topLeft.
When you adjust the event points by 1/2 the image size that causes event points to be offset incorrectly. For the Mona Lisa example, the raster size (image size) is w: 320, h: 491; divided by two they are w: 160, h: 245.5. But bounds.topLeft of the image (when I ran my sketch) was x: 252.5, y: 155.5.
Here's a sketch that shows it working. I've added a little red square highlighting the selected area just to make it easier to compare when it's done. I also didn't include the toDataURL logic as that creates the security issues you mentioned.
Here you go: Sketch
Here's code I put into an HTML file; I noticed that the sketch I put together links to a previous version of the code that doesn't completely work.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Rasters</title>
<script src="./vendor/jquery-2.1.3.js"></script>
<script src="./vendor/paper-0.9.25.js"></script>
</head>
<body>
<main>
<h3>Raster Bug</h3>
<div>
<canvas id="canvas"></canvas>
</div>
<div id="position">
</div>
</main>
<script>
// initialization code
$(function() {
// setup paper
$("#canvas").attr({width: 600, height: 600});
var canvas = document.getElementById("canvas");
paper.setup(canvas);
// show a border to make range of canvas clear
var border = new paper.Path.Rectangle({
rectangle: {point: [0, 0], size: paper.view.size},
strokeColor: 'black',
strokeWidth: 2
});
var tool = new paper.Tool();
// setup mouse position tracking
tool.on('mousemove', function(e) {
$("#position").text("mouse: " + e.point);
});
// load the image from a dataURL to avoid CORS issues
var raster = new paper.Raster(dataURL);
raster.position = paper.view.center;
var lt = raster.bounds.topLeft;
var startDrag, endDrag;
console.log('rb', raster.bounds);
console.log('lt', lt);
// setup mouse handling
tool.on('mousedown', function(e) {
startDrag = new paper.Point(e.point);
console.log('sd', startDrag);
});
tool.on('mousedrag', function(e) {
var show = new paper.Path.Rectangle({
from: startDrag,
to: e.point,
strokeColor: 'red',
strokeWidth: 1
});
show.removeOn({
drag: true,
up: true
});
});
tool.on('mouseup', function(e) {
endDrag = new paper.Point(e.point);
console.log('ed', endDrag);
var bounds = new paper.Rectangle({
from: startDrag.subtract(lt),
to: endDrag.subtract(lt)
});
console.log('bounds', bounds);
var sub = raster.getSubRaster(bounds);
sub.bringToFront();
var subData = sub.toDataURL();
sub.remove();
var subRaster = new paper.Raster(subData);
subRaster.position = paper.view.center;
});
});
var dataURL = ; // insert data or real URL here
</script>
</body>
</html>

Related

Transparent HTML5 canvas todataurl only rendering transparent background on mobile devices

EDITED, see end of question.
In my application I have two canvas elements. One shows layered, transparent pngs, the other one gets an image from a file input and masks it. The chosen image is transparent where it is not masked. This image is then converted to a dataUrl, transformed to fit into the first canvas and added as the top layer of the first canvas.
Everything works as expected on desktop browsers: Chrome OSX, Safari OSX. I only add it in on load, so I made sure no race conditions can occur.
On Android Chrome and Safari iOS the canvas converted todataURL is rendered transparent. If I add a non-transparent image to the second canvas, the rendered image will show even on mobile devices.
To check I added the supposedly transparent canvas to the body. It shows correctly on desktop, but is transparent on mobile Browsers. Here the simplified JS. I am using fabric.js for convenience, but the problem is the same without the lib. I even once added a background color. Then only the color will show. Any ideas why todataurl on mobile browsers renders only transparent pixels?
<body>
<canvas id="canv"></canvas>
<script src="fabric.js"></script>
<script>
// main canvas
var c = new fabric.Canvas('canv');
c.setWidth(200);
c.setHeight(200);
var i = document.createElement('img');
i.src = 'dummy.jpg';
// i.src = 'dummy1.png';
i.onload = function(e) {
//document.body.appendChild(i);
scale = 1; // resizes the image
var ci = new fabric.Image(i);
ci.set({
left: 0,
top: 0,
scaleX: scale,
scaleY: scale,
originX: 'left',
originY: 'top'
}).setCoords();
// temporary canvas, will be converted to dataurl, contains transformed image
var tmpCanvas = new fabric.Canvas();
tmpCanvas.setWidth(100);
tmpCanvas.setHeight(100);
ci.scaleToWidth(100);
tmpCanvas.add(ci);
tmpCanvas.renderAll();
// create image from temporary canvas
var customImage = new fabric.Image.fromURL(tmpCanvas.toDataURL({ format: 'png' }), function (cImg) {
// add it to original canvas
c.clear();
c.add(cImg);
c.renderAll();
data = c.toDataURL({ format: 'png' });
// resized image
var newc = new fabric.StaticCanvas().setWidth(300).setHeight(300);
var newImg = new fabric.Image.fromURL(data, function (c1Img) {
newc.add(c1Img);
newc.renderAll();
// append to body to check if canvas is rendered correctly
document.body.appendChild(newc.lowerCanvasEl);
});
});
}
</script>
EDIT: I solved the problem, but could not find the problem on the Javascript side.
The problem was that I copied a temporary canvas onto another canvas. The scale and position of the added canvas was computed by finding the bounding box of non transparent pixels in a png, which was generated exactly for this purpose. A mask in short.
The bounding box was calculated in another temporary canvas at the start of the app (based on this answer). Although all sizes of the mask and its canvas were set correctly and the canvas was never added to the DOM, when loaded on a small screen the results of the bounding box differed from from the full screen results. After much testing i found this was true on Desktop too.
Because I already spent so much time on the problem, I decided to try to calculate the bounds in PHP and put it into a data attribute. Which worked great!
For those interested in the PHP solution:
function get_bounding_box($imgPath) {
$img = imagecreatefrompng($imgPath);
$w = imagesx($img);
$h = imagesy($img);
$bounds = [
'left' => $w,
'right' => 0,
'top' => $h,
'bottom' => 0
];
// get alpha of every pixel, if it is not fully transparent, write it to bounds
for ($yPos = 0; $yPos < $h; $yPos++) {
for ($xPos = 0; $xPos < $w; $xPos++) {
// Check, ob Pixel nicht vollständig transparent ist
$rgb = imagecolorat($img, $xPos, $yPos);
if (imagecolorsforindex($img, $rgb)['alpha'] < 127) {
if ($xPos < $bounds['left']) {
$bounds['left'] = $xPos;
}
if ($xPos > $bounds['right']) {
$bounds['right'] = $xPos;
}
if ($yPos < $bounds['top']) {
$bounds['top'] = $yPos;
}
if ($yPos > $bounds['bottom']) {
$bounds['bottom'] = $yPos;
}
}
}
}
return $bounds;
}
The problem was that I copied a temporary canvas onto another canvas. The scale and position of the added canvas was computed by finding the bounding box of non transparent pixels in a png, which was generated exactly for this purpose. A mask in short.
The bounding box was calculated in another temporary canvas at the start of the app (based on this answer). Although all sizes of the mask and its canvas were set correctly and the canvas was never added to the DOM, when loaded on a small screen the results of the bounding box differed from from the full screen results. After much testing i found this was true on Desktop too.
Because I already spent so much time on the problem, I decided to try to calculate the bounds in PHP and put it into a data attribute. Which worked great!
For those interested in the PHP solution:
function get_bounding_box($imgPath) {
$img = imagecreatefrompng($imgPath);
$w = imagesx($img);
$h = imagesy($img);
$bounds = [
'left' => $w,
'right' => 0,
'top' => $h,
'bottom' => 0
];
// get alpha of every pixel, if it is not fully transparent, write it to bounds
for ($yPos = 0; $yPos < $h; $yPos++) {
for ($xPos = 0; $xPos < $w; $xPos++) {
// Check, ob Pixel nicht vollständig transparent ist
$rgb = imagecolorat($img, $xPos, $yPos);
if (imagecolorsforindex($img, $rgb)['alpha'] < 127) {
if ($xPos < $bounds['left']) {
$bounds['left'] = $xPos;
}
if ($xPos > $bounds['right']) {
$bounds['right'] = $xPos;
}
if ($yPos < $bounds['top']) {
$bounds['top'] = $yPos;
}
if ($yPos > $bounds['bottom']) {
$bounds['bottom'] = $yPos;
}
}
}
}
return $bounds;
}

Paper.js change layer coordinate start point

everyone.
It is possible in paper.js change layer start coordinate position from top-left corner?
Use case: I put image into bottom layer and draw some stuff on top layer. Image can be scalled and moved. I need to get my drawing path points coord in image coordinate system. And I want to set my drawing layer coordinate start to image top left point and move it then image scale/move.
I try do this:
var layer = new Layer();
project.activeLayer.setName("DrawingStuff");
project.activeLayer.setPosition(paperjs.project.view.center);
project.activeLayer.bounds.x = 300;
project.activeLayer.bounds.y = 300;
project.activeLayer.bounds.width = raster.width;
project.activeLayer.bounds.height = raster.height;
But it is don't work. Name is setted, but bounds and position still empty.
Will be very thankfull for any advise.
P.S. I know that I can just recalculate path point from canvas coords system to image coords system, but I want to try change layer coords start point
It would be easier to use the current coordinate system and change the zoom and center properties of the project's view. Take a look at the code behind the zoom tool in main.js at http://sketch.paperjs.org:
var lastPoint;
var body = $('body');
zoomTool = new Tool({
buttonClass: 'icon-zoom'
}).on({
mousedown: function(event) {
if (event.modifiers.space) {
lastPoint = paper.view.projectToView(event.point);
return;
}
var factor = 1.25;
if (event.modifiers.option)
factor = 1 / factor;
paper.view.zoom *= factor;
paper.view.center = event.point;
},
...
mousedrag: function(event) {
if (event.modifiers.space) {
body.addClass('zoom-grab');
// In order to have coordinate changes not mess up the
// dragging, we need to convert coordinates to view space,
// and then back to project space after the view space has
// changed.
var point = paper.view.projectToView(event.point),
last = paper.view.viewToProject(lastPoint);
paper.view.scrollBy(last.subtract(event.point));
lastPoint = point;
}
},
...
});

fabric js - Resize complete canvas according to a rectangle added in a canvas in order to achieve cropping

I am using fabric js for generating templates, I want user to set height and width of a canvas for which I am using following code
canvas.setHeight(height);
canvas.setWidth(width);
However on doing this, the full width canvas stretches itself and the object disappears from the resized canvas as it was lets say 500 px wide before and user resized it to 100px then the items disappear.
I want to achieve a functionality so that if a user wants to change the size of canvas then instead of resizing the canvas I will show a rectangle and user can move rectangle accordingly to get the visible area and once user clicks on save then I will resize the canvas according to that rectangle, so that I can process the SVG later for further conversions but by doing so I am not sure how to get the visible area in that rectangle to the resized canvas, in short I want functionality like Darkroom js but for fabric js canvas object.
The current functionality is added to this JS fiddle http://jsfiddle.net/tbqrn/102/
Finally after doing some research I am able to do that, instead of resizing the canvas I am adding a rectangle object with "cropper" type as mentioned below
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'transparent',
width: 400,
height: 400,
strokeDashArray: [5, 5],
stroke: 'black',
type: 'cropper',
lockScalingX: true,
lockScalingY: true,
lockRotation: true
});
canvas.add(rect);
This rectangle will serve as the cropper, when a user submits the form then I am getting that cropper's dimensions as follow along with changing canvas size
var i;
var croppedLeft = 0;
var croppedTop = 0;
var canvasJson = canvas.getObjects();
// Cropping canvas according to cropper rectangle
if (canvas.getObjects().length > 0) {
var i;
for (i = 0; i < canvas.getObjects().length; i++) {
if (canvas.getObjects()[i].type == 'cropper') {
croppedLeft = canvas.getObjects()[i].left;
croppedTop = canvas.getObjects()[i].top;
canvas.setHeight(canvas.getObjects()[i].height);
canvas.setWidth(canvas.getObjects()[i].width);
canvas.getObjects()[i].remove();
}
}
}
And after that I am shifting all the other canvas objects accordingly as follow.
for (i = 0; i < canvasJson.length; i++) {
canvas.getObjects()[i].left = canvas.getObjects()[i].left - croppedLeft
canvas.getObjects()[i].top = canvas.getObjects()[i].top - croppedTop
canvas.renderAll();
}
And after doing that I am able to achieve the desired functionality, hope this helps someone.
Here is my fiddle http://jsfiddle.net/tbqrn/115/

jQuery and Canvas loading behaviour

After being a long time lurker, this is my first post here! I've been RTFMing and searching everywhere for an answer to this question to no avail. I will try to be as informative as I can, hope you could help me.
This code is for my personal webpage.
I am trying to implement some sort of a modern click-map using HTML5 and jQuery.
In the website you would see the main image and a hidden canvas with the same size at the same coordinates with this picture drawn into it.
When the mouse hovers the main picture, it read the mouse pixel data (array of r,g,b,alpha) from the image drawn onto the canvas. When it sees the pixel color is black (in my case I only check the RED value, which in a black pixel would be 0) it knows the activate the relevant button.
(Originally, I got the idea from this article)
The reason I chose this method, is for the page to be responsive and dynamically change to fit different monitors and mobile devices. To achieve this, I call the DrawCanvas function every time the screen is re-sized, to redraw the canvas with the new dimensions.
Generally, this works OK. The thing is ,there seems to be an inconsistent behavior in Chrome and IE(9). When I initially open the page, I sometimes get no pixel data (0,0,0,0), until i re-size the browser. At first I figured there's some loading issues that are making this happen so I tried to hack it with setTimeout, it still doesn't work. I also tried to trigger the re-size event and call the drawCanvas function at document.ready, still didn't work.
What's bothering me is most, are the inconsistencies. Sometimes it works, sometimes is doesn't. Generally, it is more stable in chrome than in IE(9).
Here is the deprecated code:
<script type="text/javascript">
$(document).ready(function(){setTimeout(function() {
// Get main image object
var mapWrapper = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
// Create a hidden canvas the same size as the main image and append it to main div
var canvas = document.createElement('canvas');
canvas.height = mapWrapper.clientHeight;
canvas.width = mapWrapper.clientWidth;
canvas.fillStyle = 'rgb(255,255,255)';
canvas.style.display = 'none';
canvas.id = 'hiddencvs';
$('#map_wrapper').append(canvas);
// Draw the buttons image into the canvas
drawCanvas(null);
$("#map_wrapper").mousemove(function(e){
var canvas = document.getElementById('hiddencvs');
var context = canvas.getContext('2d');
var pos = findPos(this);
var x = e.pageX - pos.x;
var y = e.pageY - pos.y;
// Get pixel information array (red, green, blue, alpha)
var pixel = context.getImageData(x,y,1,1).data;
var red = pixel[0];
var main_img = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
if (red == 0)
{
...
}
else {
...
}
});
},3000);}); // End DOM Ready
function drawCanvas(e)
{
// Get context of hidden convas and set size according to main image
var cvs = document.getElementById('hiddencvs');
var ctx = cvs.getContext('2d');
var mapWrapper = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
cvs.width = mapWrapper.clientWidth;
cvs.height = mapWrapper.clientHeight;
// Create img element for buttons image
var img = document.createElement("img");
img.src = "img/main-page-buttons.png";
// Draw buttons image inside hidden canvas, strech it to canvas size
ctx.drawImage(img, 0, 0,cvs.width,cvs.height);
}
$(window).resize(function(e){
drawCanvas(e);
}
);
function findPos(obj)
{
...
}
</script>
I'd appreciate any help!
Thanks!
Ron.
You don't wait for the image to be loaded so, depending on the cache, you may draw an image or not in the canvas.
You should do this :
$(function(){
var img = document.createElement("img");
img.onload = function() {
var mapWrapper = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
...
// your whole code here !
...
}
img.src = "img/main-page-buttons.png";
});

Removing an image from html5 canvas when it's dragged to a certain location

I have an HTML5 canvas that is displaying a number of images and four description boxes. It is currently possible to drag and drop the images around the canvas, but I want to add functionality to remove an image when it is dragged to its correct description box.
I've tried writing the following function, but it does not currently seem to be doing anything... i.e. if I drag an image to its description box and drop it, it still remains on the canvas:
function initStage(images){
var stage = new Kinetic.Stage({
container: "container",
width: 1000,
height: 500
});
var descriptionLayer = new Kinetic.Layer();
//var imagesLayer = new Kinetic.Layer();
var allImages = [];
var currentScore = 0;
var descriptionBoxes = {
assetsDescriptionBox: {
x: 70,
y: 400
},
liabilitiesDescriptionBox: {
x: 300,
y: 400
},
incomeDescriptionBox: {
x: 530,
y: 400
},
expenditureDescriptionBox: {
x: 760,
y: 400
},
};
/*Code to detect whether image has been dragged to correct description box */
for (var key in sources){
/*Anonymous function to induce scope */
(function(){
var privateKey = key;
var imageSource = sources[key];
/*Check if image has been dragged to the correct box, and add it to that box's
array and remove from canvas if it has */
canvasImage.on("dragend", function(){
var descriptionBox = descriptionBoxes[privateKey];
if(!canvasImage.inRightPlace && isNearDescriptionBox(itemImage, descriptionBox)){
context.remove(canvasImage);
/*Will need to add a line in here to add the image to the box's array */
}
})
})();
}
}
The code I've written is based on the tutorial that I found at: http://www.html5canvastutorials.com/labs/html5-canvas-animals-on-the-beach-game-with-kineticjs/
Can anyone spot what I'm doing wrong, and how I can ensure that the image is removed from the canvas when it's dragged to its corresponding description box?
That example bugged me because it seemed old, so I edited it a little...
http://jsfiddle.net/LTq9C/1/
...keep in mind that I cant be positive that all my edits are the best way to do things, Im new and all ;)
And here I've edited it again for you to show the image being removed...
animal.on("dragend", function() {
var outline = outlines[privKey + "_black"];
if (!animal.inRightPlace && isNearOutline(animal, outline)) {
animal.remove();
animalLayer.draw();
}
});
http://jsfiddle.net/8S3Qq/1/

Categories

Resources