Update the rotation of a CALayer - ios

I am trying to update the current rotation (and sometimes the position) of a CALayer.
What I am trying to in a couple of simple steps:
Store a couple of CALayers in an array, so I can reuse them
Set the anchor point of all CALayers to 0,0.
Draw CALayer objects where the object starts at a position on a circle
The layers are rotated by the same angle as the circle at that position
Update the position and rotation of the CALayer to match new values
Here is a piece of code I have:
lineWidth is the width of a line
self.items is an array containing the CALayer objects
func updateLines() {
var space = 2 * M_PI * Double(circleRadius);
var spaceAvailable = space / (lineWidth)
var visibleItems = [Int]();
var startIndex = items.count - Int(spaceAvailable);
if (startIndex < 0) {
startIndex = 0;
}
for (var i = startIndex; i < self.items.count; i++) {
visibleItems.append(self.items[i]);
}
var circleCenter = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
/* Each line should move up and rotate accordin to this value */
var anglePerLine: CGFloat = (360 / CGFloat(visibleItems.count)).toRadians()
/* Starting position, 270 degrees is on top */
var startAngle: CGFloat = CGFloat(270).toRadians();
/* Lines default rotation, we rotate it to get the right side up */
var lineAngle: CGFloat = CGFloat(180).toRadians();
for (var itemIndex = 0; itemIndex < visibleItems.count; itemIndex++) {
var itemLayer = self.itemLayers[itemIndex];
itemLayer.opacity = 1 - ((0.9 / visibleItems.count) * itemIndex);
/* Calculate start position of layer */
var x = CGFloat(circleRadius) * cos(startAngle) + CGFloat(circleCenter.x);
var y = CGFloat(circleRadius) * sin(startAngle) + CGFloat(circleCenter.y);
var height = CGFloat((arc4random() % 80) + 10);
/* Set position and frame of layer */
itemLayer.frame = CGRectMake(CGFloat(x), CGFloat(y), CGFloat(lineWidth), height);
itemLayer.position = CGPointMake(CGFloat(x), CGFloat(y));
var currentRotation = CGFloat((itemLayer.valueForKeyPath("transform.rotation.z") as NSNumber).floatValue);
var newRotation = lineAngle - currentRotation;
var rotationTransform = CATransform3DRotate(itemLayer.transform, CGFloat(newRotation), 0, 0, 1);
itemLayer.transform = rotationTransform;
lineAngle += anglePerLine;
startAngle += anglePerLine;
}
}
The result of the first run is exactly as I want it to be:
The second run through this code just doesn't update the CALayers correctly and it starts to look like this:
I think it has to do with my code to update the location and transform properties of the CALayer, but whatever I do, it always results in the last picture.

Answered via Twitter: setting frames and transform is mutually exclusive. Happy to help. Finding my login credentials for SO is harder. :D

Found the answer thanks to #iosengineer on Twitter. When setting a position on the CALayer, you do not want to update the frame of the layer, but you want to update the bounds.
Smooth animation FTW

Related

CATransform3D.MakeScale is moving layer

