本专栏将承接上一篇文章,进一步教大家如何将一个Windows的动态光标转化为MacOS可用的动态光标。
拥有一台运行MacOS的物理机/虚拟机
Python脚本运行能力
首先,MacOS不像Windows一样,拥有原生的鼠标方案变更机制,MacOS原生设置并不支持鼠标方案的自定义。

MacOS 原生对于指针的设置
那么我们此时可以借助第三方程序来对指针进行定制,此处我们使用开源软件Mousecape进行指针定制。
Github链接:https://github.com/alexzielenski/Mousecape
注:Mousecape支持MacOS 10.8+以上版本,截至发稿时,作者的MacOS 13.0.1 (22A400)运行良好。
那么我们下载Mousecape之后打开。

Mousecape 图标
首先为了保证指针的稳定性,我们需要首先安装Helper程序,首次打开,此处应当显示Install Helper Tool,我这边已经安装了,因此显示的是卸载。

Install Helper Tool
那么,我们就可以着手制作指针了,制作前,我们可以先备份一份系统的原生方案。

备份原生指针方案
备份好,我们点击新建一个指针方案

新建指针方案
我们在新建的方案上右击,选择Edit

编辑指针方案
点击后会弹出一个指针方案基本信息的框框

指针方案基本信息
各个字段含义如下:
Name:指针方案的名字,会显示在指针选择界面。
Author:指针方案的作者名,会显示在指针选择界面。
Identifier:指针方案的唯一ID,自动生成,不建议修改。
Version:指针方案的版本。
Retina:勾选会在指针界面显示"SD",否则显示"HD"。
接下来我们点击左下角的➕加号,创建一个新的指针。

创建新指针
指针设置如下所示

指针设置
各个字段含义如下:
Type:指针的类型,即在什么情形下会显示此指针。
Frames:此指针有几帧,动态指针必须设置,静态指针请保持为1。
Frame Duration:帧切换速率,单位为秒,动态指针必须设置,静态指针请保持为1。
Hot Spot:指针焦点位置,指,对于双箭头、四箭头类的指针必须设置,绝大多数指针保持{0,0}即可,单位为像素。
Size:指针图像大小,此项显示的大小即为实际显示的大小,单位为像素。
底下的四个框框是用于放置图像的,对于静态的指针,可以直接将JPG/PNG/ICO拖入框框中,图像拖入框框会在Size显示图像大小,四个框框区别如下:
图像拖入1x的框框:图像大小即为Size内显示的大小。
图像拖入2x的框框:图像自动缩小2倍,Size内显示的大小为实际图像大小的1/2。
图像拖入5x的框框:图像自动缩小2倍,Size内显示的大小为实际图像大小的1/5。
图像拖入10x的框框:图像自动缩小2倍,Size内显示的大小为实际图像大小的1/10。
无论拖入哪个框框,都可以通过手动输入Size的值进行图像压缩(并非裁剪),一般来说{24,24}、{36,36}、{48,48}的比较合适。

拖入图片的效果
不想要的图片直接拖出这个框即可。
关于焦点的设置,这是{0,0}焦点的效果

{0,0}焦点
这是1/2 Size的值作为焦点的效果,例如size为{36,36},焦点值为{18,18}

{18.18}焦点
那么接下来,如何让图片动起来呢,首先,我们需要引入SpriteSheet的概念。
简单来说,SpriteSheet是指动画的切片。例如:

一个常见的SpriteSheet
那么,想象一下,我们使用一个纸条盖在这个图片上,挖一个和一帧大小一致的小孔,从左到右快速滑过,会发生什么?没错,它会动起来!

