• 如果还没看前一篇,可以移驾看看:ios 3D引擎 SceneKit 开发(4) –关于旋转的几点问题(1)上一篇我们用CABasicAnimation 来模拟了太阳-地球-月球的天体运动。其中月球绕太阳运动和月球绕地球运动都可以看做一个点绕另...

    如果还没看前一篇,可以移驾看看:ios 3D引擎 SceneKit 开发(4) –关于旋转的几点问题(1)

    上一篇我们用CABasicAnimation 来模拟了太阳-地球-月球的天体运动。其中月球绕太阳运动和月球绕地球运动都可以看做一个点绕另一个点作圆周运动。(当然现实中是椭圆运动,有远地点,近地点,这里我们看作圆周运动)

    一个点绕另一个点作圆周运动,是不是很熟悉。对,就是我们之前学习的数学知识,这里完全可以用数学知识做。

    相关数学知识点: 任意点a(x,y),绕一个坐标点b(rx0,ry0)逆时针旋转a角度后的新的坐标设为c(x0, y0),有公式:

    x0= (x - rx0)*cos(a) - (y - ry0)*sin(a) + rx0 ;
    
    y0= (x - rx0)*sin(a) + (y - ry0)*cos(a) + ry0 ;
    

    OK,有这些数学基础,那我们就很好做了,我们让地月系统绕太阳转的效果用数学方法来实现。太阳(sunNode)是b点,地月系统(earthGroupNode)是a点,我们将地月系统添加到太阳里面:

    [_sunNode addChildNode:_earthGroupNode];

    那么相对于a点来说,b点的坐标就是(0,0),然后我们通过计算得到c点,让c点的坐标重新赋值给earthGroupNode 的 position 就可以了。代码如下:

     // custom Action
    
        float totalDuration = 10.0f;        //10s 围绕地球转一圈
        float duration = totalDuration/360; //每隔duration秒去执行一次
    
    
        SCNAction *customAction = [SCNAction customActionWithDuration:duration actionBlock:^(SCNNode * _Nonnull node, CGFloat elapsedTime){
    
    
            if(elapsedTime==duration){
    
    
                SCNVector3 position = node.position;
    
                float rx0 = 0;    //原点为0
                float ry0 = 0;
    
                float angle = 1.0f/180*M_PI;
    
                float x =  (position.x - rx0)*cos(angle) - (position.z - ry0)*sin(angle) + rx0 ;
    
                float z = (position.x - rx0)*sin(angle) + (position.z - ry0)*cos(angle) + ry0 ;
    
                node.position = SCNVector3Make(x, node.position.y, z);
    
            }
    
        }];
    
        SCNAction *repeatAction = [SCNAction repeatActionForever:customAction];
    
        [_earthGroupNode runAction:repeatAction];

    从上面可以看出我们用了SceneKit 的API SCNAction 去循环计算赋值,其实最主要的就是actionBlock 里面的代码,你也可以完全用线程sleep 和 NSTimer 去实现。

    最终实现效果:

    这里写图片描述

    引出的问题:我们可以看到上面数学方法的局限性,y 轴的值一直没变,因为三个天体都处于X-Z这一平面,如果他们 y 值不一样,没有处于X-Z这一平面。即一个三维点绕着另一个三维点做圆周运动,数学方法该怎么实现?

    demo 代码已上传到github

    https://github.com/pzhtpf/SceneKitRoationDemo

    展开全文
  • iOS的UI是基于UIView类的,我们能看到的每个UI元素都是UIView或者UIView的子类。View按树形结构组织起来,树根是UIWindow。  View负责界面的交互和显示,其中显示部分由CALayer来完成。每个UIView包含一个...

     iOS的UI是基于UIView类的,我们能看到的每个UI元素都是UIView或者UIView的子类。View按树形结构组织起来,树根是UIWindow。

         View负责界面的交互和显示,其中显示部分由CALayer来完成。每个UIView包含一个CALayer实例。可以这么认为,UIView本身是不可见的,我们能看到的都是CALayer,UIView只是负责对CALayer进行管理。

        UIView的显示设置都是对CALayer属性的封装,但是这层封装掩盖了CALayer提供的3D显示功能。所以我们想让UIView显示3D的效果的话,需要直接操作CALayer。

    很简单,注意保持一个清晰的空间想象力,然后把每一个CALayer执行相应的3D变换,最后使用Core Animation是主Layer动起来,OK。

    让我们开始动手!首先,在ViewController中定义主Layer,这个CALayer用来存放其他子Layer,我们一共需要6个子Layer,每一个子Layer代表正方体的一个面。

    //Layer

    CALayer *_rootLayer;

     

    接着,也是最重要的,定义一个创建3D变换后的CALayer辅助函数。

    这里为了使Layer有渐变色,所以使用CAGradientLayer类型,因此第一步就是设置好CAGradientLayer的那些杂七杂八的属性(颜色,位置等),第二步,从参数中获取偏移和旋转3D变换的值,然后执行相应的变换。具体参数我们会在之后调用这个方法时传入,这里总共需要用来偏移的X,Y,Z参数和用来旋转的角度,X,Y,Z参数,一共7个参数。设置好3D Transform后,这个方法的第三步就是把这个新的Layer加入到主Layer中。

    整个方法代码如下:

    //用来添加经过3D变换的CALayer

    - (void)addLayer:(NSArray*)params

    {

        //创建支持渐变背景的CAGradientLayer

        CAGradientLayer *gradient = [CAGradientLayer layer];

        //设置位置,和颜色等参数

        gradient.contentsScale = [UIScreen mainScreen].scale;

        gradient.bounds = CGRectMake(00100100);  //这里是生成的图像的大小,注意这个大小必须等于同一个轴上的两个坐标之差。

        gradient.position = CGPointMake(CGRectGetMidX(self.view.bounds),CGRectGetMidY(self.view.bounds));

        gradient.colors = @[(id)[UIColor grayColor].CGColor, (id)[UIColor blackColor].CGColor];

        gradient.locations = @[@0@1];

        gradient.startPoint = CGPointMake(00);

        gradient.endPoint = CGPointMake(01);

       

        //根据参数对CALayer进行偏移和旋转Transform

        CATransform3D transform = CATransform3DMakeTranslation([[params objectAtIndex:0floatValue], [[params objectAtIndex:1floatValue], [[params objectAtIndex:2floatValue]);

        transform = CATransform3DRotate(transform, [[params objectAtIndex:3floatValue], [[paramsobjectAtIndex:4floatValue], [[params objectAtIndex:5floatValue], [[params objectAtIndex:6]floatValue]);

        //设置transform属性并把Layer加入到主Layer

        gradient.transform = transform;

        [_rootLayer addSublayer:gradient];

    }

     

    接着,在ViewController的viewDidLoad方法内,开始利用这个辅助函数来创建每一个面,注意最后要将主Layer进行一次3D变换,这样才能看出3D效果。

    如下代码:

    //创建主Layer

    _rootLayer = [CALayer layer];

    _rootLayer.contentsScale = [UIScreen mainScreen].scale;

    _rootLayer.frame = self.view.bounds;

     //下面将生成六个面,每个面都是垂直与自己所在的坐标,并且相应的坐标值为面大小的一半。

    //

    [self addLayer:@[@0@0@50@0@0@0@0]];

    //

    [self addLayer:@[@0@0@(-50)@(M_PI)@0@0@0]];

    //

    [self addLayer:@[@(-50)@0@0@(-M_PI_2)@0@1@0]];

    //

    [self addLayer:@[@50@0@0@(M_PI_2)@0@1@0]];

    //

    [self addLayer:@[@0@(-50)@0@(-M_PI_2)@1@0@0]];

    //

    [self addLayer:@[@0@50@0@(M_PI_2)@1@0@0]];

     

    //Layer3D变换

    CATransform3D transform = CATransform3DIdentity;

        /*解释下M34

         struct CATransform3D

         {

         CGFloat m11x缩放), m12y切变), m13(旋转), m14();

         CGFloat m21x切变), m22y缩放), m23(), m24();

         CGFloat m31(旋转), m32(), m33(), m34(透视效果,要操作的这个对象要有旋转的角度,否则没有效果。正直/负值都有意义);

         CGFloat m41x平移), m42y平移), m43z平移), m44();

         };

         ps:整体比例变换时,也就是m11==m22时,若m33>1,图形整体缩小,若0<m33<1,图形整体放大,若s<0,发生关于原点的对称等比变换。

         首先要实现viewlayer)的透视效果(就是近大远小),是通过设置m34的:

         CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;

         rotationAndPerspectiveTransform.m34 = 1.0 / -500;

         m34负责z轴方向的translation(移动),m34= -1/D,  默认值是0,也就是说D无穷大,这意味layer in projection plane(投射面)和layer in world coordinate重合了。

         D越小透视效果越明显。

         所谓的D,是eye(观察者)到投射面的距离。

         */

    transform.m34 = -1.0 / 700;

    //X轴上做一个20度的小旋转 在那个轴上面旋转就是对应的坐标值为1,当前为X轴旋转

    transform = CATransform3DRotate(transform, M_PI / 9100); 

    //设置CALayersublayerTransform

    _rootLayer.sublayerTransform = transform;

    //添加Layer

    [self.view.layer addSublayer:_rootLayer];

     

    OK,这个时候,静态的模型已经定义好了,最后我们要把整个正方体动起来,为了达到更好的视觉效果,之前我们已经对主Layer在X轴上做了20度角的小旋转,而对于动画,我们则需要对主Layer在Z轴上做一个360度的旋转,然后设置动画的重复次数为无限次,这样的话,方块就会无限次得转起来,代码:

    //动画

    CABasicAnimation *animation = [CABasicAnimationanimationWithKeyPath:@"sublayerTransform.rotation.y"];

    //0360

    animation.toValue = [NSNumber numberWithFloat:2 * M_PI];

    //间隔3

    animation.duration = 3.0;

    //无限循环

    animation.repeatCount = HUGE_VALF;

    //开始动画

    [_rootLayer addAnimation:animation forKey:@"rotation"]

    代码可在这里下载

    展开全文
  • 前几篇文章介绍了很多Unity3D引擎自身的一些问题, 今天我们在回到IOS设备上讨论一些触摸屏幕手势,本章的目标是通过触摸iPhone屏幕手势 实现模型左右的旋转,与模型的缩放。 大家想一想模型的旋转,实际上是镜头的...

    前几篇文章介绍了很多Unity3D引擎自身的一些问题, 今天我们在回到IOS设备上讨论一些触摸屏幕手势,本章的目标是通过触摸iPhone屏幕手势 实现模型左右的旋转,与模型的缩放。

    大家想一想模型的旋转,实际上是镜头的旋转。模型的缩放实际上是镜头Z轴方向的坐标。那么实现本章的内容只需要控制镜头的位置方可实现。

    我们创建一个简单的游戏平面, 然后平面中放一个箱子做为旋转缩放的参照物。如下图所示,选中摄像机,给摄像机添加一个脚本名称为Move. 脚本中有一个参数 Target,它的作用是设置摄像头旋转移动参照物,这里把一个箱子赋值给了Target,那么左右滑动屏幕会发现箱子在旋转,两手缩放屏幕会发现箱子在放大与缩小。

     
        //用于绑定参照物对象
        public Transform target;
        //缩放系数
        float distance = 10.0f;
        //左右滑动移动速度
        float xSpeed = 250.0f;
        float ySpeed = 120.0f;
        //缩放限制系数
        float yMinLimit = -20f;
        float yMaxLimit = 80f;
        //摄像头的位置
        float x = 0.0f;
        float y = 0.0f;
        //记录上一次手机触摸位置判断用户是在左放大还是缩小手势
        private Vector2 oldPosition1;
        private Vector2 oldPosition2;
    
        //初始化游戏信息设置
        void Start()
        {
            //eulerAngles(欧拉角):x、y、z角代表绕z轴旋转z度,绕x轴旋转x度,绕y轴旋转y度(这个顺序)。
            var angles = transform.eulerAngles;//即摄像头的相对Rotation的值
            x = angles.y;
            y = angles.x;
    
            // Make the rigid body not change rotation
            if (rigidbody)
                rigidbody.freezeRotation = true;//冻结旋转,如果freezeRotation被启用,旋转不会被物体模拟修改。这对于创建第一人称射击游戏时是很有用的,因为玩家需要使用鼠标完全控制旋转。
        }
    
        void Update()
        {
            //判断触摸数量为单点触摸
            if (Input.touchCount == 1)
            {
                //触摸类型为移动触摸
                if (Input.GetTouch(0).phase == TouchPhase.Moved)
                {
                    //根据触摸点计算X与Y位置
                    x += (float)(Input.GetAxis("Mouse X") * xSpeed * 0.02);
                    y -= (float)(Input.GetAxis("Mouse Y") * ySpeed * 0.02);
                }
            }
    
            //判断触摸数量为多点触摸
            if (Input.touchCount > 1)
            {
                //前两只手指触摸类型都为移动触摸
                if (Input.GetTouch(0).phase == TouchPhase.Moved || Input.GetTouch(1).phase == TouchPhase.Moved)
                {
                    //计算出当前两点触摸点的位置
                    var tempPosition1 = Input.GetTouch(0).position;
                    var tempPosition2 = Input.GetTouch(1).position;
                    //函数返回真为放大,返回假为缩小
                    if (isEnlarge(oldPosition1, oldPosition2, tempPosition1, tempPosition2))
                    {
                        //放大系数超过3以后不允许继续放大
                        //这里的数据是根据我项目中的模型而调节的,大家可以自己任意修改
                        if (distance > 3)
                        {
                            distance -= 0.5f;
                        }
                    }
                    else
                    {
                        //缩小系数返回18.5后不允许继续缩小
                        //这里的数据是根据我项目中的模型而调节的,大家可以自己任意修改
                        if (distance < 18.5)
                        {
                            distance += 0.5f;
                        }
                    }
                    //备份上一次触摸点的位置,用于对比
                    oldPosition1 = tempPosition1;
                    oldPosition2 = tempPosition2;
                }
            }
        }
    
        //函数返回真为放大,返回假为缩小
        bool isEnlarge(Vector2 oP1, Vector2 oP2, Vector2 nP1, Vector2 nP2)
        {
            //函数传入上一次触摸两点的位置与本次触摸两点的位置计算出用户的手势
            // var leng1 = Mathf.Sqrt((oP1.x - oP2.x) * (oP1.x - oP2.x) + (oP1.y - oP2.y) * (oP1.y - oP2.y));
            // var leng2 = Mathf.Sqrt((nP1.x - nP2.x) * (nP1.x - nP2.x) + (nP1.y - nP2.y) * (nP1.y - nP2.y));
            var leng1 = Vector2.Distance(oP1, oP2);
            var leng2 = Vector2.Distance(nP1, nP2);
            if (leng1 < leng2)
            {
                //放大手势
                return true;
            }
            else
            {
                //缩小手势
                return false;
            }
        }
        //Update方法一旦调用结束以后进入这里算出重置摄像机的位置
        void LateUpdate()
        {
            //target为我们绑定的箱子变量,缩放旋转的参照物
            if (target)
            {
                //重置摄像机的位置
                y = ClampAngle(y, yMinLimit, yMaxLimit);
                var rotation = Quaternion.Euler(y, x, 0);
                var position = rotation * new Vector3(0.0f, 0.0f, -distance) + target.position;
                //rotation.ToAngleAxis(out zwhangle, out zwhaxis); 转换一个旋转用“角-轴”表示。
                //position表示此时将camera对象的位置设置为new Vector3(0.0f, 0.0f, -distance),为起点位置,然后从这个位置绕zwhaxis轴旋转zwhangle角度为止,从而获得新的位置,而加上target.position,是使camera对象的位置距离zwhaxis轴的更远
                //transform.rotation = rotation;//这句可以用下面的方式,都是一样的
                transform.position = position;
                transform.rotation = Quaternion.LookRotation(target.position - transform.position);
            }
        }
    
        static float ClampAngle(float angle, float min, float max)
        {
            if (angle < -360)
                angle += 360;
            if (angle > 360)
                angle -= 360;
            return Mathf.Clamp(angle, min, max);
        }

     

    我们看看Move这条脚本,说明一下几个重要的 :

    这些方法都是系统自己调用的方法

    function Start () : 游戏启动以后只调用一次,可用于脚本的初始化操作,

    function Update ():Start()方法调用结束以后每一帧都会调用,可以在这里更新游戏逻辑。

    function LateUpdate (): Start()方法调用结束以后每一帧都会调用,但是它是在 Update()调用完后调用。

     

    上面关键的代码是:

    var position = rotation * new Vector3(0.0f, 0.0f, -distance) + target.position;

    我已经注释说明了!不懂的可以看另一个列子,有助于理解:
        public GameObject cameraObject;
        public float cameraDistance;//the distance the camera should be palced from the palyer
        public float cameraHeight;//how heigh the camera should be
        private float cameraAngleToPlayer;// the current angle the camera is to the palyer
        private Vector3 tempVector; // the temporary vector we shall use for calcuations
    
        void Update()
        {
            tempVector = Vector3.left;
            if (Input.GetKey("left")) //rotation the angle based on input
            {
                cameraAngleToPlayer = cameraAngleToPlayer - (Time.deltaTime * 50f);
            }
            else if (Input.GetKey("right"))
            {
                cameraAngleToPlayer = cameraAngleToPlayer + (Time.deltaTime * 50f);
            }
            tempVector = Quaternion.AngleAxis(cameraAngleToPlayer, Vector3.up) * tempVector;
            Debug.DrawLine(transform.position, tempVector * 10f, Color.yellow);
            //cameraObject.transform.position = transform.position + (tempVector.normalized * cameraDistance);
            cameraObject.transform.position = tempVector; //tempVector表示此时的camera对象的位置将从Vector3.left(-1f,0,0)这个位置(起点位置)绕Vector3.up轴旋转到cameraAngleToPlayer角度为止,从而获得新的位置,而乘以10,是使camera对象的位置距离Vector3.up轴的更远
            cameraObject.transform.rotation = Quaternion.LookRotation(transform.position - cameraObject.transform.position);
        }

    参考:http://www.cnblogs.com/88999660/archive/2013/08/16/3262656.html

     

     

    转载于:https://www.cnblogs.com/MrZivChu/p/touchrotationscale.html

    展开全文
  • 如何创建坐标,如何根据坐标方便的控制球绕着x和y轴旋转 如何实现3d效果 如何使得各个标题在球上均匀分布 如何实现点击标题后的回调函数 我的解决方案 关于坐标 肯定是使用球坐标最方便啦,这里需要再温习一下球...
        

    什么是3d文字球

    我也不知道专业的名字叫什么,总之效果如下:
    动态效果

    需要考虑的关键问题

    1. 如何创建坐标,如何根据坐标方便的控制球绕着x和y轴旋转

    2. 如何实现3d效果

    3. 如何使得各个标题在球上均匀分布

    4. 如何实现点击标题后的回调函数

    我的解决方案

    关于坐标

    肯定是使用球坐标最方便啦,这里需要再温习一下球坐标的表示方法:
    球坐标系

    如图,一个3维空间中的点,用球坐标可以表示为(r, Ry, Rxz),其中r是半径大小,Ry是半径和y轴的夹角;Rxz是半径在xz平面上的投影和z轴的夹角;类似的,这个点也可以表示为(r, Rx, Ryz),Rx是半径和x轴的夹角;Ryz是半径在yz平面上的投影和z轴的夹角。这样,绕着x轴旋转就改变Ryz,绕着y轴的旋转就改变Rxz即可。事实上,这2种坐标是可以相互转换的,因为他们都表示球面上同一个点,使用2种表示,只是为了方便后续控制文字球绕着x、y轴旋转。

    3d效果

    确定了坐标系的选择,3d效果只要保证把坐标算对就行了。但是,在iOS中单纯设置zPosition无法实现透视效果(即近处文字大远处文字小),需要设置

    self.layer.transform.m34 = CGFloat(-1.0/perspective)

    才行。perspective越小,深度越大,透视效果越强。具体解释见这里

    球面title的均匀分布问题

    我采用的算法是这样的:

    先根据纬度,在球上等间距切出N个圆环,然后计算N个圆环的总长度,用title总数除以该长度,得到每单位长度获得的title数,然后单位title数*每个圆环的周长即为每个圆环上应该获得的title数,用360度除以这个数,就得到圆上每隔多少度需要安排一个title了。

    实现点击标题后的回调函数

    UIView类本身就包含touchesEnded:withEvent:函数,所以子类override这个函数即可。该函数可以获得touch到的x、y坐标,算出该坐标下的所有title,取zPosition最大的作为目标title即可。swift中,回调函数可以当做变量传入,跟javascript类似。当确定了目标title之后,把该title作为回调函数的参数传入,执行回调函数。

    具体代码实现

    代码结构

    详细代码见github,我在文章里就说一些重点和当时遇到的坑。欢迎大家批评指正!

    主要代码在Label3DView.swift文件中,其中包含2个类:

    // Label3DView.swift
    // 容器View类,包含所有的title。负责对title进行统一配置:
    public class Label3DView: UIView {
    // 配置项包括:
        public var perspective:Float = 2000
        public var fontSize:CGFloat = 15
        public var fontColor:UIColor = UIColor.blackColor()
        public var sphereRadius:Float = 0.5
        public var onEachLabelClicked : ((label:UILabel)->Void)?
    // 功能函数包括:
    //  从resource文件中读取所有title:
        public func loadLabelsFromFile(fpath:String) {}
    //  配置项重新赋值后,统一配置所有title,把它们正确画在容器View中,需要在viewLoad时调用:
        public func resetLabelOnView() {}
    }
    
    // 这是放置每个title的label类,继承自UILabel。最核心的代码都在这里面
    class LabelSphere: UILabel {
        // cx、cy保存x、y轴的偏移。因为我的球坐标系默认以(0,0,0)为原点,但是画到容器类时,必须偏移到容器类的中心点。
        var cx:Float
        var cy:Float 
        // 下面的属性名字基本上和以上介绍球坐标系时一样。
        // 半径在xz平面上的投影和z轴之间的夹角
        var rxz:Float
        // label和z轴夹角
        var ry:Float
        var radius:Float
        // 辅助属性:ryz和rx都是根据rxz、ry和radius算出来的。
        // 半径在yz平面上的投影和z轴之间的夹角
        var ryz:Float 
        // 辅助属性:
        // label和x轴夹角
        var rx:Float 
        
        var perspective:Float
    }

    辅助属性的计算

    这里需要特别繁琐的数学运算,尽管原理其实很简单。但是对于毕业已经好几年的我来说,还是有些挑战。。。

    先说明一下,半径和坐标轴之间的夹角(如ry和rx)取值范围是0到π,而投影和轴之间的夹角(如ryz和rxz)取值范围则是0到2π。只有这样才能保证取到空间中的所有点。

    之所以强调这个,是因为这在通过反余弦计算角度时非常重要!
    反余弦曲线是这样的:
    反余弦曲线

    其中正常x的取值只能在-1和1之间,而这样算出来的角度只能在0到π之间。而ryz的取值是在0到2π之间,这该怎么办呢?
    其实很简单,以下图为例:
    反余弦计算说明

    其中a、b分别是2个直线和x轴的夹角,但是一个大于π,一个小于π。通过反余弦可以正确算出b = arccos(x),但是a就需要通过 (2π - arccos(x))算出,可以通过判断y值是否小于0来决定是否需要这样处理。

    这样,我的代码就容易理解了:

    // 首先根据ry、rxz和radius算出x、y、z坐标,这点很容易,都是非常基本的三角运算:
    func getXYZ() -> (Float, Float, Float) {
        let pxz = sin(ry) * radius
        let px = sin(rxz) * pxz
        let pz = cos(rxz) * pxz
        
        return (px, cos(ry)*radius, pz)
    }
    // rx的计算也很简单,一个反余弦搞定!
    var rx:Float {
        get {
            let (px, _, _) = getXYZ()
            return acos(px/radius)
        }
    }
    // 通过我刚才介绍的方法正确计算、设置ryz和rxz
    var ryz:Float {
        get {
            let (_, py, pz) = getXYZ()
            let pyz = sqrt(py*py + pz*pz)
            let ryz = acos(pz/pyz)
            let PI = Float(M_PI)
            if py < 0 {
                return 2*PI - ryz
            } else {
                return ryz
            }
        }
        set {
            let pyz = sin(rx) * radius
            let py = sin(newValue) * pyz
            let pz = cos(newValue) * pyz
            let px = cos(rx) * radius
            let pxz = sqrt(px*px + pz*pz)
            
            ry = acos(py/radius)
            let PI = Float(M_PI)
    
            let new_rxz = acos(pz/pxz)
            if px < 0 {
                rxz = 2*PI - new_rxz
            }else {
                rxz = new_rxz
            }
        }
    }

    3d效果

    开始我只是设置了zPosition和m34,发现没有3d效果,需要同时设置一下transform,同时为了使得3d效果更加明显,我还把距离较远的title进行了半透明处理。

    // class LabelSphere: 
    let old_pz = self.layer.zPosition
    self.layer.zPosition = CGFloat(cos(rxz) * pxz)
    setTransform3D(self.layer.zPosition - old_pz)
    self.alpha = self.getAlpha()
    
    func setTransform3D(dz:CGFloat) {
        let t = self.layer.transform
        self.layer.transform = CATransform3DTranslate(t, 0, 0, dz)
    }
    func getAlpha() -> CGFloat {
        var alpha = 2*self.layer.zPosition/CGFloat(perspective) + 0.5
        alpha = min(1.0, alpha)
        alpha = max(0.1, alpha)
        return alpha
    }

    点击事件

    最简单的就是使用delegate了。但是这种方法总感觉比较麻烦,如果能像javascript那样直接赋值给一个闭包多好。

    所以我就把回调函数定义为了一个函数:

    public var onEachLabelClicked : ((label:UILabel)->Void)?

    最开始的时候,我是这样给它赋值的

    // 入口ViewController:
    class ViewController: UIViewController {
    
        var label3dView: Label3DView?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // 略过初始化操作 ......
            label3dView?.onEachLabelClicked = self.clickEachLabel
         }
    
        func clickEachLabel(label:UILabel) {
            let ac = UIAlertController(title: label.text!, message: self.labelDescription[label.text!],
                    preferredStyle: .Alert)
            ac.addAction(UIAlertAction(title: "确定", style: .Cancel, handler: {
                    [unowned self] _ in
                    self.dismissViewControllerAnimated(true, completion: nil)
                    }))
            self.presentViewController(ac, animated: true, completion: nil)
        }

    但是这样在label3dView中就会引用ViewController实例,同时ViewController的实例也会引用label3dView,形成循环引用,导致二者的内存都无法释放!

    为此我的解决方案是:

    // 重新定义func clickEachLabel:
    
    func clickEachLabel() -> ((l:UILabel)->Void){
        return {[unowned self] (label:UILabel) in
            let ac = UIAlertController(title: label.text!, message: self.labelDescription[label.text!],
                preferredStyle: .Alert)
            ac.addAction(UIAlertAction(title: "确定", style: .Cancel, handler: {
                [unowned self] _ in
                self.dismissViewControllerAnimated(true, completion: nil)
                }))
            self.presentViewController(ac, animated: true, completion: nil)
        }
    }

    然后在viewDidLoad中,这样给onEachLabelClicked赋值:

    label3dView?.onEachLabelClicked = self.clickEachLabel()

    这个思路来自于苹果官网论坛,感谢swiftgg的翻译(见Question2)

    回调函数的事情,说完了,下一步就是在哪里调用它了。开始我把touchesEnded:withEvent:函数定义在LabelSphere里面,因为我觉得这样可能简单点。随之发现的问题是:

    iOS的zPosition不会使得前面的元素覆盖下面的,即zPosition大的label,尽管视觉上它位于前面,但是它并没有覆盖后面的label,点击时后面的label可能会优先响应。因为我的label是通过父view的addSubView方法加入的,ios中后add进去的label会覆盖之前add的label,和zPosition的值无关!

    所以,我只好在Label3DView这个容器类中定义touchesEnded:withEvent:函数,然后根据点击位置算出最靠前的label,把它作为目标label,传入回调函数。这样才是我预期的功能。

    基本就这些了,代码都在github上,欢迎大家批评指正,共同学习!

    展开全文
  • 说到粒子效果就要说到核心动画Core Animation,因为粒子效果所用到的特殊图层是包含在核心动画框架中的。这个特殊图层就是CAEmitterLayer。 CAEmitterLayer是CALayer的一个常用子类,CALayer的子类有很多,...

    说到粒子效果就要说到核心动画Core Animation,因为粒子效果所用到的特殊图层是包含在核心动画框架中的。这个特殊图层就是CAEmitterLayer。

    CAEmitterLayer是CALayer的一个常用子类,CALayer的子类有很多,CAEmitterLayer就是其中之一,CAEmitterLayer是用于实现基于Core Animation的高性能粒子引擎系统。

    粒子系统使用到两个类CAEmitterLayer与CAEmitterCell,CAEmitterLayer是粒子发射源,用来发射粒子的,它所发射的粒子就是CAEmitterCell(当然粒子也可以发射粒子,也就是CAEmitterCell也可以发射CAEmitterCell)。可以认为CAEmitterLayer是CAEmitterCell的发射源,通过不同的参数设置就会不断的产生不同的粒子。

    我们首先来快速看一下这两个类的常用属性:

    CAEmitterLayer常用属性

    @property(nullable, copy) NSArray<CAEmitterCell *> *emitterCells; // 用来装粒子种类的数组, 通过给数组赋值,来支持多个cell
    @property float birthRate; //粒子产生系数(倍数),默认1.0
    @property float lifetime; // 粒子的生命周期系数, 默认1.0
    @property CGPoint emitterPosition; // 决定粒子发射位置的中心点
    @property CGFloat emitterZPosition; //三维立体中的位置
    @property CGSize emitterSize; // 发射源的尺寸大小
    @property CGFloat emitterDepth; // 发射源的深度
    @property(copy) NSString *emitterShape; // 发射源的形状,默认是点point,(还有line,rectangle,circle,cuboid,sphere).
    @property(copy) NSString *emitterMode; // 发射模式,默认是volume,(还有points,outline,surface).
    @property(copy) NSString *renderMode; // 渲染模式,默认是unordered,还有oldestFirst,oldestLast,backToFront,additive
    @property BOOL preservesDepth; //是否需要深度(一般使用这个属性)
    @property float velocity; // 粒子基本速度系数, 默认1.0
    @property float scale; // 粒子缩放比例系数, 默认1.0
    @property float spin; // 粒子自旋转速度系数, 默认1.0
    @property unsigned int seed; // 随机数发生器
    复制代码

    CAEmitterLayer里面的所有属性都在这里了,但是在实际情况中可能用不到这么多的属性,下面来重点介绍一些属性:

    1. CAEmitterLayer控制发射源发射位置和形状的属性

    • emitterPosition:决定发射源的中心点
    • emitterSize: 决定发射源的大小
    • emitterShape:表示粒子从什么形状发射出来,它并不是表示粒子自己的形状。

    emitterShape是一个枚举类型,提供如下类型可供选择:

    • kCAEmitterLayerPoint 点形状,发射源的形状就是一个点
    • kCAEmitterLayerLine 线形状,发射源的形状是一条线
    • kCAEmitterLayerRectangle 矩形状,发射源的形状是一个矩形
    • kCAEmitterLayerCuboid 立体矩形形状(3D),发射源是一个立体矩形,这里要生效的话需要设置z方向的数据,如果不设置就同矩形状.
    • kCAEmitterLayerCircle 圆形形状,发射源是一个圆形
    • kCAEmitterLayerSphere 立体圆形(3D),三维的圆形,同样需要设置z方向数据,不设置则如同二维的圆.
    • emitterMode:发射模式,它的作用其实就是进一步决定发射的区域是在发射形状的哪一部份。
    • kCAEmitterLayerPoints 点模式,发射器是以点的形式发射粒子。发射点就是形状的某个特殊的点,比如shap是一个点的话,那么这个点就是中心点,如果是圆形,那么就是圆心。
    • kCAEmitterLayerOutline 轮廓模式,从形状的边界上发射粒子。
    • kCAEmitterLayerSurface 表面模式,从形状的表面上发射粒子。
    • kCAEmitterLayerVolume 是相对于3D形状的物体内部发射.

    我们用kCAEmitterLayerLine来说明一下。当你的CAEmitterLayer的emitterSize为CGSize(10, 10)时,你的所选择的emitterPosition为CGPoint(10,10)。那么形状为“Line”的CAEmitterLayer就会在如下图紫色的直线上产生粒子,对于“Line”来说,emitterSize的高度是被忽略的。

    我们可以这样理解,emitterPosition是所选emitterShape的中心点,例如对于矩形是对角线交点,对于圆形是圆心,对于直线是中点。而emitterSize则决定了矩形的大小,圆形的大小,直线的长度。 如果我把CAEmitterCell的速度属性全部设为0,此时没有速度的粒子就不会动了,再把发射模式emitterMode设为kCAEmitterLayerOutline,我们来看一下这几种发射形状的效果图:
    现在我们利用同样的方法看下emitterMode的样式是什么样的,我们设置emitterShape发射形状为kCAEmitterLayerRectangle):

    • renderMode : 渲染模式,决定了粒子是以怎么一种形式进行渲染的。
    • kCAEmitterLayerUnordered 粒子是无序出现的
    • kCAEmitterLayerOldestFirst 声明时间长的粒子会被渲染在最上层
    • kCAEmitterLayerOldestLast 声明时间短的粒子会被渲染在最上层
    • kCAEmitterLayerBackToFront 粒子的渲染按照Z轴的前后顺序进行
    • kCAEmitterLayerAdditive 进行粒子混合

    2. CAEmitterLayer决定发射源粒子系数的属性

    • birthRate: 种类粒子产生系数,默认1.0;需要结合单个粒子cell的产生系数,每个粒子cell的产生系数乘以这个粒子产生系数,得出每秒具体产生粒子的个数。 即:每秒粒子产生个数 = layer.birthRate * cell.birthRate ;
    • lifetime:粒子的生命周期系数,默认1.0。
    • velocity:粒子速度系数, 默认1.0。
    • scale:粒子的缩放比例系数, 默认1.0。
    • spin:自旋转速度系数, 默认1.0。计算方式同上;

    3.CAEmitterLayer决定发射源粒子内容的属性

    emitterCells:用来装粒子的数组。每一种粒子就是一个CAEmitterCell。

    CAEmitterCell常用属性

    @property(nullable, copy) NSString *name; // 粒子名字, 默认为nil,有了名字才能找到对应的粒子
    @property(getter=isEnabled) BOOL enabled; 
    @property float birthRate; // 粒子的产生率,默认0
    @property float lifetime; // 粒子的生命周期,默认0,以秒为单位。
    @property float lifetimeRange; // 粒子的生命周期的范围,默认0,以秒为单位。
    @property CGFloat emissionLatitude;// 指定纬度,纬度角代表了在x-z轴平面坐标系中与x轴与z轴之间的夹角,默认0
    @property CGFloat emissionLongitude; // 指定经度,经度角代表了在x-y轴平面坐标系中与x轴与y轴之间的夹角,默认0
    @property CGFloat emissionRange; //发射角度范围,默认0
    @property CGFloat velocity; // 速度,默认是0
    @property CGFloat velocityRange; //速度范围,默认是0
    @property CGFloat xAcceleration; // 在x方向上的重力加速度分量,默认是0
    @property CGFloat yAcceleration;// 在y方向上的重力加速度分量,默认是0
    @property CGFloat zAcceleration;// 在z方向上的重力加速度分量,默认是0
    @property CGFloat scale; // 粒子在生命周期范围内的缩放比例, 默认是1
    @property CGFloat scaleRange; // 缩放比例范围,默认是0
    @property CGFloat scaleSpeed; // 在生命周期内的缩放速度,默认是0,负数缩小,正数放大;
    @property CGFloat spin; // 粒子的平均旋转速度,默认是0
    @property CGFloat spinRange; // 自旋转角度范围,弧度制,默认是0
    @property(nullable) CGColorRef color; // 粒子的颜色,默认白色
    @property float redRange; // 粒子的颜色red,green,blue,alpha能改变的范围,默认0
    @property float greenRange;
    @property float blueRange;
    @property float alphaRange;
    @property float redSpeed; // 粒子速度red,green,blue,alpha在生命周期内的改变的速度,默认都是0
    @property float greenSpeed;
    @property float blueSpeed;
    @property float alphaSpeed;
    @property(nullable, strong) id contents; // 粒子的内容,设置为CGImageRef的对象
    @property CGRect contentsRect;//粒子内容的位置
    @property CGFloat contentsScale; //粒子内容的缩放比例
    @property(copy) NSString *minificationFilter;//缩小的过滤器(基本不用)
    @property(copy) NSString *magnificationFilter;//放大的过滤器(基本不用)
    @property float minificationFilterBias;
    @property(nullable, copy) NSArray<CAEmitterCell *> *emitterCells; // 粒子里面的粒子
    @property(nullable, copy) NSDictionary *style;
    复制代码

    接下来重点说说CAEmitterCell一些常用的属性:

    1.CAEmitterCell决定粒子生命周期的属性

    • lifetime、lifetimeRange:粒子在系统上的生命周期,即存活时间,单位是秒。配合lifetimeRage来让粒子生命周期均匀变化,以便可以让粒子的出现和消失显得更加离散。
    • birthRate:粒子产生数量的决定参数,它表示CAEmitterLayer上每秒产生的粒子数量,是浮点数。对于这个数量为浮点数,在测试的时候可以灵活使用它。比如你想看粒子的运动状态,但是太多了可能会很迷糊,这时候你把birthRate 设置成0.1f,把lifetime设置时间较长,就能看到单个粒子的运动状态。

    2.CAEmitterCell决定粒子内容的属性

    • contents:为CGImageRef的对象。关于contents会联想到CALayer了,在CALayer中展示静态的图片是需要用到这个属性。在CAEmitterCell上它的意义也是一样的。但是因为粒子系统可以给粒子上色,为了做出好的颜色变换效果,通常提供的图片为白色的纯色的图片,

    • name:粒子的名字,当CAEmitterLayer里面有很多个cell的时候,可以给每个cell设置好名字,要修改一些属性以达到动画效果的改变等,就可以通过KVC拿到这个cell的某个属性。

    3.CAEmitterCell决定粒子颜色状态的属性

    • color:color是粒子的颜色属性,这个颜色属性的作用是给粒子上色,color 会结合contents内容的颜色来改变我们的CAEmitterCell,它的实现方法其实很简单,就是将contents自身的颜色的RGBA值 * color的RGBA值,就得到最终的粒子的颜色。我们想完全通过color来控制CAEmitterCell的颜色,那么最好就选用一张颜色值为(255,255,255),即白色的图片作为CAEmitterCell的contents。因为大家都知道在UIColor中,rgb值为255的component的值其实为1,因为白色的UIColor每个component都为1,CAEmitterCell的color中任意component乘以contents中颜色对应的component都会得到color上原来的值。
    • redRange、greenRange、blueRange、alphaRange:这些是对应的color的RGBA的取值范围,默认为0,取值范围为0~1,如果设置粒子的颜色为[[UIColor colorWithRed:0.3 green:0.5 blue:0.3 alpha:1.0]CGColor];再设置 redRange、greenRange、blueRange、alphaRange全部为0.1,那么也就是说在产生粒子时,粒子的颜色RGBA对应的取值范围是:R(0.1, 0.3),G(0.1, 0.5),B(0.1, 0.3),A(0.1, 1.0).
    • redSpeed、greenSpeed、blueSpeed、alphaSpeed:这些是对应的是粒子的RGBA的变化速度,默认为0,取值范围为0~1。表示每秒钟的RGBA的变化率,如果设置粒子颜色的RGBA,以及redSpeed,其他的属性没设置默认为0。粒子的生命周期(lifetime)为20秒,那么这个粒子从产生到消失的时间就是20秒。它的Red值为0,redSpeed为0.2,那么在粒子的这个生命周期内,粒子的每秒钟的Rde值就会增加0.2 * 255,表现在外观上的状态就是粒子颜色在不断变化,接近白色。最后粒子生命周期结束的时候,粒子的color正好是RGBA(1,1,1,1)。当然个变化的速度也可以负数,计算方式相同。

    4.CAEmitterCell决定粒子运行轨迹的属性。

    • emissionLongitude:表示粒子飞行方向跟水平坐标轴(x轴)之间的夹角,默认是0,顺时针方向是正向。例如emissionLongtitude为0 ,则粒子顺着x轴飞行。如果想沿着Y轴向下飞行,那么可以设置emissionLongtitude = M_PI_2。

    • emissionLatitude:这个和emissionLongitude的原理是一样的,只不过是在三维平面上的x-z轴上与x轴的夹角。

    • emissionRange则决定了粒子的发射角度范围,同样是一个弧度值。表示粒子在沿着emissionLongtitude方向所形成的顶角为2倍emissionRange的扇形范围内发散。我们把emisstionLongtitude设置为M_PI_2,让粒子向下飞行,并且让emissionRange为M_PI_4,就会得到一个顶角为2 * PI/4的扇形区域,那么粒子就会在这个区域内发射

    • velocity、velocityRange、xAcceleration 、yAcceleration、zAcceleration:前面两个是粒子的初速度和初速度的范围,后面是三个分别是在x、y、z轴方向的加速度,当xAcceleration为正数时,粒子每秒向x轴正方向加速,为负数时则向负方向即水平向左加速。当yAcceleration为正数时,粒子向y轴的负方向加速,也是就是向下加速,否则向上加速。

    • spin,spinRange:是粒子的自转属性,表示每秒钟粒子自转的弧度数。粒子的自转是以弧度制来计算的,表示每秒钟粒子自转的弧度数。spin为正数代表粒子是顺时针旋转的,为负数的话就是逆时针旋转。如果粒子的生命周期就是10秒,那么你想让你的粒子这个生命周期内刚好自转1周,若spinRange为0,那么粒子的spin值就应该为((π/180)*360 )/10,就得到了每秒需要转动的弧度数。

    5.CAEmitterCell子粒子的属性

    emitterCells:CAEmitterCell的emitterCells跟CAEmitterLayer的一样,也是一个CAEmitterCell的数组。我们基本可以按照操作CAEmitterLayer的emitterCells一样来设置我们粒子的emitterCells。

    不过在这里需要注意的是:

    1. CAEmitterCell是服从CAMediatiming协议的,我们通过控制子CAEmitterCell的beginTime来控制子CAEmitterCell的出现时机。当子CAEmitterCell的beginTime为0时,表示你的粒子从CAEmitterLayer上发射出来后就会立即开始发射子CAEmitterCell,而且子CAEmitterCell的beginTime是不能大于你的粒子的lifetime的。
    2. 无论粒子是从什么样的形状上发射出来的,当它要发射子CAEmitterCell的时候,子CAEmitterCell总是从kCAEmitterLayerPoint形状上由父粒子的中心发射出来的。
        colorBallCell.birthRate = 20.f;
        colorBallCell.lifetime = 10.f;
        colorBallCell.velocity = 40.f;
        colorBallCell.velocityRange = 100.f;
        colorBallCell.yAcceleration = 15.f;
        colorBallCell.emissionLongitude = M_PI_2; 
        colorBallCell.emissionRange = M_PI_4l;
        colorBallCell.scale = 0.2;
    
    
        CAEmitterCell*subCell=[CAEmitterCell emitterCell];
        subCell.lifetime=5;
        subCell.birthRate=3;
        subCell.contents=(id)[UIImage imageNamed:@"circle_white"].CGImage;
        subCell.velocity=60;
        subCell.emissionLongitude= - M_PI_2;
        subCell.emissionRange=M_PI_4/4;
        subCell.beginTime=5;
        subCell.scale=0.5;
        colorBallCell.emitterCells=@[subCell];
    
        colorBallLayer.emitterCells = @[colorBallCell];
    复制代码

    父粒子的发射方向是子粒子的emissionLongtitude为0时的飞行方向。

    上述内容已将CAEmitterLayer和CAEmitterCell的属性基本介绍完毕,后续会再做补充,下面我们来看一个具体的案例,把这些属性全部串联起来。

    案例

    1.设置CAEmitterLayer

    设置CAEmitterLayer以及它的一些模式,并添加到要显示的view的图层上,当然也可以替换view的图层。这里是直接添加到控制器的view的layer上的,这个view的layer是一个calyer类型的,如果想替换掉calyer,可以自定义view,并在view里面重写如下方法即可实现替换

    // 替换view的layer
    + (Class)layerClass{return [CAEmitterLayer class];}
    复制代码
    #import "ViewController.h"
    
    @interface ViewController ()
    @property (nonatomic, strong) CAEmitterLayer * colorBallLayer;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        self.view.backgroundColor = [UIColor blackColor];
        [self setupEmitter];
        
    }
    - (void)setupEmitter{
        
        UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(0, 100, self.view.bounds.size.width, 50)];
        [self.view addSubview:label];
        label.textColor = [UIColor whiteColor];
        label.text = @"轻点或拖动来改变发射源位置";
        label.textAlignment = NSTextAlignmentCenter;
        
        // 1. 设置CAEmitterLayer
        CAEmitterLayer * colorBallLayer = [CAEmitterLayer layer];
        [self.view.layer addSublayer:colorBallLayer];
        self.colorBallLayer = colorBallLayer;
        
        //发射源的尺寸大小
        colorBallLayer.emitterSize = CGSizeMake(100, 10);
        //发射源的形状
        colorBallLayer.emitterShape = kCAEmitterLayerPoint;
        //发射模式
        colorBallLayer.emitterMode = kCAEmitterLayerPoints;
        //粒子发射形状的中心点
        colorBallLayer.emitterPosition = CGPointMake(self.view.layer.bounds.size.width/2, 20);
        
        // 2. 配置CAEmitterCell
        CAEmitterCell * colorBallCell = [CAEmitterCell emitterCell];
        //粒子名称
        colorBallCell.name = @"colorBallCell";
        //粒子产生率,默认为0
        colorBallCell.birthRate = 20.f;
        //粒子生命周期
        colorBallCell.lifetime = 10.f;
        //粒子速度,默认为0
        colorBallCell.velocity = 40.f;
        //粒子速度平均量
        colorBallCell.velocityRange = 100.f;
        //x,y,z方向上的加速度分量,三者默认都是0
        colorBallCell.yAcceleration = 15.f;
        //指定纬度,纬度角代表了在x-z轴平面坐标系中与x轴之间的夹角,默认0:
        colorBallCell.emissionLongitude = M_PI_2; // 向左
        //发射角度范围,默认0,以锥形分布开的发射角度。角度用弧度制。粒子均匀分布在这个锥形范围内;
        colorBallCell.emissionRange = M_PI_4; // 围绕X轴向左90度
        // 缩放比例, 默认是1
        colorBallCell.scale = 0.2;
        // 缩放比例范围,默认是0
        colorBallCell.scaleRange = 0.1;
        
        // 在生命周期内的缩放速度,默认是0
        colorBallCell.scaleSpeed = 0.02;
        // 粒子的内容,为CGImageRef的对象
        colorBallCell.contents = (id)[[UIImage imageNamed:@"circle_white"] CGImage];
        //颜色
        colorBallCell.color = [[UIColor colorWithRed:0.5 green:0.f blue:0.5 alpha:1.f] CGColor];
        // 粒子颜色red,green,blue,alpha能改变的范围,默认0
        colorBallCell.redRange = 1.f;
        colorBallCell.greenRange = 1.f;
        colorBallCell.alphaRange = 0.8;
        // 粒子颜色red,green,blue,alpha在生命周期内的改变速度,默认都是0
        colorBallCell.blueSpeed = 1.f;
        colorBallCell.alphaSpeed = -0.1f;
        
        
        
        CAEmitterCell*subCell=[CAEmitterCell emitterCell];
        subCell.lifetime=5;
        subCell.birthRate=3;
        subCell.contents=(id)[UIImage imageNamed:@"circle_white"].CGImage;
        subCell.velocity=60;
        subCell.emissionLongitude= - M_PI_2;
        subCell.emissionRange=M_PI_4/4;
        subCell.beginTime=5;
        subCell.scale=0.5;
        colorBallCell.emitterCells=@[subCell];
        // 添加
        colorBallLayer.emitterCells = @[colorBallCell];
    }
    复制代码

    到此CAEmitterLayer和CAEmitterCell的属性配置都已完成,下面我们让发射源根据手指的点击位置进行改变:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        CGPoint point = [self locationFromTouchEvent:event];
        [self setBallInPsition:point];
    }
    
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        CGPoint point = [self locationFromTouchEvent:event];
        [self setBallInPsition:point];
    }
    
    /**
     * 获取手指所在点
     */
    - (CGPoint)locationFromTouchEvent:(UIEvent *)event{
        UITouch * touch = [[event allTouches] anyObject];
        return [touch locationInView:self.view];
    }
    复制代码

    下面我们用一个动画来实现移动发射源到手指点击的点上:

    - (void)setBallInPsition:(CGPoint)position{
        
        //创建基础动画
        CABasicAnimation * anim = [CABasicAnimation animationWithKeyPath:@"emitterCells.colorBallCell.scale"];
        //fromValue
        anim.fromValue = @0.2f;
        //toValue
        anim.toValue = @0.5f;
        //duration
        anim.duration = 1.f;
        //线性起搏,使动画在其持续时间内均匀地发生
        anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
        
        // 用事务包装隐式动画
        [CATransaction begin];
        //设置是否禁止由于该事务组内的属性更改而触发的操作。
        [CATransaction setDisableActions:YES];
        //为colorBallLayer 添加动画
        [self.colorBallLayer addAnimation:anim forKey:nil];
        //为colorBallLayer 指定位置添加动画效果
        [self.colorBallLayer setValue:[NSValue valueWithCGPoint:position] forKeyPath:@"emitterPosition"];
        //提交动画
        [CATransaction commit];
    }
    复制代码

    下面我们来看下实现的效果:

    至此,一个简单的粒子效果的代码就写完成了,其实大部分内容都是在配置CAEmitterLayer与CAEmitterCell的属性,因为这两个类是苹果为开发者封装好的,可以直接使用,已经省去了大部分的时间,我们只需要调用属性即可。在以后的时间中,或许可以写一个OpenGL ES 的GLSL语言来实现粒子效果。

    转载于:https://juejin.im/post/5d0bab916fb9a07ed911c8d0

    展开全文
  • 本期内容: CAEmitterLayer 粒子发射器(层) CAEmitterCell 粒子 下雨效果 ... 是CALayer的一个常用子类,CALayer的子类有很多,如果能很好的使用它们会得到一些意想不到的效果。CAEmitterL...
  • ios arkit原理理解

    2018-03-14 15:12:38
    arkit的效果案例 arkit的运用场景 arkit现有的产品 arkit的局限性 随着技术的进步,arkit的局限性会越来越小 1)只支持A9处理器;iphone6s以上的设备,ios11以上 2)只支持水平平面检测,不支持竖直平面...
  • 平面 什么是引擎?经包装的函数库,方便开发者使用。也就是说苹果帮我们封装了一套绘图的函数库 同时支持iOS和Mac系统什么意思?用Quartz2D写的同一份代码,既可以运行在iphone上又可以运行在mac上,可以跨平台开发。 ...
  •  最近项目中需要一个落叶的效果,本来想用粒子特效来实现,但是几经调试,虽然调出了落叶的效果,但是并不是十分理想,最大的不足就是落叶是平面的,没有立体感,虽然把落叶做小之后却是立体感的感觉会有所缓解,但...
  • iOS动画

    2017-03-21 14:00:33
    在2013年六月,苹果推出了iOS 7,并与iOS 6大相径庭,让设计师回归本初。曾经代表漂亮iOS设计的现实主义拟物化离去了,而一个更加平面、光滑,更加“计算机真实”的美学到来了。这种向平面设计专项的一个重大影响...
  • 想必以前QQ空间的点赞效果大家都知道吧...这些看似很炫酷的动画可能让我们敬而远之,但是其实iOS封装的很好,利用简单的几行代码就能完成很炫酷的动画效果。由于目前正在玩儿iOS动画的内容,利用iOS的CAEmitter...
  • IOS Layer 简析

    2016-11-03 09:20:28
     * 在iOS系统中,你能看得见摸得着的东西基本上都是UIView,比如一个按钮、一个文本标签、一个文本输入框、一个图标等等,这些都是UIView。  * 其实UIView之所以能显示在屏幕上,完全是因为它内部的一个层。  ...
  • 作者黄志标:中国科学院大学硕士,京东AI与大数据部算法工程师。擅长图像检索、深度学习领域。曾参与京东的上亿重图检测项目,目前主要负责京东全景主图、视频审核项目。安山:山东大学机器人研究中心硕士,京东AI与...
  • 2016又被称为VR元年,借此势头VR获得了巨大的发展,因此作为IT人员有必要对VR有更多的学习和了解,而全景图是VR技术中发展较火的一个技术,许多全景公司做的很好,如Insta360,Wipet、理光、暴风魔眼等公司。...
  • 纯CSS3实现<!DOCTYPE html> <html> <meta charset="utf-8" /> <title></title> a { display: block; float: left;
  • iOS 专用图层

    2019-05-22 22:23:34
    如果你依然在编程的世界里迷茫,不知道自己的未来规划,小编给大家推荐一个IOS高级交流群:458839238 里面可以与大神一起交流并走出迷茫。小白可进群免费领取学习资料,看看前辈们是如何在编程的世界里傲然前行! 群...
  • Core Location是iOS SDK中一个提供设备位置的框架。可以使用三种技术来获取位置:GPS、蜂窝或WiFi。在这些技术中,GPS最为精准,如果有GPS硬件,Core Location将优先使用它。如果设备没有GPS硬件(如WiFi iPad)或使用...
  • IOS常用第三方类库

    2016-05-05 17:55:02
    1.AFNetworking  访问服务器 2.FMDB 操作数据库 3.IQKeyboardManager UITextField、UITextView 被键盘挡住 4.MBProgressHUD 过渡特效 5.Masonry 屏幕适配 6.SDWebImage ...7.UITableView+FDTemplateLayoutCell ...
  • 贝塞尔曲线 帧动画 //关键帧动画 -(void)layerKeyFrameAnimation { //画一个path UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(-40, 100)];... [path addLineToPoint:CGPoint
1 2 3 4 5 ... 20
收藏数 431
精华内容 172
关键字:

360度平面旋转效果 ios