从Windows动态指针到MacOS动态指针—— 在MacOS上制作指针
蓝晶小福泥
2022年12月19日 19:16
收录于文集
共3篇

0x01 写在前面

本专栏将承接上一篇文章​,进一步教大家如何将一个Windows的动态光标转化为MacOS可用的动态光标。

0x02 所需条件

  • 拥有一台运行MacOS的物理机/虚拟机

  • Python脚本运行能力

0x03 MacOS对指针方案的限制

首先,MacOS不像Windows一样,拥有原生的鼠标方案变更机制,MacOS原生设置并不支持鼠标方案的自定义。

MacOS 原生对于指针的设置

那么我们此时可以借助第三方程序来对指针进行定制,此处我们使用开源软件Mousecape进行指针定制。

Github链接:https://github.com/alexzielenski/Mousecape

注:Mousecape支持MacOS 10.8+以上版本,截至发稿时,作者的MacOS 13.0.1 (22A400)运行良好。

0x04 使用Mousecape进行指针定制

那么我们下载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的概念。

0x05 SpriteSheet的概念与制作

简单来说,SpriteSheet是指动画的切片。例如:

一个常见的SpriteSheet

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

动起来的SpriteSheet

Mousecape加载动画需要纵向的SpriteSheet,我们可以通过Python脚本实现,此处我们给出两种脚本,第一种是加载GIF生成SpriteSheet,第二种是从ANI生成SpriteSheet。因为从ANI生成SpriteSheet如果要经过GIF,它的过程是ANI → Pillow Image → GIF → Pillow Image →  SpriteSheet。为了避免可能的图像质量损失,我们选择直接生成。

加载GIF生成SpriteSheet:

代码块
text/x-python
自动换行
复制代码
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:

代码块
text/x-python
自动换行
复制代码
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进行图像尺寸更改。

0x06 让指针动起来

终于到了最终阶段了,回到Mousecape,将我们转换的图像拖入指针的图像框。

拖入图片

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

指针设置

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

保存提示

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

使之生效

右击我们的指针方案点击Show in Finder

分享指针

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

文件位置

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

指针文件导入

0x07 Type的选择

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

系统原生指针方案

当我们不知道Type到底是什么含义时,我们可以通过查看原生方案中使用的图标进行猜测。

0x08 后记

那么如果我们没有Mac电脑,应该如何制作一个可被Mousecape导入的指针方案呢?我将在下一篇专栏进行更新。