I am developing a Xamarin.Forms application for iOS. This app consists of an UIView, which has sublayers which are CALayers. They are added like this:
// draw all the pins from the list
foreach (var pin in _control.PinsSource)
{
var point = new CGPoint
{
X = pin.Longitude,
Y = pin.Latitude
};
var shapeLayer = new CAShapeLayer
{
Name = nameof(MapItem),
Path = MakeCircleAtLocation(point, PinRadius).CGPath,
FillColor = UIColor.Red.CGColor
};
Layer.AddSublayer(shapeLayer);
}
// Create a UIBezierPath which is a circle at a certain location of a certain radius.
private static UIBezierPath MakeCircleAtLocation(CGPoint location, nfloat radius)
{
var path = new UIBezierPath();
path.AddArc(location, radius, 0, (float)(Math.PI * 2.0), true);
return path;
}
Then I have a UIPinchGestureRecognizer which can scale the UIView and some other GestureRecognizers like panning.
Scaling and panning the base view works well. The UIView is scaled using a variable called _currentScale. See the full scale method here:
private void HandlePinch(UIPinchGestureRecognizer recognizer)
{
// Prevent the object to become too large or too small
var newScale = (nfloat)Math.Max(MinZoomLevel, Math.Min(_currentScale * recognizer.Scale, MaxZoomLevel));
if (_currentScale != newScale)
{
_currentScale = newScale;
Transform = CGAffineTransform.MakeScale(_currentScale, _currentScale);
foreach (var subLayer in Layer.Sublayers)
{
if (subLayer.Name == nameof(MapItem))
subLayer.Transform = CATransform3D.MakeScale(PinRadius / _currentScale, PinRadius / _currentScale, 1);
}
}
recognizer.Scale = 1;
}
If the sublayer is a map pin, I did like to NOT scale it with the _currentScale, so that's why I am dividing the scale using PinRadius / _currentScale.
The scaling is working fine, however the pin is moving across the map which is weird. See here:
How can I resolve this?
Unfortunately I couldn't find another way then recreating a new CAShapeLayer on every pinch. It's very ugly, but it's working.
private void HandlePinch(UIPinchGestureRecognizer recognizer)
{
// Prevent the object to become too large or too small
var newScale = (nfloat)Math.Max(MinZoomLevel, Math.Min(_currentScale * recognizer.Scale, _maxZoomLevel));
if (_currentScale != newScale)
{
_currentScale = newScale;
_currentPinRadius = _pinRadius / _currentScale;
Transform = CGAffineTransform.MakeScale(_currentScale, _currentScale);
// First layer is a CALayer, so start at 1
for (var i = 1; i < Layer.Sublayers.Length; i++)
{
var caLayer = ((CAShapeLayer)Layer.Sublayers[i]);
var cgPoint = new CGPoint
{
X = _control.PinsSource[i - 1].Longitude,
Y = _control.PinsSource[i - 1].Latitude
};
caLayer.Path = CreateCircle(cgPoint, _currentPinRadius);
}
}
recognizer.Scale = 1;
}

Draw a circle with defined diameter in OpenLayers

I'm trying to write a code which let my users define some points on the map and once they created a point, the program should draw a circle with defined diameter(in kilometers or ... ) around the point.
I can draw a point but I don't know how I could handle what I said.
Here is an example about what I want:
Use the following function to create circular points around a point.
//pass the
//#pointX,
//#pointY,
//#radius of circle (in your case diameter/2)
//#pointsToFind this is how detail you want the circle to be (360 if you want one point for each rad)
function createCirclePointCoords(circleCenterX,circleCenterY,circleRadius,pointsToFind){
var angleToAdd = 360/pointsToFind;
var coords = [];
var angle = 0;
for (var i=0;i<pointsToFind;i++){
angle = angle+angleToAdd;
console.log(angle);
var coordX = circleCenterX + circleRadius * Math.cos(angle*Math.PI/180);
var coordY = circleCenterY + circleRadius * Math.sin(angle*Math.PI/180);
coords.push([coordX,coordY]);
}
return coords;
}
And then create a polygon out of the coordinates returned from the function
var circleCoords = createCirclePointCoords(0,0,1000,360);
var geom = new ol.geom.Polygon([
circleCoords
])

Getting object at location on screen in Metal

I'm using Metal and I want the user to be able to tap certain objects onscreen. I'm using a tap gesture recognizer, and so I have a CGPoint, but I don't know how to find the object at that point. Is there either a way to get the onscreen coordinates for metal, or a way to transform the CGPoint into the metal coordinate space?
I ended up doing this with a raytracing-type technique. I used a bounding sphere for each object and calculated whether or not the ray intersected it and how far from the ray's origin the intersection was, with the ray being from the camera through the onscreen point of clicking. The code is as follows:
var location = gestureRecognizer.locationInView(self.view)
var proj_matrix = projectionMatrix.copy()
var view_matrix = worldMatrix.copy()
var x = (2.0 * location.x) / self.view.bounds.size.width - 1.0;
var y = 1.0 - (2.0 * location.y) / self.view.bounds.size.height;
var newLocation = simpleVertex(x: Float(x), y: Float(y), z: Float(-1.0))
var projSpaceLoc = GLKMatrix4MultiplyVector4(GLKMatrix4Invert(proj_matrix.matrix(), nil), GLKVector4Make(newLocation.x, newLocation.y, newLocation.z, 1.0))
projSpaceLoc = GLKVector4Make(projSpaceLoc.x, projSpaceLoc.y, -1.0, 0.0)
var viewSpaceLoc = GLKMatrix4MultiplyVector4(GLKMatrix4Invert(view_matrix.matrix(), nil), projSpaceLoc)
GLKVector4Normalize(viewSpaceLoc)
//tappable is an array of nodes that can be tapped
for node in tappable {
var r: Float = node.r //this is the radius of the bounding sphere for that node
var c: GLKVector4 = node.origin //this is the origin of the bounding sphere
var little_c = GLKVector4DotProduct(c, c) - powf(r, 2)
var b = GLKVector4DotProduct(viewSpaceLoc, c)
var discriminant = powf(b, 2) - little_c
//If the discriminant is positive, their are two points of intersection, if it is 0, one point, and if it is negative, no points.
if (discriminant >= 0) {
var t_1 = (Float(-1.0)*b) + sqrtf(discriminant)
var t_2 = (Float(-1.0)*b) - sqrtf(discriminant)
if (intersect != nil) {
if (min(t_1, t_2) < intersect.intersect_point) {
intersect = (node, min(t_1, t_2))
}
} else {
intersect = (node, min(t_1, t_2))
}
}
}

