Reputation: 2086
I'm building an implementation of Croppie as an AngularJS directive to provide a UI for users to crop their profile images.
My backend is PHP, and I need a way for the UI selections to be reflected in the server-side manipulations.
I have a 150 by 150 square in the center of my image, as the linked Croppie example similarly shows. As you can see however, you can move and scale the image around, which means cropping from center in Imagick wouldn't always work depending on how the user positions their image around the center point.
I know I can scale the image and then crop from the center in Imagick, but how do I account for the fact that the image will be translated as well?
When I move the image around in my implementation, I get the following data-points to parse and send to the server:
{transform: "translate3d(-60px, -121px, 0px) scale(1)"}
This shows for example, that the image has been shifted along the X and Y axis, and has not been scaled. How do I shift the center of the image accordingly in Imagick?
Thank you!
EDIT:
I see these commands: Imagick::cropThumbnailImage and Imagick::cropImage is there some way to kind of combine them where the cropped area remains in the center as the image is scaled and the image can be shifted on the x or y axis?
EDIT 2
Well, I figured out I need to use a combination of Imagick::cropImage and Imagick::scaleImage but I can't figure out the correct math to send to the server.
I'm attaching an image so you can see what I mean, I'm getting very close but the crop is off on some images:
My HTML is like so:
<div class="viewBox">
<img class="bigTuna">
<div class="enclosedCrop small"></div>
<div class="overlay small"></div>
</div>
The image you are seeing, which I want to crop using .encloseCrop.small
which is the circle, is .bigTuna
.
I am using the following to calculate the offset of the image (which I can drag around the .encloseCrop.small
) relative to the scale of the image (.bigTuna
). The offset of .encloseCrop.small
is .viewBox
, and the image takes up the entire .viewBox
with a max-width: 100%;
Because the image has a width
, naturalWidth
and getBoundingClientRect().width
(and respective properties for height) I'm unsure how to accomplish this where I can send to Imagick just an X
and Y
for the cropImage()
arguments.
This is the closest I have gotten:
var Xvalue = (angular.element(".enclosedCrop")[0].offsetLeft + ( -1 * $scope.matrixobject.x )) * ((angular.element('.bigTuna')[0].getBoundingClientRect().width/angular.element('.bigTuna')[0].width)*(angular.element('.bigTuna')[0].naturalWidth/angular.element('.bigTuna')[0].width));
var Yvalue = (angular.element(".enclosedCrop")[0].offsetTop + ( -1 * $scope.matrixobject.y )) * ((angular.element('.bigTuna')[0].getBoundingClientRect().height/angular.element('.bigTuna')[0].height)*(angular.element('.bigTuna')[0].naturalHeight/angular.element('.bigTuna')[0].height));
The bounding width divided by the width is the scale factor, but I need to account for the natural width somehow as well I believe. The issue is that the image you see in the modal where the Croppie
-like directive resides already is scaled because it isn't the natural dimensions. On top of that, I'm scaling this (already) scaled image again using CSS transforms.
But when I send the image to PHP, it doesn't know about this. I just receives the natural dimensions and wants to know how to crop it. I need that crop to reflect the UI crop.
$scope.matrixobject.x
is the value of the transform of the image, in this case, the amount of pixels it was translated left. I can get all these values just fine, I just don't know what equation is actually the correct one utilizing said values.
EDIT 3:
I've refined my equation like so, but this equation, although it makes the most sense to me, gives me completely off crops.
var Xvalue = (angular.element(".enclosedCrop")[0].offsetLeft + ( -1 * $scope.matrixobject.x )) * (angular.element('.bigTuna')[0].naturalWidth/angular.element('.bigTuna')[0].getBoundingClientRect().width);
var Yvalue = (angular.element(".enclosedCrop")[0].offsetTop + ( -1 * $scope.matrixobject.y )) * (angular.element('.bigTuna')[0].naturalHeight/angular.element('.bigTuna')[0].getBoundingClientRect().height);
The equation first tries to get the offset of the 150 x 150 circle (.enclosedCrop
) from from the top and left of the image's container:
In the case of the height, this looks like so:
angular.element(".enclosedCrop")[0].offsetTop + ( -1 * $scope.matrixobject.y )
The first part of this excerpt of the equation gets the offsetTop, which would remain constant as .enclosedCrop
itself is stationary in the center of the container. $scope.matrixobject.y
is the transform of the image around the container, and by inverting it, we can get the transform needed if instead the .enclosedCrop
moved around the image. By adding this to the offset, we know where the crop is happening on the image.
However, the offset and transforms are relative to the size of the image in the container (max-width:100%
) which means that they need to be scaled to reflect the natural size of the image and any transforms that may be occurring using CSS scale
property.
So I'm multiplying the offset by this:
* (angular.element('.bigTuna')[0].naturalHeight/angular.element('.bigTuna')[0].getBoundingClientRect().height);
The natural height divided by the height of the image. Importantly, this "height of the image" is derived from getBoundingClientRect()
which means it takes CSS scaling into account.
So what is wrong with my logic here? I get crops all over the image, not in the coordinates I have specified.
EDIT 4:
I got the very close given the following equation, which I don't really understand (got from Croppie's source code)
var Xvalue = angular.element(".enclosedCrop")[0].getBoundingClientRect().left - angular.element(".bigTuna")[0].getBoundingClientRect().left + angular.element(".enclosedCrop")[0].offsetWidth + (angular.element(".enclosedCrop")[0].getBoundingClientRect().width - angular.element(".enclosedCrop")[0].offsetWidth) / 2;
var Yvalue = angular.element(".enclosedCrop")[0].getBoundingClientRect().top - angular.element(".bigTuna")[0].getBoundingClientRect().top + angular.element(".enclosedCrop")[0].offsetHeight + (angular.element(".enclosedCrop")[0].getBoundingClientRect().height - angular.element(".enclosedCrop")[0].offsetHeight) / 2;
Xvalue = parseFloat(Xvalue).toFixed(0);
Yvalue = parseFloat(Yvalue).toFixed(0);
However it is always a few pixels off, usually slightly down and to the right.
Can someone explain this equation so I can better edit it and find the mistake?
Upvotes: 4
Views: 842
Reputation: 3775
This tricky part by css and js.
Markup html:
<div class="viewBox layer-image">
<img class="bigTuna">
<div class="enclosedCrop small"></div>
<div class="overlay small"></div>
</div>
Css
.layer-image {
background-image: url('sample/image.png');
position: relative;
height: 800px; /* actual height sample*/
weight: 800px; /* actual weight sample*/
}
.enclosedCrop.small {
position: absolute;
}
We set class layer-image have background image and position as relative. well you know base absolute position is relative.
Javascript
Well for dragging, sizing you can use your favorite library or use own code. The big question is how to get x and y. If you see the code, we have already how to get x y. Imagine x y start from top left corner. So what just you need is get margin top of .enclosedCrop.small and margin left of .enclosedCrop.small from layer-image. for ex:
var offsetLayerBase = $('.enclosedCrop.small').offset();
var offsetCropping = $('.enclosedCrop.small').offset();
var yPositionCrop = offsetCropping.top - offsetLayerBase.top;
var xPositionCrop = offsetCropping.left - offsetLayerBase.left;
set offsetLayerBase
as base from window (you need any custom if have css custom).
let me know if work for you. Cheers ;)
Upvotes: 0