摘要:中,回调函数可以当做变量传入,跟类似。当确定了目标之后,把该作为回调函数的参数传入,执行回调函数。
什么是3d文字球
我也不知道专业的名字叫什么,总之效果如下:
如何创建坐标,如何根据坐标方便的控制球绕着x和y轴旋转
如何实现3d效果
如何使得各个标题在球上均匀分布
如何实现点击标题后的回调函数
我的解决方案 关于坐标肯定是使用球坐标最方便啦,这里需要再温习一下球坐标的表示方法:
如图,一个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上,欢迎大家批评指正,共同学习!
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/16002.html
摘要:看看这些惊人的纯实验,也许你自己也可以尝试一下。项目链接叠叠高游戏你可以不用来编写一个游戏。这个纯粹用实现的叠叠高游戏看上去很简单,但是很有趣,而且图形也很漂亮。项目链接鬼影渐变效果按钮令人惊讶的是它是只用编写的。 翻译:疯狂的技术宅原文:https://1stwebdesigner.com/12... 本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜...
摘要:看看这些惊人的纯实验,也许你自己也可以尝试一下。项目链接叠叠高游戏你可以不用来编写一个游戏。这个纯粹用实现的叠叠高游戏看上去很简单,但是很有趣,而且图形也很漂亮。项目链接鬼影渐变效果按钮令人惊讶的是它是只用编写的。 翻译:疯狂的技术宅原文:https://1stwebdesigner.com/12... 本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜...
摘要:看看这些惊人的纯实验,也许你自己也可以尝试一下。项目链接叠叠高游戏你可以不用来编写一个游戏。这个纯粹用实现的叠叠高游戏看上去很简单,但是很有趣,而且图形也很漂亮。项目链接鬼影渐变效果按钮令人惊讶的是它是只用编写的。 翻译:疯狂的技术宅原文:https://1stwebdesigner.com/12... 本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜...
摘要:在文末,我会附上一个可加载的模型方便学习中文艺术字渲染用原生可以很容易地绘制文字,但是原生提供的文字效果美化功能十分有限。 showImg(https://segmentfault.com/img/bVWYnb?w=900&h=385); WebGL 可以说是 HTML5 技术生态链中最为令人振奋的标准之一,它把 Web 带入了 3D 的时代。 初识 WebGL 先通过几个使用 Web...
摘要:按下右侧的点击预览按钮可以在当前页面预览,点击链接可以打开原始页面。 按下右侧的点击预览按钮可以在当前页面预览,点击链接可以打开原始页面。 1. 一个正 20 面体的骰子https://codepen.io/chrisvfrit... 2. 纯 css 写的夜间景色的视差滚动效果https://codepen.io/danbhala/p... 3. 机器人喝油的动画https://co...
阅读 2733·2021-11-25 09:43
阅读 3042·2021-11-24 09:39
阅读 2631·2021-09-22 15:59
阅读 1706·2021-09-13 10:24
阅读 378·2019-08-29 17:02
阅读 1989·2019-08-29 13:23
阅读 2944·2019-08-29 13:06
阅读 3415·2019-08-29 13:04