什么是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 = 2000public var fontSize:CGFloat = 15public var fontColor:UIColor = UIColor.blackColor()public var sphereRadius:Float = 0.5public 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:Floatvar cy:Float // 下面的属性名字基本上和以上介绍球坐标系时一样。// 半径在xz平面上的投影和z轴之间的夹角var rxz:Float// label和z轴夹角var ry:Floatvar 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) * radiuslet px = sin(rxz) * pxzlet pz = cos(rxz) * pxzreturn (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) * radiuslet py = sin(newValue) * pyzlet pz = cos(newValue) * pyzlet px = cos(rx) * radiuslet 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.transformself.layer.transform = CATransform3DTranslate(t, 0, 0, dz)
}
func getAlpha() -> CGFloat {var alpha = 2*self.layer.zPosition/CGFloat(perspective) + 0.5alpha = 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] _ inself.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) inlet ac = UIAlertController(title: label.text!, message: self.labelDescription[label.text!],preferredStyle: .Alert)ac.addAction(UIAlertAction(title: "确定", style: .Cancel, handler: {[unowned self] _ inself.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上,欢迎大家批评指正,共同学习!

在ios中制作3d文字球效果相关推荐

  1. html怎么制作3D字体,用CSS3制作3D文字效果代码实例教程

    这个简单的CSS文本阴影教程将一步步教你如何通过堆叠多层阴影来创建3D文字,然后进一步利用CSS3的transform和transition属性来实现鼠标移过字体放大的效果. 阿里西西web开发网为大 ...

  2. android led闪烁功能,如何在Android应用层中制作一个LED指示灯效果

    如何在Android应用层中制作一个LED指示灯效果 发布时间:2020-12-08 16:12:59 来源:亿速云 阅读:86 作者:Leah 本篇文章给大家分享的是有关如何在Android应用层中 ...

  3. html+css制作3D透明立体效果

    今天给大家分享一下如何制作3D透明立体 首先还是说一下原理: 在一个大盒子中包含六个小盒子,每个盒子中放置一张图片,通过我们使用rotate和translate属性,让每一张图片旋转一定的角度,从而使 ...

  4. Three.js中的3D文字效果

    对于一些设计网页中经常会出现一些3D的文字效果,本文将利用Three.js实现各种动画WebGL文本输入效果. 示例效果 原文章 文本采样 通常情况下,文本网格是2D的平面形状,我们所要实现的3D文本 ...

  5. 5月8日——iOS中的3D Touch效果

    需要在manifest.json文件中进行配置 需要执行的js代码为: 最终操作效果为 本篇文章主要采用了HTML5+  中的 launcher属性 具体可参照 http://www.html5plu ...

  6. iOS中制作一张水印图片

    如果这篇文章帮助到了您,希望您能点击一下喜欢或者评论,你们的支持是我前进的强大动力.谢谢! 我们在很多APP中都会看到水印图片,例如下面微博中的一张图片 下面就来分享一下怎么制作一张水印图片吧 首先生 ...

  7. 【3D游戏模型】在ZBrush中制作3D兽人

    介绍 在本教程中,您将学习如何基于概念艺术创建风格化角色.从雕刻 (ZBrush) 到纹理(sp),毛发(Xgen),照明/材质/渲染(Maya+ arnold). 创建您自己的风格化角色 从 ZBr ...

  8. 谈谈iOS中粘性动画以及果冻效果的实现

    文 / 杨骑滔 在最近做个一个自定义PageControl--KYAnimatedPageControl中,我实现了CALayer的形变动画以及CALayer的弹性动画,效果先过目: 先做个提纲: 第 ...

  9. 游戏的角色模型是如何创建的?ZBrush中制作3D兽人,全流程解析

    一个游戏角色模型是如何创建的?想学习游戏建模的朋友可以看看,可以大概了解如何基于概念艺术创建风格化角色.从雕刻 (ZBrush) 到纹理(sp),毛发(Xgen),照明/材质/渲染(Maya+ arn ...

  10. 在PPT2007中制作礼花绽放动画效果ppt模板打包下载

    家家户户都有除夕放烟花的习惯,尤其是一些小地方城市,过节礼花那是必不可少的. 毕业论文ppt背景放礼花可以增添节日的喜庆气氛,但是大家也需要注意自身安全,下面简单介绍一下礼花绽放动画效果的制作. 1. ...

最新文章

  1. “80后”财富新贵创业秘诀
  2. 如何获得images.xcassets 中图片的路径?
  3. CodeForces - 246E Blood Cousins Return(树上启发式合并)
  4. P2626 斐波那契数列(升级版)
  5. Linux 内存管理 | 物理内存、内存碎片、伙伴系统、SLAB分配器
  6. jQuery如何创建元素
  7. golang读取文件编码转换问题
  8. 第一次使用博客及Coursera课程体验
  9. EF Code First 学习笔记:表映射
  10. 计算机ip地址查询精确的位置,本机ip地址查询精确的位置 简单两步轻松搞定
  11. Vim插件(三) Terminal 终端
  12. Python Matplotlib add_subplot 和 subplots_adjust详解及代码详细说明 配图片说明
  13. [go]根据背景色计算文本颜色
  14. MMM配置文件及相关命令
  15. android打包工具多渠道批量打包,android 二次打包完成apk多渠道打包的方法
  16. 微博图片地址查uid网页版源码
  17. Frida出现process with pid XXXX either refused to load frida-agent, or terminated during injection错误的原因
  18. 编译出现 unused parameter [-Werror,-Wunused-parameter]
  19. 12个View绘制流程高频面试题,震撼来袭免费下载!
  20. 调整图像亮度之 线性拉伸 (2) 百分比截断拉伸

热门文章

  1. javaweb java代码写在哪里_写了那么多年 Java 代码,终于 debug 到 JVM 了
  2. mysql时间设计模式_java 23种设计模式及具体例子 收藏有时间慢慢看
  3. js添加option设置空值_3.11 在散点图中添加标签(2)
  4. lamda表达式修改数据_关系数据库SQL语言简介
  5. linux应用--yum
  6. win7 mac虚拟机linux,Mac虚拟机parallels desktop超详细安装Win7图文分解
  7. openmp 第一次运行时间比较长_Android App 启动时间优化
  8. 错误未找到引用源_你好,C++(77)12.1 用右值引用榨干C++的性能
  9. 每天二十分钟学习python_每天 3 分钟,小闫带你学 Python(二十五)
  10. mysql实现主从复制的方式_mysql实现主从复制、读写分离的配置方法(二)