动起来的SpriteSheet
Mousecape加载动画需要纵向的SpriteSheet,我们可以通过Python脚本实现,此处我们给出两种脚本,第一种是加载GIF生成SpriteSheet,第二种是从ANI生成SpriteSheet。因为从ANI生成SpriteSheet如果要经过GIF,它的过程是ANI → Pillow Image → GIF → Pillow Image → SpriteSheet。为了避免可能的图像质量损失,我们选择直接生成。
加载GIF生成SpriteSheet:
from PIL import Image
import io,os,sys
import logging
logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',level=logging.INFO)
if __name__ == '__main__':
if len(sys.argv) < 2:
logging.fatal("Usage:python gif2spritesheet.py <inputFile> <outputFile,Option>")
else:
OUTPUT_SIZE = (48,48)
gif = Image.open(f"{sys.argv[1]}")
output = Image.new("RGBA", (OUTPUT_SIZE[0], OUTPUT_SIZE[1] * gif.n_frames))
for frame in range(0,gif.n_frames):
gif.seek(frame)
extracted_frame = gif.resize(OUTPUT_SIZE)
position = (0, OUTPUT_SIZE[0] * frame)
output.paste(extracted_frame, position)
if(len(sys.argv) >= 3):
output.save(sys.argv[2],format="PNG")
else:
output.save(f"{sys.argv[1].strip('.gif')}.png",format="PNG")
logging.info("转换完成!")
从ANI生成SpriteSheet:
from PIL import Image
import io,os,sys
import logging
logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',level=logging.INFO)
def analyzeANIFile(filePath):
with open(filePath,'rb') as f:
if f.read(4) != b'RIFF':
return {"code":-1,"msg":"File is not a ANI File!"}
logging.debug('文件头检查完成!')
fileSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
# if os.path.getsize(filePath) != fileSize:
# return {"code":-2,"msg":"File is damaged!"}
logging.debug('文件长度检查完成!')
if f.read(4) != b'ACON':
return {"code":-1,"msg":"File is not a ANI File!"}
logging.debug('魔数检查完成!')
frameRate = (1/60)*1000
while(True):
chunkName = f.read(4)
if chunkName == b'LIST':
break
chunkSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
if chunkName.lower() == b'rate':
logging.debug('发现自定义速率!')
frameRate = frameRate * int.from_bytes(f.read(4), byteorder='little', signed=False)
logging.warning('发现自定义速率!由于GIF限制,将取第一帧与第二帧的速率作为整体速率!')
f.read(chunkSize - 4)
else:
logging.debug('发现自定义Chunk!')
f.read(chunkSize)
listChunkSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
if f.read(4) != b'fram':
return {"code":-3,"msg":"File not a ANI File!(No Frames)"}
logging.debug('frame头检查完成!')
frameList = []
nowSize = 4
while(nowSize < listChunkSize):
if f.read(4) != b'icon':
return {"code":-4,"msg":"File not a ANI File!(Other Kind Frames)"}
nowSize += 4
subChunkSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
nowSize += 4
frameList.append(f.read(subChunkSize))
nowSize += subChunkSize
return {"code":0,"msg":frameList,"frameRate":frameRate}
if __name__ == '__main__':
OUTPUT_SIZE = (48,48)
if len(sys.argv) < 2:
logging.fatal("Usage:python ani2spritesheet.py <inputFile> <outputFile,Option>")
else:
res = analyzeANIFile(sys.argv[1])
GIFframes = []
if res["code"] == 0:
logging.info('ANI文件分析完成,帧提取完成!')
output = Image.new("RGBA", (OUTPUT_SIZE[0], OUTPUT_SIZE[1] * len(res["msg"])))
for frameIndex in range(len(res["msg"])):
frameImage = Image.open(io.BytesIO(res["msg"][frameIndex]),formats=['cur']).convert('RGBA')
extracted_frame = frameImage.resize(OUTPUT_SIZE)
position = (0, OUTPUT_SIZE[0] * frameIndex)
output.paste(extracted_frame, position)
if(len(sys.argv) >= 3):
output.save(sys.argv[2],format="PNG")
else:
output.save(f"{sys.argv[1].strip('.ani')}.png",format="PNG")
logging.info('SpriteSheet生成完成!')
else:
logging.fatal(res["msg"]) 为了避免可能的版权争议,此处不再附转换成功的例子。如果担心Mousecape的图像压缩可能带来质量损失,可以修改脚本中的OUTPUT_SIZE,有Pillow进行图像尺寸更改。
终于到了最终阶段了,回到Mousecape,将我们转换的图像拖入指针的图像框。

拖入图片
图像帧数可以手动数也可以通过公式计算,此处816 / 48 = 17,因此此处Frames填入17,Frame Duration处填入合适的动画速率,一般为.1较为合适,注意此处单位为秒,因此填写.1的含义就是每隔100ms切换一帧。务必不要忘了修改Size,我们可以看到指针动起来了,选择Type为Arrow,可以看到我们的动态指针已经设置完成。

指针设置
直接Ctrl + S保存,或者点叉,随后在弹出的提示框点击保存。

保存提示
最后,双击我们的指针方案,发现已经设置成功了!

使之生效
右击我们的指针方案点击Show in Finder

分享指针
可以发现由指针方案的唯一ID命名的文件,我们只需要把这个文件分享给朋友

文件位置
朋友直接操作导入,就可以使用由你制作的指针方案啦~

指针文件导入
首先明确,MacOS的指针类型比Windows的多很多,因此将Windows指针向MacOS指针迁移时,有的图标可以考虑复用。还记得我们一开始备份的系统默认图标吗,我们对它使用Edit。

系统原生指针方案
当我们不知道Type到底是什么含义时,我们可以通过查看原生方案中使用的图标进行猜测。
那么如果我们没有Mac电脑,应该如何制作一个可被Mousecape导入的指针方案呢?我将在下一篇专栏进行更新。