Animate multiple shapes in UIView

I have a custom class that inherit from UIView. In the draw method I draw several shapes including some circles. I want to animate the color (now stroke color) of the circles independent of each other, e.g. I would like the color of one or more the circles to "pulse" or flash (using ease-in/ease-out and not linearly).
What would be the best way to archive this?
It would be great to be able to use the built-in animation code (CABasicAnimation and the like) but I'm not sure how?
EDIT: Here's the code involved. (I am using Xamarin.iOS but my question is not specific to this).
CGColor[] circleColors;
public override void Draw (RectangleF rect)
{
base.Draw (rect);
using (CGContext g = UIGraphics.GetCurrentContext ()) {
g.SetLineWidth(4);
float size = rect.Width > rect.Height ? rect.Height : rect.Width;
float xCenter = ((rect.Width - size) / 2) + (size/2);
float yCenter = ((rect.Height - size) / 2) + (size/2);
float d = size / (rws.NumCircles*2+2);
var circleRect = new RectangleF (xCenter, yCenter, 0, 0);
for (int i = 0; i < rws.NumCircles; i++) {
circleRect.X -= d;
circleRect.Y -= d;
circleRect.Width += d*2;
circleRect.Height += d*2;
CGPath path = new CGPath ();
path.AddEllipseInRect (circleRect);
g.SetStrokeColor (circleColors [i]);
g.AddPath (path);
g.StrokePath ();
}
}
}
You need to move all your drawing code to a subclass of CALayer, and decide parameters which, once varied, will produce the desired animations. Convert these parameters to the layer's properties, and you can animate the layer's properties with CABasicAnimation (or even [UIView animateXXX]).
See this SO question for more information.
Make sure that you set the layer's rasterizationScale to [UIScreen mainScreen].scale to avoid blurs on Retina.

Starling + Box2d - Collision not precise

I create stage walls and a box inside on my mobile app using starling + as3.
Ok, now when I test the app the box falls but it does not match the walls, as if there
was an offset:
https://www.dropbox.com/s/hd4ehnfthh0ucfm/box.png
Here is how I created the boxes (walls and the box).
It seems like there is an offset hidden, what do you think?
public function createBox(x:Number, y:Number, width:Number, height:Number, rotation:Number = 0, bodyType:uint = 0):void {
/// Vars used to create bodies
var body:b2Body;
var boxShape:b2PolygonShape;
var circleShape:b2CircleShape;
var fixtureDef:b2FixtureDef = new b2FixtureDef();
fixtureDef.shape = boxShape;
fixtureDef.friction = 0.3;
// static bodies require zero density
fixtureDef.density = 0;
var quad:Quad;
bodyDef = new b2BodyDef();
bodyDef.type = bodyType;
bodyDef.position.x = x / WORLD_SCALE;
bodyDef.position.y = y / WORLD_SCALE;
// Box
boxShape = new b2PolygonShape();
boxShape.SetAsBox(width / WORLD_SCALE, height / WORLD_SCALE);
fixtureDef.shape = boxShape;
fixtureDef.density = 0;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.2;
// create the quads
quad = new Quad(width, height, Math.random() * 0xFFFFFF);
quad.pivotX = 0;
quad.pivotY = 0;
// this is the key line, we pass as a userData the starling.display.Quad
bodyDef.userData = quad;
//
body = m_world.CreateBody(bodyDef);
body.CreateFixture(fixtureDef);
body.SetAngle(rotation * (Math.PI / 180));
_clipPhysique.addChild(bodyDef.userData);
}
The SetAsBox method takes half width and half height as its parameters. I'm guessing your graphics don't match your box2d bodies. So either you will need to make your graphics twice as big or multiply your SetAsBox params by 0.5. Also the body pivot will be in the center of it, so offset your movieclip accordingly depending on its pivot position.
Note that box2d has a debugrenderer which can outline your bodies for you to see what's going on.

Resources