I have an image and a set of four points (describing a quadrilateral Q). I want to transform this image so that it is fits the quadrilateral Q. Photoshop calls this transformation "Distort." But according to the source of this quadrilateral (the perspective of the image moving in space), it is in fact the combination of a scale, a rotation and a perspective matrix.
I am wondering if this is possible using a CATransform3D 4x4 matrix. Do you have any hints on how to do that? I've tried to take the four points and build 16 equations (out of A' = A x u) but it did not work: I'm not sure of what I should use as z, z', w and w' coefficients…
The following picture shows what I want to do:
Here are some examples of points:
276.523, 236.438, 517.656, 208.945, 275.984, 331.285, 502.23, 292.344
261.441, 235.059, 515.09, 211.5, 263.555, 327.066, 500.734, 295
229.031, 161.277, 427.125, 192.562, 229.16, 226, 416.48, 256
I've created a kit for doing this on iOS: https://github.com/hfossli/AGGeometryKit/
Make sure your anchor point is top left (CGPointZero).
+ (CATransform3D)rectToQuad:(CGRect)rect
quadTL:(CGPoint)topLeft
quadTR:(CGPoint)topRight
quadBL:(CGPoint)bottomLeft
quadBR:(CGPoint)bottomRight
{
return [self rectToQuad:rect quadTLX:topLeft.x quadTLY:topLeft.y quadTRX:topRight.x quadTRY:topRight.y quadBLX:bottomLeft.x quadBLY:bottomLeft.y quadBRX:bottomRight.x quadBRY:bottomRight.y];
}
+ (CATransform3D)rectToQuad:(CGRect)rect
quadTLX:(CGFloat)x1a
quadTLY:(CGFloat)y1a
quadTRX:(CGFloat)x2a
quadTRY:(CGFloat)y2a
quadBLX:(CGFloat)x3a
quadBLY:(CGFloat)y3a
quadBRX:(CGFloat)x4a
quadBRY:(CGFloat)y4a
{
CGFloat X = rect.origin.x;
CGFloat Y = rect.origin.y;
CGFloat W = rect.size.width;
CGFloat H = rect.size.height;
CGFloat y21 = y2a - y1a;
CGFloat y32 = y3a - y2a;
CGFloat y43 = y4a - y3a;
CGFloat y14 = y1a - y4a;
CGFloat y31 = y3a - y1a;
CGFloat y42 = y4a - y2a;
CGFloat a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42);
CGFloat b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
CGFloat c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
CGFloat d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a);
CGFloat e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42);
CGFloat f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a)));
CGFloat g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43);
CGFloat h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42);
CGFloat i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a));
const double kEpsilon = 0.0001;
if(fabs(i) < kEpsilon)
{
i = kEpsilon* (i > 0 ? 1.0 : -1.0);
}
CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0};
return transform;
}
I take no credit for this code. All I did was scouring the internet and put together various incomplete answers.
Here is a sample project which applies code from hfossli's answer above and creates a category on UIView which sets the frame and applies the transform in one call:
https://github.com/joshrl/FreeTransform
UIView+Quadrilateral code :
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#interface UIView (Quadrilateral)
//Sets frame to bounding box of quad and applies transform
- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br;
#end
#implementation UIView (Quadrilateral)
- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br
{
NSAssert(CGPointEqualToPoint(self.layer.anchorPoint, CGPointZero),#"Anchor point must be (0,0)!");
CGRect boundingBox = [[self class] boundingBoxForQuadTR:tr tl:tl bl:bl br:br];
self.frame = boundingBox;
CGPoint frameTopLeft = boundingBox.origin;
CATransform3D transform = [[self class] rectToQuad:self.bounds
quadTL:CGPointMake(tl.x-frameTopLeft.x, tl.y-frameTopLeft.y)
quadTR:CGPointMake(tr.x-frameTopLeft.x, tr.y-frameTopLeft.y)
quadBL:CGPointMake(bl.x-frameTopLeft.x, bl.y-frameTopLeft.y)
quadBR:CGPointMake(br.x-frameTopLeft.x, br.y-frameTopLeft.y)];
self.layer.transform = transform;
}
+ (CGRect)boundingBoxForQuadTR:(CGPoint)tr tl:(CGPoint)tl bl:(CGPoint)bl br:(CGPoint)br
{
CGRect boundingBox = CGRectZero;
CGFloat xmin = MIN(MIN(MIN(tr.x, tl.x), bl.x),br.x);
CGFloat ymin = MIN(MIN(MIN(tr.y, tl.y), bl.y),br.y);
CGFloat xmax = MAX(MAX(MAX(tr.x, tl.x), bl.x),br.x);
CGFloat ymax = MAX(MAX(MAX(tr.y, tl.y), bl.y),br.y);
boundingBox.origin.x = xmin;
boundingBox.origin.y = ymin;
boundingBox.size.width = xmax - xmin;
boundingBox.size.height = ymax - ymin;
return boundingBox;
}
+ (CATransform3D)rectToQuad:(CGRect)rect
quadTL:(CGPoint)topLeft
quadTR:(CGPoint)topRight
quadBL:(CGPoint)bottomLeft
quadBR:(CGPoint)bottomRight
{
return [self rectToQuad:rect quadTLX:topLeft.x quadTLY:topLeft.y quadTRX:topRight.x quadTRY:topRight.y quadBLX:bottomLeft.x quadBLY:bottomLeft.y quadBRX:bottomRight.x quadBRY:bottomRight.y];
}
+ (CATransform3D)rectToQuad:(CGRect)rect
quadTLX:(CGFloat)x1a
quadTLY:(CGFloat)y1a
quadTRX:(CGFloat)x2a
quadTRY:(CGFloat)y2a
quadBLX:(CGFloat)x3a
quadBLY:(CGFloat)y3a
quadBRX:(CGFloat)x4a
quadBRY:(CGFloat)y4a
{
CGFloat X = rect.origin.x;
CGFloat Y = rect.origin.y;
CGFloat W = rect.size.width;
CGFloat H = rect.size.height;
CGFloat y21 = y2a - y1a;
CGFloat y32 = y3a - y2a;
CGFloat y43 = y4a - y3a;
CGFloat y14 = y1a - y4a;
CGFloat y31 = y3a - y1a;
CGFloat y42 = y4a - y2a;
CGFloat a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42);
CGFloat b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
CGFloat c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
CGFloat d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a);
CGFloat e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42);
CGFloat f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a)));
CGFloat g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43);
CGFloat h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42);
CGFloat i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a));
const double kEpsilon = 0.0001;
if(fabs(i) < kEpsilon)
{
i = kEpsilon* (i > 0 ? 1.0 : -1.0);
}
CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0};
return transform;
}
#end
We finally got this to work. We've tried several different methods, but most were failing. And some were even retrieving a non identity matrix when giving the same points as input and outputs (for example, the one from KennyTM… we must have been missing something there).
Using OpenCV as following, we get a CATransform3D ready to be used on a CAAnimation layer:
+ (CATransform3D)transformQuadrilateral:(Quadrilateral)origin toQuadrilateral:(Quadrilateral)destination {
CvPoint2D32f *cvsrc = [self openCVMatrixWithQuadrilateral:origin];
CvMat *src_mat = cvCreateMat( 4, 2, CV_32FC1 );
cvSetData(src_mat, cvsrc, sizeof(CvPoint2D32f));
CvPoint2D32f *cvdst = [self openCVMatrixWithQuadrilateral:destination];
CvMat *dst_mat = cvCreateMat( 4, 2, CV_32FC1 );
cvSetData(dst_mat, cvdst, sizeof(CvPoint2D32f));
CvMat *H = cvCreateMat(3,3,CV_32FC1);
cvFindHomography(src_mat, dst_mat, H);
cvReleaseMat(&src_mat);
cvReleaseMat(&dst_mat);
CATransform3D transform = [self transform3DWithCMatrix:H->data.fl];
cvReleaseMat(&H);
return transform;
}
+ (CvPoint2D32f *)openCVMatrixWithQuadrilateral:(Quadrilateral)origin {
CvPoint2D32f *cvsrc = (CvPoint2D32f *)malloc(4*sizeof(CvPoint2D32f));
cvsrc[0].x = origin.upperLeft.x;
cvsrc[0].y = origin.upperLeft.y;
cvsrc[1].x = origin.upperRight.x;
cvsrc[1].y = origin.upperRight.y;
cvsrc[2].x = origin.lowerRight.x;
cvsrc[2].y = origin.lowerRight.y;
cvsrc[3].x = origin.lowerLeft.x;
cvsrc[3].y = origin.lowerLeft.y;
return cvsrc;
}
+ (CATransform3D)transform3DWithCMatrix:(float *)matrix {
CATransform3D transform = CATransform3DIdentity;
transform.m11 = matrix[0];
transform.m21 = matrix[1];
transform.m41 = matrix[2];
transform.m12 = matrix[3];
transform.m22 = matrix[4];
transform.m42 = matrix[5];
transform.m14 = matrix[6];
transform.m24 = matrix[7];
transform.m44 = matrix[8];
return transform;
}
With 100% thanks to JoshRL, here's a Swift version of JoshRL's class.
This has been completely and totally debugged. Lines that suffer the "too long in Swift" issue have been refactored and destruction tested. It is working flawlessly in high-volume production.
Couldn't be easier to use. Example showing how to use in Swift below.
2016 Swift version... full, working, copy and paste solution
Refreshed for 2022 ! Drop in code with current syntax etc.
// JoshQuadView in Swift
// from: https://stackoverflow.com/a/18606029/294884
// NB: JoshRL uses the ordering convention
// "topleft, topright, bottomleft, bottomright"
// which is different from "clockwise from topleft".
// Note: is not meant to handle concave.
import UIKit
class JoshQuadView: UIImageView { // or UIView, as preferred
func transformToFitQuadTopLeft(tl: CGPoint, tr: CGPoint, bl: CGPoint, br: CGPoint) {
guard self.layer.anchorPoint == .zero else {
print("suck")
return
}
let b = boundingBoxForQuadTR(tl, tr, bl, br)
self.frame = b
self.layer.transform = rectToQuad(bounds,
.init(x: tl.x-b.origin.x, y: tl.y-b.origin.y),
.init(x: tr.x-b.origin.x, y: tr.y-b.origin.y),
.init(x: bl.x-b.origin.x, y: bl.y-b.origin.y),
.init(x: br.x-b.origin.x, y: br.y-b.origin.y))
}
func boundingBoxForQuadTR(_ tl: CGPoint, _ tr: CGPoint, _ bl: CGPoint, _ br: CGPoint) -> CGRect {
var b: CGRect = .zero
let xmin: CGFloat = min(min(min(tr.x,tl.x),bl.x),br.x)
let ymin: CGFloat = min(min(min(tr.y,tl.y),bl.y),br.y)
let xmax: CGFloat = max(max(max(tr.x,tl.x),bl.x),br.x)
let ymax: CGFloat = max(max(max(tr.y,tl.y),bl.y),br.y)
b.origin.x = xmin
b.origin.y = ymin
b.size.width = xmax - xmin
b.size.height = ymax - ymin
return b
}
func rectToQuad(_ rect: CGRect,
_ topLeft: CGPoint, _ topRight: CGPoint, _ bottomLeft: CGPoint, _ bottomRight: CGPoint) -> CATransform3D {
rectToQuadCalculation(rect,
topLeft.x, topLeft.y,
topRight.x, topRight.y,
bottomLeft.x, bottomLeft.y,
bottomRight.x, bottomRight.y)
}
func rectToQuadCalculation(_ rect: CGRect,
_ x1a: CGFloat, _ y1a: CGFloat,
_ x2a: CGFloat, _ y2a: CGFloat,
_ x3a: CGFloat, _ y3a: CGFloat,
_ x4a: CGFloat, _ y4a: CGFloat) -> CATransform3D {
let XX = rect.origin.x
let YY = rect.origin.y
let WW = rect.size.width
let HH = rect.size.height
let y21 = y2a - y1a
let y32 = y3a - y2a
let y43 = y4a - y3a
let y14 = y1a - y4a
let y31 = y3a - y1a
let y42 = y4a - y2a
let a = -HH * (x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42)
let b = WW * (x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43)
let c0 = -HH * WW * x1a * (x4a*y32 - x3a*y42 + x2a*y43)
let cx = HH * XX * (x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42)
let cy = -WW * YY * (x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43)
let c = c0 + cx + cy
let d = HH * (-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a)
let e = WW * (x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42)
let f0 = -WW * HH * (x4a * y1a * y32 - x3a * y1a * y42 + x2a * y1a * y43)
let fx = HH * XX * (x4a * y21 * y3a - x2a * y1a * y43 - x3a * y21 * y4a + x1a * y2a * y43)
let fy = -WW * YY * (x4a * y2a * y31 - x3a * y1a * y42 - x2a * y31 * y4a + x1a * y3a * y42)
let f = f0 + fx + fy
let g = HH * (x3a * y21 - x4a * y21 + (-x1a + x2a) * y43)
let h = WW * (-x2a * y31 + x4a * y31 + (x1a - x3a) * y42)
let iy = WW * YY * (x2a * y31 - x4a * y31 - x1a * y42 + x3a * y42)
let ix = HH * XX * (x4a * y21 - x3a * y21 + x1a * y43 - x2a * y43)
let i0 = HH * WW * (x3a * y42 - x4a * y32 - x2a * y43)
var i = i0 + ix + iy
let kEpsilon: CGFloat = 0.0001
if abs(i) < kEpsilon {
i = kEpsilon * (i > 0 ? 1 : -1)
}
return CATransform3D(
m11: a/i, m12: d/i, m13: 0, m14: g/i,
m21: b/i, m22: e/i, m23: 0, m24: h/i,
m31: 0, m32: 0, m33: 1, m34: 0,
m41: c/i, m42: f/i, m43: 0, m44: 1.0)
}
}
For a quick test:
#IBOutlet var someImage: JoshQuadView!
someImage.transformToFitQuadTopLeft(
tl: CGPoint(x: 0, y: 0),
tr: CGPoint(x: 400, y: 0),
bl: CGPoint(x: 0, y: 400),
br: CGPoint(x: 400, y: 400))
It will render the normal square image.
someImage.transformToFitQuadTopLeft(
tl: CGPoint(x: -50, y: -20),
tr: CGPoint(x: 400, y: 0),
bl: CGPoint(x: 0, y: 400),
br: CGPoint(x: 400, y: 400))
It will bend away the top left corner.
Important reminder. JoshRL originally used the ordering tl tr bl br. So that is maintained here. It's more common to use tl then clockwise when dealing with verts (ie, tl tr br bl), so just bear it in mind!
To use in Swift with draggable corner handles:
say you have a container view "QuadScreen".
The view you want to stretch will be a JoshQuadView. Drop it in the scene. Connect it to the IBOutlet, "jqv" in the example here.
Simply put four corner-handles (ie, images) in the scene, being PNGs of your handle icons. Link those to the four IBOutlets for handles. The code just completely handles these handles. (Follow the comments in the code for how to easily set them up in storyboard.)
Then, it's just one line of code to do the stretching:
class QuadScreen: UIViewController {
// sit your JoshQuadView in this view
#IBOutlet var jqv: JoshQuadView!
// simply have four small subview views, "handles"
// with an icon on them (perhaps a small circle)
// and put those over the four corners of the jqv
// NOTE numbered CLOCKWISE from top left here:
#IBOutlet var handle1: UIView!
#IBOutlet var handle2: UIView!
#IBOutlet var handle3: UIView!
#IBOutlet var handle4: UIView!
// put a pan recognizer on each handle, action goes to here
// (for the pan recognizers, set cancels-in-view as needed
// if you, example, highlight them on touch in their class)
#IBAction func dragHandle(p: UIPanGestureRecognizer!) {
let tr = p.translationInView(p.view)
p.view!.center.x += tr.x
p.view!.center.y += tr.y
p.setTranslation(.zero, inView: p.view)
jqv.transformToFitQuadTopLeft( handle1.center, tr: handle2.center, bl: handle4.center, br: handle3.center)
// it's that simple, there's nothing else to do
p.setTranslation(.zero, inView: p.view)
}
override func viewDidLayoutSubviews() {
// don't forget to do this....is critical.
jqv.layer.anchorPoint = .zero
}
}
As a curiosity, and for the sake of google, it's ridiculously easy to do this in
Android
they have a built-in command for reshaping polys. This excellent answer has copy and paste code: https://stackoverflow.com/a/34667015/294884
If your new quadrilateral is a parallelogram, then this is called "shear," and can be done most easily with CGAffineTransform. See Jeff LaMarche's excellent article, CGAffineTransform 1.1.
If your new quadrilateral is not a parallelogram, then see the following question for how to apply CATransform3D: iPhone image stretching (skew).
ANCHOR POINT INDEPENDENT Solution:
I really like #joshrl answer where he makes a category "UIView+Quadrilateral" which uses #hfossli's most excellent answer above. However, multiple calls to the category to change the quadrilateral fails, and the code requires the AnchorPoint to be top-left.
My solution (derived from theirs):
Accounts for any AnchorPoint
Allows for changes to the quadrilateral
UIView+Quadrilateral.h:
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#interface UIView (Quadrilateral)
//Sets frame to bounding box of quad and applies transform
- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br;
#end
UIView+Quadrilateral.m:
#import "UIView+Quadrilateral.h"
#implementation UIView (Quadrilateral)
- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br
{
CGRect boundingBox = [[self class] boundingBoxForQuadTR:tr tl:tl bl:bl br:br];
self.layer.transform = CATransform3DIdentity; // keeps current transform from interfering
self.frame = boundingBox;
CGPoint frameTopLeft = boundingBox.origin;
CATransform3D transform = [[self class] rectToQuad:self.bounds
quadTL:CGPointMake(tl.x-frameTopLeft.x, tl.y-frameTopLeft.y)
quadTR:CGPointMake(tr.x-frameTopLeft.x, tr.y-frameTopLeft.y)
quadBL:CGPointMake(bl.x-frameTopLeft.x, bl.y-frameTopLeft.y)
quadBR:CGPointMake(br.x-frameTopLeft.x, br.y-frameTopLeft.y)];
// To account for anchor point, we must translate, transform, translate
CGPoint anchorPoint = self.layer.position;
CGPoint anchorOffset = CGPointMake(anchorPoint.x - boundingBox.origin.x, anchorPoint.y - boundingBox.origin.y);
CATransform3D transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0.);
CATransform3D transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0.);
CATransform3D fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg);
// Now we set our transform
self.layer.transform = fullTransform;
}
+ (CGRect)boundingBoxForQuadTR:(CGPoint)tr tl:(CGPoint)tl bl:(CGPoint)bl br:(CGPoint)br
{
CGRect boundingBox = CGRectZero;
CGFloat xmin = MIN(MIN(MIN(tr.x, tl.x), bl.x),br.x);
CGFloat ymin = MIN(MIN(MIN(tr.y, tl.y), bl.y),br.y);
CGFloat xmax = MAX(MAX(MAX(tr.x, tl.x), bl.x),br.x);
CGFloat ymax = MAX(MAX(MAX(tr.y, tl.y), bl.y),br.y);
boundingBox.origin.x = xmin;
boundingBox.origin.y = ymin;
boundingBox.size.width = xmax - xmin;
boundingBox.size.height = ymax - ymin;
return boundingBox;
}
+ (CATransform3D)rectToQuad:(CGRect)rect
quadTL:(CGPoint)topLeft
quadTR:(CGPoint)topRight
quadBL:(CGPoint)bottomLeft
quadBR:(CGPoint)bottomRight
{
return [self rectToQuad:rect quadTLX:topLeft.x quadTLY:topLeft.y quadTRX:topRight.x quadTRY:topRight.y quadBLX:bottomLeft.x quadBLY:bottomLeft.y quadBRX:bottomRight.x quadBRY:bottomRight.y];
}
+ (CATransform3D)rectToQuad:(CGRect)rect
quadTLX:(CGFloat)x1a
quadTLY:(CGFloat)y1a
quadTRX:(CGFloat)x2a
quadTRY:(CGFloat)y2a
quadBLX:(CGFloat)x3a
quadBLY:(CGFloat)y3a
quadBRX:(CGFloat)x4a
quadBRY:(CGFloat)y4a
{
CGFloat X = rect.origin.x;
CGFloat Y = rect.origin.y;
CGFloat W = rect.size.width;
CGFloat H = rect.size.height;
CGFloat y21 = y2a - y1a;
CGFloat y32 = y3a - y2a;
CGFloat y43 = y4a - y3a;
CGFloat y14 = y1a - y4a;
CGFloat y31 = y3a - y1a;
CGFloat y42 = y4a - y2a;
CGFloat a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42);
CGFloat b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
CGFloat c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
CGFloat d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a);
CGFloat e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42);
CGFloat f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a)));
CGFloat g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43);
CGFloat h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42);
CGFloat i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a));
const double kEpsilon = 0.0001;
if(fabs(i) < kEpsilon)
{
i = kEpsilon* (i > 0 ? 1.0 : -1.0);
}
CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0};
return transform;
}
#end
The above category is so simple and elegant, it ought to be included in every toolbox. THANK YOUs to the ultimate sources of the above code. No credit should be given to me.
Using built-in Swift matrix math:
https://github.com/paulz/PerspectiveTransform#swift-code-example
import PerspectiveTransform
let destination = Perspective(
CGPoint(x: 108.315837, y: 80.1687782),
CGPoint(x: 377.282671, y: 41.4352201),
CGPoint(x: 193.321418, y: 330.023027),
CGPoint(x: 459.781253, y: 251.836131)
)
// Starting perspective is the current overlay frame or could be another 4 points.
let start = Perspective(overlayView.frame)
// Caclulate CATransform3D from start to destination
overlayView.layer.transform = start.projectiveTransform(destination: destination)
#hfossli answer, (the accepted and most voted answer) is calculating the final transform matrix, which is "magic code" that is complicated and unreadable in any way, and I think without any real reason.
What you need to do is the following transformations:
translation x rotation x scaling
(Order is important - you must have the scaling as the most right side, and translation most left).
And then invert the matrix.
(Or you could already calculate the inverted matrix by inverting the order, and doing the opposite transformations (translating in opposite direction, rotating in opposite angle, and scale in inverse size). )
In iOS I guess it will be something along the lines of:
CATransform3D t = CATransform3DIdentity;
t = CATransform3DScale(t, .... )
t = CATransform3DRotate(t, ....)
t = CATransform3DTranslate(t, ....)
CATransform3D invertT = CATransform3DInvert(t);
where you fill .... with the actual scaling, rotation and translation needed.
Related
I'm trying to make Concentration game (with matching cards) and my task is to set frames for the array of 12 UIButtons with for-loop (I tried to realize it with switches, but it wan't right as my mentor said).
So maybe you can help me with this. The code added below works correctly, but l have to escape of switch and make it works with the help of for-loop and universal formulas.
func setFrames() {
for i in 0...11{
var x = CGFloat()
var y = CGFloat()
switch i {
case 0...2:
y = 50
case 3...5:
y = (7/36 * view.bounds.height + 50);
case 6...8:
y = (7/18 * view.bounds.height + 50);
default:
y = (7/12 * view.bounds.height + 50);
}
switch i {
case 0, 3, 6, 9:
x = 1/18 * view.bounds.width; //20
case 1, 4, 7, 10:
x = 3/8 * view.bounds.width; //3/8
default:
x = 3/4 * view.bounds.width - 20;
}
buttonsArray[i].frame = CGRect(x: x, y: y, width: (1/4 * view.bounds.width), height: (1/6 * view.bounds.height))
}
}
You can use something like so:
func placeBtnsIn(frame: CGRect, padding: CGFloat) {
let numCols = 3
let numRows = 4
let buttonWidth = (frame.width - CGFloat(numCols - 1) * padding) / CGFloat(numCols)
let buttonHeight = (frame.height - CGFloat(numRows - 1) * padding) / CGFloat(numRows)
var x: CGFloat
var y: CGFloat
for row in 0...(numRows - 1) {
y = frame.origin.y + (buttonHeight + padding) * CGFloat(row)
for col in 0...(numCols - 1) {
x = frame.origin.x + (buttonWidth + padding) * CGFloat(col)
let button = UIButton()
button.frame = CGRect(x: x, y: y, width: buttonWidth, height: buttonHeight)
self.addSubview(button)
}
}
}
Then in your code use it like so:
placeBtnsIn(frame: yourFrame, padding: 5)
Where yourFrame is a rectangle in which buttons should be drawn, and padding is the distance between button "cells"
In my code below I have two points point1 and point2 and a rectangle. Generally I would like to know how to determine if a line from both points would pass through the rectangle.
var point1 = CGPoint(x: screenSize.width/4,y: screenSize.height/4)
var point2 = CGPoint(x: 3*screenSize.width/4,y: 3*screenSize.height/4)
var rectangle = CGRect(x: sprite.position.x, y: sprite.position.x/2, width: sprite.frame.width, height: sprite.frame.height))
You can do line intersection tests between the line determined by your 2 points and each the 4 sides of the rectangle
func intersectionBetweenSegments(p0: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint) -> CGPoint? {
var denominator = (p3.y - p2.y) * (p1.x - p0.x) - (p3.x - p2.x) * (p1.y - p0.y)
var ua = (p3.x - p2.x) * (p0.y - p2.y) - (p3.y - p2.y) * (p0.x - p2.x)
var ub = (p1.x - p0.x) * (p0.y - p2.y) - (p1.y - p0.y) * (p0.x - p2.x)
if (denominator < 0) {
ua = -ua; ub = -ub; denominator = -denominator
}
if ua >= 0.0 && ua <= denominator && ub >= 0.0 && ub <= denominator && denominator != 0 {
return CGPoint(x: p0.x + ua / denominator * (p1.x - p0.x), y: p0.y + ua / denominator * (p1.y - p0.y))
}
return nil
}
func intersectionBetweenRectAndSegment(rect: CGRect, _ p0: CGPoint, _ p1: CGPoint) {
var result = false
let topLeftCorner = rect.origin
let topRightCorner = CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y)
let bottomLeftCorner = CGPoint(x: rect.origin.x, y: rect.origin.y + rect.size.height)
let bottomRightCorner = CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height)
if intersectionBetweenSegments(po, p1, topLeftCorner, topRightCorner) != nil {
return true
}
if intersectionBetweenSegments(po, p1, topRightCorner, bottomRightCorner) != nil {
return true
}
if intersectionBetweenSegments(po, p1, bottomRightCorner, bottomLeftCorner) != nil {
return true
}
if intersectionBetweenSegments(po, p1, bottomLeftCorner, topLeftCorner) != nil {
return true
}
return false
}
Segment intersection code copied from here.
Not tested!
Okay so I want to rotate CGPoint(A) 50 degrees around CGPoint(B) is there a good way to do that?
CGPoint(A) = CGPoint(x: 50, y: 100)
CGPoint(B) = CGPoint(x: 50, y: 0)
Here's what I want to do:
This is really a maths question. In Swift, you want something like:
func rotatePoint(target: CGPoint, aroundOrigin origin: CGPoint, byDegrees: CGFloat) -> CGPoint {
let dx = target.x - origin.x
let dy = target.y - origin.y
let radius = sqrt(dx * dx + dy * dy)
let azimuth = atan2(dy, dx) // in radians
let newAzimuth = azimuth + byDegrees * CGFloat(M_PI / 180.0) // convert it to radians
let x = origin.x + radius * cos(newAzimuth)
let y = origin.y + radius * sin(newAzimuth)
return CGPoint(x: x, y: y)
}
There are lots of ways to simplify this, and it's a perfect case for an extension to CGPoint, but I've left it verbose for clarity.
public extension CGFloat {
///Returns radians if given degrees
var radians: CGFloat{return self * .pi / 180}
}
public extension CGPoint {
///Rotates point by given degrees
func rotate(origin: CGPoint? = CGPoint(x: 0.5, y: 0.5), _ byDegrees: CGFloat) -> CGPoint {
guard let origin = origin else {return self}
let rotationSin = sin(byDegrees.radians)
let rotationCos = cos(byDegrees.radians)
let x = (self.x * rotationCos - self.y * rotationSin) + origin.x
let y = (self.x * rotationSin + self.y * rotationCos) + origin.y
return CGPoint(x: x, y: y)
}
}
Usage
var myPoint = CGPoint(x: 40, y: 50).rotate(45)
var myPoint = CGPoint(x: 40, y: 50).rotate(origin: CGPoint(x: 0, y: 0), 45)
I'm looking for a way to programmatically create stars, sunburst, and other "spiky" effects using UIBezierPath.
UIBezierPath *sunbeamsPath = [UIBezierPath bezierPath];
[sunbeamsPath moveToPoint: CGPointMake(x, y)];
Are there any algorithms that can generate points for sunburst like shapes programmatically, without paths overlapping?
I'm also interested in an irregular shape sunburst like the one below:
I would imagine that such algorithm would take a certain number of rays, then roughly divide the circle in a number of segments and generate points for such segment in a clockwise direction. Does an algorithm like the one I'm describing already exists or will I have to write one by myself?
Thank you!
I know this old, but I was curious about the first part of this question myself, and going off jrturton's post, I created a custom UIView that generates a UIBezierPath from center of the view. Even animated it spinning for bonus points. Here is the result:
The code I used is here:
- (void)drawRect:(CGRect)rect {
CGFloat radius = rect.size.width/2.0f;
[self.fillColor setFill];
[self.strokeColor setStroke];
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
CGPoint centerPoint = CGPointMake(rect.origin.x + radius, rect.origin.y + radius);
CGPoint thisPoint = CGPointMake(centerPoint.x + radius, centerPoint.y);
[bezierPath moveToPoint:centerPoint];
CGFloat thisAngle = 0.0f;
CGFloat sliceDegrees = 360.0f / self.beams / 2.0f;
for (int i = 0; i < self.beams; i++) {
CGFloat x = radius * cosf(DEGREES_TO_RADIANS(thisAngle + sliceDegrees)) + centerPoint.x;
CGFloat y = radius * sinf(DEGREES_TO_RADIANS(thisAngle + sliceDegrees)) + centerPoint.y;
thisPoint = CGPointMake(x, y);
[bezierPath addLineToPoint:thisPoint];
thisAngle += sliceDegrees;
CGFloat x2 = radius * cosf(DEGREES_TO_RADIANS(thisAngle + sliceDegrees)) + centerPoint.x;
CGFloat y2 = radius * sinf(DEGREES_TO_RADIANS(thisAngle + sliceDegrees)) + centerPoint.y;
thisPoint = CGPointMake(x2, y2);
[bezierPath addLineToPoint:thisPoint];
[bezierPath addLineToPoint:centerPoint];
thisAngle += sliceDegrees;
}
[bezierPath closePath];
bezierPath.lineWidth = 1;
[bezierPath fill];
[bezierPath stroke];
}
And you can download a sample project here:
https://github.com/meekapps/Sunburst
I'm not aware of an algorithm to create these but I do have some advice - create your bezier path such that (0,0) is the centre of the sunburst, then define however many points you need to draw one "beam" of your sunburst going upwards, returning to (0,0)
Then, for as many beams as you want, perform a loop: apply a rotation transform (2 pi / number of beams) to your sunbeam points (CGPointApplyTransform), and add them to the path.
Once you are finished, you can translate and scale the path for drawing.
I used a similar process to draw star polygons recently and it was very simple. Credit to Rob Napier's book for the idea.
Swift version for this
import UIKit
extension Int {
var degreesToRadians: Double { return Double(self) * .pi / 180 }
var radiansToDegrees: Double { return Double(self) * 180 / .pi }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
class SunBurstView: UIView {
override func draw(_ rect: CGRect) {
let radius: CGFloat = rect.size.width / 2.0
UIColor.red.setFill()
UIColor.blue.setStroke()
let bezierPath = UIBezierPath()
let centerPoint = CGPoint(x: rect.origin.x + radius, y: rect.origin.y + radius)
var thisPoint = CGPoint(x: centerPoint.x + radius, y: centerPoint.y)
bezierPath.move(to: centerPoint)
var thisAngle: CGFloat = 0.0
let sliceDegrees: CGFloat = 360.0 / self.beams / 2.0
for _ in 0..<self.beams {
let x = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
let y = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
thisPoint = CGPoint(x: x, y: y)
bezierPath.addLine(to: thisPoint)
thisAngle += sliceDegrees
let x2 = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
let y2 = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
thisPoint = CGPoint(x: x2, y: y2)
bezierPath.addLine(to: thisPoint)
bezierPath.addLine(to: centerPoint)
thisAngle += sliceDegrees
}
bezierPath.close()
bezierPath.lineWidth = 1
bezierPath.fill()
bezierPath.stroke()
}
}
I noticed that the Swift version didn't compile for me or take up enough of the screen, so here's Reinier's answer in Swift 4 adjusted for a rectangular view.
extension Int {
var degreesToRadians: Double { return Double(self) * .pi / 180 }
var radiansToDegrees: Double { return Double(self) * 180 / .pi }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
class SunBurstView: UIView {
var beams: CGFloat = 20
override func draw(_ rect: CGRect) {
self.clipsToBounds = false
self.layer.masksToBounds = false
let radius: CGFloat = rect.size.width * 1.5
UIColor.orange.withAlphaComponent(0.3).setFill()
UIColor.clear.setStroke()
let bezierPath = UIBezierPath()
let centerPoint = CGPoint(x: rect.origin.x + (radius / 3), y: rect.origin.y + (radius / 1.5))
var thisPoint = CGPoint(x: centerPoint.x + radius, y: centerPoint.y)
bezierPath.move(to: centerPoint)
var thisAngle: CGFloat = 0.0
let sliceDegrees: CGFloat = 360.0 / self.beams / 2.0
for _ in 0...Int(beams) {
let x = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
let y = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
thisPoint = CGPoint(x: x, y: y)
bezierPath.addLine(to: thisPoint)
thisAngle += sliceDegrees
let x2 = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
let y2 = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
thisPoint = CGPoint(x: x2, y: y2)
bezierPath.addLine(to: thisPoint)
bezierPath.addLine(to: centerPoint)
thisAngle += sliceDegrees
}
bezierPath.close()
bezierPath.lineWidth = 1
bezierPath.fill()
bezierPath.stroke()
}
}
I am transforming a view with CGAffineTransformMake. It rotates, scales and translates. This works just fine. But I cannot figure out a way to limit the scale to a maximum size.
If the scale is exceeded, I need to still apply the current rotation and translation.
Any suggestions are greatly appreciated!
Source:
UITouch *touch1 = [sortedTouches objectAtIndex:0];
UITouch *touch2 = [sortedTouches objectAtIndex:1];
CGPoint beginPoint1 = *(CGPoint *)CFDictionaryGetValue(touchBeginPoints, touch1);
CGPoint currentPoint1 = [touch1 locationInView:self.superview];
CGPoint beginPoint2 = *(CGPoint *)CFDictionaryGetValue(touchBeginPoints, touch2);
CGPoint currentPoint2 = [touch2 locationInView:self.superview];
double layerX = self.center.x;
double layerY = self.center.y;
double x1 = beginPoint1.x - layerX;
double y1 = beginPoint1.y - layerY;
double x2 = beginPoint2.x - layerX;
double y2 = beginPoint2.y - layerY;
double x3 = currentPoint1.x - layerX;
double y3 = currentPoint1.y - layerY;
double x4 = currentPoint2.x - layerX;
double y4 = currentPoint2.y - layerY;
// Solve the system:
// [a b t1, -b a t2, 0 0 1] * [x1, y1, 1] = [x3, y3, 1]
// [a b t1, -b a t2, 0 0 1] * [x2, y2, 1] = [x4, y4, 1]
double D = (y1-y2)*(y1-y2) + (x1-x2)*(x1-x2);
if (D < 0.1) {
return CGAffineTransformMakeTranslation(x3-x1, y3-y1);
}
double a = (y1-y2)*(y3-y4) + (x1-x2)*(x3-x4);
double b = (y1-y2)*(x3-x4) - (x1-x2)*(y3-y4);
double tx = (y1*x2 - x1*y2)*(y4-y3) - (x1*x2 + y1*y2)*(x3+x4) + x3*(y2*y2 + x2*x2) + x4*(y1*y1 + x1*x1);
double ty = (x1*x2 + y1*y2)*(-y4-y3) + (y1*x2 - x1*y2)*(x3-x4) + y3*(y2*y2 + x2*x2) + y4*(y1*y1 + x1*x1);
return CGAffineTransformMake(a/D, -b/D, b/D, a/D, tx/D, ty/D);
Do something like this:
CGAffineTransform transform = self.view.transform;
float scale = sqrt(transform.a*transform.a + transform.c*transform.c);
if (scale > SCALE_MAX)
self.view.transform = CGAffineTransformScale(transform, SCALE_MAX/scale, SCALE_MAX/scale);
else if (scale < SCALE_MIN)
self.view.transform = CGAffineTransformScale(transform, SCALE_MIN/scale, SCALE_MIN/scale);
At the end of your touchesMoved:withEvent: and updateOriginalTransformForTouches methods. Basically, you check if the current scale exceeds a certain SCALE_MAX value, then multiply your transformation matrix with the reversed scale value.
In case anyone needs #Enzo Tran's answer in Swift with UIPanGestureRecognizer:
func handlePinch(recognizer : UIPinchGestureRecognizer) {
if let view = recognizer.view {
view.transform = CGAffineTransformScale(view.transform,
recognizer.scale, recognizer.scale)
let transform = view.transform
let maxScale: CGFloat = 1.7 //Anything
let minScale: CGFloat = 0.5 //Anything
let scale = sqrt(transform.a * transform.a + transform.c * transform.c)
if scale > maxScale {
view.transform = CGAffineTransformScale(transform, maxScale / scale, maxScale / scale)
}
else if scale < minScale {
view.transform = CGAffineTransformScale(transform, minScale / scale, minScale / scale)
}
recognizer.scale = 1
}
}