# Design Scenes All Posts ## #90 Figma Deep Search 扩展试验 Author: fenx | Link: https://fenx.work/figma-deep-search-chrome-extension/ Article:

Figma Deep Search 是一个基于 Chromium 的浏览器扩展。它会在你搜索 Figma 文件时,展示搜索的文本来自文件哪里。

Figma 搜索界面截图,搜素结果是三个不同类型的设计文件

如果你填写了 Figma personal access token,那么点击标签可以看到所有相关节点(上图白色弹窗),再点击节点即可在新标签页中打开已定位的设计文件。

这个扩展只是一个想法验证,所以没有打磨细节和体验,当然也没有交 5 美元的「朋友费」。已开源:

GitHub - fenxer/figma-deep-search-chrome-extensions: 用于展示 Figma Deep Search 信息,支持节点级搜索
用于展示 Figma Deep Search 信息,支持节点级搜索. Contribute to fenxer/figma-deep-search-chrome-extensions development by creating an account on GitHub.

使用

建议配合浏览器快捷搜索使用:

https://www.figma.com/files/team/{你的任意teamid}/search?q=%s

或者 Raycast Quicklinks:

https://www.figma.com/files/team/{你的任意teamid}/search?q={Query}

展示来源标签

虽然想说这个浏览器扩展名字有蹭 deep %any% 的嫌疑,但是 Figma 2020 年文章确实称之为 Deep Search ↓

A deep dive on deep search | Figma Blog
A behind-the-scenes look at how we built deep search

它的原理大致是,从你密集操作的设计文件中,每小时单独处理文件中的各种文本,将其作为单独的 text_instances 存储起来,这样在你搜索某些关键词时,可以直接搜索到文件内部图层(节点)的文本。也是因为如此,刚添加的文本是搜索不到的,需要等待一段时间。

不过 Figma 并没有依此完善搜索结果,并且限制了搜索 API 范围(无法搜索 drafts 中的文件)。

可实际在 Figma 中搜索是能搜到 drafts 文件的,打开浏览器 devtools 找了一下,都在 /api/search/full_results 里面,每个返回的文件大致结构为:

{
  "model": {
    ...
    "key": "文件 key",
    "name": "文件名",
    "editor_type": "design",
    "url": "文件 url",
    "thumbnail_url": "封面缩略图 url",
    ...
    "owner": {
        "id": "Figma UID",
        "img_url": "头像地址",
        "handle": "Fenx",
        "email": "注册邮箱"
    }
    },
    "score": 1774.2072,
    "matched_queries": [
    "deep-search-text",
    "frame-name",
    "fuzzy-name",
    "name-word-delimiter-v2",
    "name-word-delimiter-v2-with-or",
    "page-name"
    ]
}

该 API 请求需要 cookie,所以用浏览器扩展实现比较方便。

matched_queries 它的对应关系可能是这样的:

最终会得到一个 “score” 来做为搜索匹配度,score 越高相关性越高,也就是默认的 relevance 排列顺序。

扩展的第一步工作就是把这 4 个作为标签映射出来。比如显示的是「内部文本」标签,那么便可以点进去直接按 ⌘+F 搜索全部 page。

搜索节点

在上一步返回的结果中我们可以顺手拿到设计文件的 key 值,它是每个 figma 文件的唯一 id。平时在分享网址中也可以看到:

https://www.figma.com/{文件类型}/{Key}/{文件名}?node-id={nodeID}

Figma 的 REST API 中,GET /v1/files/{key} 可以用于获得文件的所有节点信息。此操作需要个人 token 鉴权。

curl -H 'X-FIGMA-TOKEN: <personal access token>' 'https://api.figma.com/v1/files/:file_key'

返回的结果有很多元信息,但这里只搜索 "name" 和 "characters" 字段。

Text(分为图层名和文本内容两种)
"id": "1:2",
"name": "goosefish in Canvas",
"type": "TEXT"
"characters": "goosefish in Canvas"
---
Page
"id": "0:1",
"name": "Page 1",
"type": "CANVAS"
---
Frame
 "id": "4:7",
"name": "goosefish",
"type": "FRAME",
---
Group
"id": "1:7",
"name": "Group 1",
"type": "GROUP",

匹配上的再查找同一层级的 "id" 和 "type",将 "type" 与上面标签匹配,"id" 用于构造访问文件的 url(文件名不可用带上)。

Slides 虽然也是差不多的结构,但是本 API 不支持,无法查询。

优化方向

虽说实现了节点级别的搜索,但实际上没我想象的直观。可能每个节点需要更多的上下文信息,以更方便确认哪个节点是我们想要寻找的结果。所以文件内的全局搜索还是很有优势的,配合扩展一起使用起码不会像以前那样看着一堆莫名其妙的文件没有头绪。

Figma 个人 token 会使用 chrome.storage API 存储,未加密,这样很不好……建议用完随时清理 token 或者不填写。

本扩展依然是由 Cursor 完成,花费了 20 多个 request。感觉有不少冗余的代码,又让开发 dalao 们见笑了。

另外如果你的广告扩展屏蔽了某些 Figma 域名(例如 www.figma.com/api/figment-proxy/monitor)那么标签的展示可能会有 bug。


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## #89 Tana 迷雾 Author: fenx | Link: https://fenx.work/tana-fog/ Article:

大家好,我是使用 Tana 时长两年的个人练习生 fenx,喜欢读、写、bullet 和标签。MUSIC!

▶️ 安静 (周杰伦) —○——— 1:20 / 5:34


上周 Tana 正式结束邀请测试,登陆 Product Hunt 并取得了不错的反响(week#1)。无论你用 Deep research 还是查看讨论,都能感知到它是一款优秀的 PKM 产品。就像它的前辈 Logseq、Workflowy 一样,基于节点,并用 supertags 连接和配置一切。全面的查询语句,继承自 Notion 的对象属性,邃密的指令功能让 Tana 远超出 outline 应用功能。

在 Tana 的官网导航列出了很多应用场景,非常推荐查看一下。

Tana
Stay on top of everything—without the busywork. An AI-native workspace that gives you an unfair advantage.

社区内也有不少入门视频,带你领略 Tana 的魅力。我也手动提取了官方的帮助文档,你可以将这个 txt 文件直接嵌入模型当做知识库。

tana_doc
download-circle

但是,本文并非介绍如何 Tana。而是帮助你考察一个新 PKM 工具时,给予一些建议——特别是难以察觉的「逆耳之言」。

文档支持

Tana 的格式和 markdown 很像,但非常遗憾是它不支持直接解析 md 格式。也就是说,你复制粘贴进去的 ** 、~~、- 等符号都会作为纯文本保留,仅换行会分节点。

这大概会给很多人致命一击,2025 年还有不支持解析 md 格式的应用?你好有的。

Tana Paste 文档中,可以看到你应该以这样的格式直接粘贴进 Tana 才会被解析:

%%tana%%
!! 标题
- **加粗文本** ^^高亮文本^^ #[[supertag]]
  - 子节点文本 __斜体文本__
    - 子文本
      - field 1:: field 值
      - field 2:: [[多个 field 值]] 
  - %%search%% 搜索节点
    * OR:: 
      * 条件 1
      * 条件 2
    * 筛选的 field:: 条件 3
- [[节点名称^节点 ID]]
- [[date:2025-02-28]]
- [插入链接](https://tana.inc)
- ![插入图片](https://image.url)

其中 field 本应有多种类型,但是在 Paste 格式中没有区分。

该格式本身不是为大量处理数据而生,所以 Tana 在长文章支持上很弱,只有基础的富文本编辑。而且其他 App 的数据导入也成问题,官方只支持 Roam、Logseq 和 Workflowy/OPML 导入,其他格式只能自己编写转换脚本。在 Tana Paste example scripts 这个 repo 里可以看到一些 mjs 脚本。

或者是官方更推荐的使用 AI 让其结构化输出,这也是后面我会提到的诟病之一。

离线模式

目前没有 → 官方 QA

从迭代趋势来看,离线本地运行一直不是 Tana 团队的重点。如果你很看重这点的话,那么 Tana 很长一段时间都不会适合你。

另外 Tana 依赖大量 Google 服务,如果你的企业防火墙阻挡了 Gmail 那么大概率也无法正常使用 Tana。

导出支持

Tana 支持导出整个 workspace(内容最大单位)的 json,但是导出的 json 包含了大量 nodeID 等冗余信息,像这样的:

{
    "currentWorkspaceId": "XXXXXXXXXXX",
    "docs": [
        {
            "id": "SYS_T01",
            "props": {
                "_docType": "tagDef",
                "_metaNodeId": "SYS_T01_META",
                "_ownerId": "SYS_T00",
                "created": 1739343247171,
                "description": "The Core supertag.  The supertag for the supertag nodes",
                "name": "supertag"
            }
        },
        {
            "id": "SYS_C01",
            "props": {
                "_metaNodeId": "SYS_C01_META",
                "_ownerId": "SYS_T41",
                "created": 1739343247171,
                "description": "Describe what this app is for",
                "name": "Description"
            }
        },
      ...

可读性极差,清洗价值低,前几百行都找不出个有用信息。

后来 Tana 更新了 Copy as Tana PasteCopy as plain markdown,情况有所缓解,但功能上来说依然很潦草。

AI, API 与 会员

Tana 中可以直接发起 API 请求做为指令,并可以将指令打包起来插入到几乎任何地方。这个功能非常强大,也是我觉得其他 PKM 产品没有做到的地方。举个例子,你可以新建个每日新闻模板,配置好仅需一键便可通过 API 获取今日新闻和天气信息。低代码万岁。

但是——

Tana 的主推策略是什么都得和 AI 强绑一下。比如你可以建立 3 个 prompts,将其做为某个指令的 3 个选项,这看起来很不错。但是到 API 解析这里你也只能寄希望于 AI 为你结构化输出,否则返回 json 只能作为一坨纯文本输入。

更别说 POST 模式时 payload 内容(也就是 body 请求内容)格式也有去换行需求和种种限制,你只能通过 ${Field Name} 调用父节点下的 field 值;要是直接引用某个节点的话,[[]] 会被直接格式化为文本,从而和 json 中的 array 格式冲突……

而 AI 正是 Tana 会员的收费点。

会员

无论是 8 美元每月的 Pro 还是 12 美元每月的 Core 会员,AI 功能都是其中的重点。免费用户即使有自己的 API key 也无法使用。你没有更多的模型选择,只有 OpenAI 和 Claude(微调的重心也是前者)。自定义模型和自定义 API Key 都是不存在的。

Tana 价格对比
Tana 订阅价格对比

这种只差临门一脚的「卡脖子」行为,正是我感到很不舒服的地方。使用 Tana 到这里的人会看不出来你的小心思吗?该开放的地方开放,去贩售核心买点而不是对完整功能上锁,才是理想中会员模式。朋友说有这有点像米哈游的命座 / 星魂 / 影画,有时就得多抽几命才能体验完整,你看 Tana 还便宜着呢。我嘴上说 Tana 罪不至此,但心里也是默默赞同。

说回正题,上面的会员服务对我来说价值不太高。Google 日历和语音记录那些目前完全用不到,其他功能多少也是可有可无,这让我进退两难。Tana 为了解决这种焦虑,直接在免费套餐里定义了 20,000 个节点上限。如 Reddit 上评论所说,「这种模式让我感觉,我在使用一个早晚会抛弃的服务。」我理解小团队的艰难,所以我依然会观察 Tana 一段时间。

话说能在哪里看到,已经使用了多少节点呢?貌似也没有。

发布到网络

API 方面,Tana 支持 Input API,且仅支持 POST,想要 GET 的话还得再等等。也就是说目前仅支持其他应用向 Tana 写入数据,在 Tana 写的很多东西却无法分享。这点上远不如 Notion 和 Capacities。

很急。

某月某日,Tana 发布了 Publish 功能,这是我非常期待的功能,也是一个让我感到无比失望的功能。

它的功能是发布某个节点到网络上,其他人可以公开查看。但是界面无比简陋,以至于毫无分享欲望。

左侧是 Tana 中编辑界面,右侧是发布预览

如上图右侧样式,这已经是我尽力调整过的了。不支持自定义 CSS 就算了,能像左侧编辑模式一样也可以啊,结果这个样式……他真的我哭死(贬义)。

之后打算设计一个工作流来独立展示 Tana 上的这些内容。

半成品表达式

文本表达中,逻辑的构建是富文本之上,通往知识库的康庄大道。

Tana 支持一些简单的表达式从 field 取值。比如下方节点结构中:

Tana 中一个节点示意

在 #Article 下,${name} 代表标题自己输入, ${总结} 即可调用总结属性对应的信息,${总结|30…} 代表取总结信息的前 30 字符。还有一些系统默认属性(比如节点创建时间)可供调用。

除此之外,没了。

这让我很费解,Tana 支持表达式,但是却只用在节点筛选(Node Filter 属性)上,而无法精确取值。比如上图中我为 #url 所赋予的 Domain 属性,明明可以从 URL 中通过正则获取,但我需要手动输入。近几个月我在高强度使用快捷指令(Shortcuts),了解到完整的逻辑建立在任何系统都非常有用。希望 Tana 不要止步于此。

其他体验

Tana 的界面说不上精致,但是能用下去。唯独 ⌘ + K 这个上下文菜单让我感到了交互困惑。

Tana 的上下文菜单截图

它是这样的:

另外 Tana 帮助文档给我的感觉是,它是为能看懂文档的人而写的——不懂的人看了文档也是不懂,它缺少很多细节例证和默认值。比如可以看看这篇 AI 指令文档

总的来说

允许我再次重申一下,本文并非阻止你使用 Tana。我很乐意向大家推荐去使用它,一旦解锁了 AI,Tana 能做很多事情。我自己也在上面记录了很多,并 copy 全文 as markdown 放到了知识库里。但也不得不说明出短板来节省你的时间。你可以在官方反馈列表中看到更多尚未满足的需求。据我的感知 (?),核心用户更集中在他们的 slack 群,所以基本会优先满足他们的需求。

Tana 的 Roadmap 停留在去年的 8 月份,2025 年的还没有更新。不过能看到的是,Tana 完成了由 Tola Capital 领投的 1400 万美元 A 轮融资,总融资额达到 2500 万美元。财务状况比较乐观,希望 Tana 多招聘点能人多完善产品吧!


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## # SP3 给 Ghost 增加 llms.txt Author: fenx | Link: https://fenx.work/add-llms-txt-to-ghost/ Article:

/llms.txt 文件是用于 LLM 的标准化文件,由 Answer.AI 发起。就像 sitemap.xml 之于搜索引擎,llms.txt 旨在让 LLM 更好更全面的理解一些在线文档。

你可以在下方的官网了解。

The /llms.txt file – llms-txt
A proposal to standardise on using an /llms.txt file to provide information to help LLMs use a website at inference time.

llms.txt directory 上能看到,许多产品文档都添加了该文件,这更有助于一些编程 agent 理解代码文档。

由于 /llms.txt 是开放的,如果理论上可以为任意网站添加一个。Firecrawl 就做了一个生成器 LLMs.txt generator,需要 API 才能完整输出(也可以本地部署)。

Ghost 这样的 CMS 系统,效仿 RSS 生成使用各种 handlebars 便可自行组装一个 /llms.txt

如何禁止AI Bot 抓取网站内容

先叠个甲,大多数人不愿意自己网站被 AI Bot 抓取,训练的模型不会给你的著作权署名,反而会消耗网站的流量。你可以在 robots.txt 加入各种 user-agent 限制。比如:

User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

你能在很多地方找到 AI 厂商公开的 user-agent(比如这里),但是还有未公开的,目的就是为了获取新鲜语料。像 Cloudflare 这种大厂直接分析上万域名的 robots.txt 寻找隐藏的 AI Bot,来专门维护一个规则群,基本能排除得差不多。

Ghost 可以直接在主题的根目录下新建 robots.txt文件来覆盖默认的规则,非常方便。

如果你和我一样秉承开放的心态,且网站流量还算正常,那么可以向网站根目录添加 /llms.txt,方便自己,也可以顺便方便他人。

添加 llmx.txt

上面提到过,效仿 RSS 生成原理即可。可以先看看自定义 RSS 这篇文章。

Official Ghost + Custom RSS Integration
Ghost comes with automatic always-on RSS feeds for your site content - but fully supports custom RSS for when you need to add extra functionality!

打开 Ghost 后台设置,找到 Labs 设置项,点击 Download current routes

Ghost 后台设置的 Labs 项

你会得到当前网站的路由配置文件 routes.yaml。找一个编辑器,打开它。默认结构大概类似这样:

routes:

collections:
  /:
    permalink: /{slug}/
    template: index

taxonomies:
  tag: /tag/{slug}/
  author: /author/{slug}/

在 routes 下方,添加两个路径:

routes:
  /llms.txt/:
    template: llms
    content_type: text/plain
  /llms-full.txt/:
    template: llms-full
    content_type: text/plain

根据 Ghost 规则,之后要在主题的根目录里创建 llms.hbsllms-full.hbs 文件,来对应这两个路由。前者是文章的预览,包含文章的标题、摘要和链接;后者是当前网站的所有文本内容。


llms.hbs:

# {{@site.title}}

> {{@site.description}}

## Posts

{{#get "posts" limit="all" include="authors,tags"}}
    {{#foreach posts}}
- [{{title}}]({{url absolute="true"}}): {{excerpt}}
    {{/foreach}}
{{/get}}

## Optional

- [Blog Platform](https://ghost.org/)
- [Author](https://x.com/haxfenx)

这里 handlebars 都是语义化的不多解释了。本质这是一份 markdown 格式的纯文本文件,在遵照 llms.txt 格式下,你可以自由添加额外信息。#foreach posts 用于循环来获取你的全部文章,各种参数限制可以参照这里添加,比如限制文章个数等等。

Optional 是额外可选信息,可以不展示。复制粘贴的话记得把我的主页链接替换掉……


llms-full.hbs:

# {{@site.title}} All Posts

{{#get "posts" limit="all" include="authors,tags"}}
    {{#foreach posts}}
## {{title}}

Author: {{primary_author.name}} | Link: {{url absolute="true"}}

Article: 

{{content}}

    {{/foreach}}
{{/get}}

全文本这里格式更自由一些,我也只是随便列了一些。{{content}} 自带了访问权限,即你的文章设置了非所有人查看,那么这里也会对应无法查看。Ghost 有着丰富的 handlebars,你可以自定义一份你自己的文本。


之后把更改的 routes.yaml传回 Ghost(就是下载该文件的上方按钮),新加的 hbs 文件也要随着主题重新上传或激活一下。访问你的网站域名+ /llms.txt 即可看到效果。

例如我的网站添加后可访问:

将网址直接添加到上下文或 RAG 中即可。


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## #88 制作一款 Figma 中查看图片直方图的插件 Author: fenx | Link: https://fenx.work/figma-plugin-show-image-histogram/ Article:

Image Histogram 插件可以让你在 Figma 中查看图片的直方图,以及复制粘贴曝光、对比度等图片调整参数。

Image Histogram | Figma
在 Figma 查看图片直方图 支持彩色直方图和亮度直方图;调整图片实时预览;复制粘贴图片调整参数;两种直方图样式;免费; Show Image Histogram in Figma Supports RGB and luminosity histogram;Real-time preview of image adjustments;Copy and paste image adjustments;Two histogram styles;Free;

之前我还做了一个在 Figma 中给位图添加描边的插件

这个插件应该无需详细说明,下面分享的是制作这款插件的一些历程。对于开发大佬们应该没什么价值,权当是我自己的一个梳理复盘。

需求来源

最开始的痛点在于 Figma 无法复制粘贴甚至无法看到图片调整的具体数值。

Figma 中的图片调整工具截图

2025 年了,调整完连个「重置」按钮也没有,归零还有手动一一滑动每个滑块。我知道 Figma 的产品重点不是图像编辑。但这不太符合 Figma 以往的设计调性。以前的 Figma 在迭代复制粘贴时展示了足够的耐心和细心。而在这种边缘角落便和常规互联网公司没什么区别——又不是不能用。

复制粘贴数值这点,已经有一些图像编辑插件可以实现(搜索 Image edit 关键词)。那一晚灵光突现,想看看有没有人在 Figma 做直方图。一番搜索后发现貌似没有,是啊毕竟对 UI 设计没什么用。

但是感觉挺有意思的!

效果

一组不同亮度的风景图片
一组不同曝光侧重的动物图片

拆分需求

插件有两个核心需求:

  1. 展示选中图片直方图
  2. 复制/粘贴图片调整参数

以此拓展:

  1. 展示选中图片直方图:
    1. 【高】展示彩色和亮度(灰度)直方图
      1. 【中】并可切换只看某通道的直方图;
    2. 【中】调整图片时,直方图实时更新;
  2. 复制/粘贴图片调整参数:
    1. 【中】查看参数数值;
    2. 【高】参数能被重置;
  3. 插件通用需求:
    1. 【中】英文本地化;
    2. 【低】浅色/深色主题模式适配;

这里用 高中低 代表了需求优先级。目前的 AI 辅助编程,最好还是模块化去实现各个需求,方便测试、纠错,并且可以在每个实现需求的关键节点内部迭代版本。下面是我在本次开发中的迭代记录:

0.1: 初步获取直方图各个通道数据;
0.2: 初步显示直方图;
0.3: 完善直方图细节,调节准确性;
0.4: 可以实时获取调整后的图片直方图;
0.5: 增加滤镜参数展示,复制、粘贴功能;
0.6: 初步完成 UI 适配;
0.7: 更改通道展示逻辑,可自由配置通道组合;
0.8: 实现直方图样式切换;
0.9: 修复Figma 新 API 导致的 <canvas> DPI 显示错误;
1.0: 上线送审版本:添加翻译,添加对多图层的识别逻辑;

就像我们熟悉的大多产品流程一样,需求并非一成不变,根据测试和使用体验需要不断调整。

学习学习

我不是开发,所以我在 Cursor 左侧配置了 Continue 的tab,拿 DeepSeek 代码和对话模型去验证和理解代码知识。这样不占用 Cursor 额度,便宜量大,缺点是上下文长度 128K。

拿到颜色

Figma 中涉及图片处理,基本都需要解码图像。上次做描边插件也是,解码后会 Figma 会返回该图像的带有 RGBA 的一维数组(A 是 alpha 透明度信息),这样每四个字节一循环便可拿到所有像素 RGB 信息。

一个 2x2 的图像格式为:
[R0, G0, B0, A0, R1, G1, B1, A1, R2, G2, B2, A2, R3, G3, B3, A3]

至于亮度计算采用了 BT.709 标准,比 BT.601 更适合现代屏幕,增加了绿色权重并进一步降低蓝色权重。

电脑中的图像输出 RGB 值大多已经经过伽马校正的非线性数值(也就是 R G B 后面带个撇号),所以直接拿来做乘法。当然 BT.601 的亮度公式我也试过,日后看看需不需要修改回来或者作为切换项。

Grayscale - Wikipedia

备菜完毕。

展示直方图

这次 code.ts 主要用于处理图片调整参数,所以可以歇歇了,直方图展示需要再 ui.html 中新建 <canvas> 来展示。按照我的需求,AI 的解决方案是预先绘制 4 个 canvas:

从下到上执行。

插件两种直方图样式

默认是右侧 style 2 的样式。描边 + 半透明填充的曲线。为了实现左侧这种,启用了上面提到的 offscreenCanvas 以 plus lighter 模式混合各通道重叠处,也就是:

Plus lighter 需要提高曲线的填充透明度,索性设置成了两套样式。

const histogramStyles = {
  1: {
    red: { stroke: 'rgba(255, 0, 0, 0.8)', fill: 'rgba(255, 0, 0)' },
    green: { stroke: 'rgba(0, 255, 0, 0.8)', fill: 'rgba(0, 255, 0)' },
    blue: { stroke: 'rgba(0, 0, 255, 0.8)', fill: 'rgba(0, 0, 255)' },
    luminosity: { stroke: 'rgba(255, 255, 255, 0.8)', fill: 'rgba(255, 255, 255, 0.4)' }
  },
  2: {
    red: { stroke: 'rgba(255, 0, 0, 0.9)', fill: 'rgba(255, 0, 0, 0.2)' },
    green: { stroke: 'rgba(0, 255, 0, 0.9)', fill: 'rgba(0, 255, 0, 0.2)' },
    blue: { stroke: 'rgba(0, 0, 255, 0.9)', fill: 'rgba(0, 0, 255, 0.2)' },
    luminosity: { stroke: 'rgba(255, 255, 255, 0.9)', fill: 'rgba(255, 255, 255, 0.2)' }
  }
};

曲线精度没打算像专业软件一样,变成凹凸不平的柱状图,所以用三次贝塞尔曲线来连接各个点。不过测试时,在某些极端状况下,曲线会变得如锯齿般难以解读(无意义的锯齿),通常是距离较近的 X 轴上有着很高的 Y 轴落差。所以这里面又加入了高斯滤波,减少局部凸起的「噪声」对整体直方图的影响。

还在摘 OpenCV 的桃。

图像调整参数

Figma 中的一切都是由 node 构成的,所以寻找哪个元素就像「山里有座庙,庙里有个老和尚」一样层层寻找判断。这次要找的是当前选中的图层,Figma 有专门的接口 SceneNode,我们直接开始声明:

let selectedNode: SceneNode | null = null;
//...

//如果当前选中的不是矩形节点,输出警告信息
if (!selectedNode || selectedNode.type !== 'RECTANGLE') {
    console.log('No selection or not a rectangle');
    figma.ui.postMessage({ type: 'no-selection' });
    return;
}

//看看 Fill 有无内容,输出警告信息。.fills.length 也可用于选中特定第 N 个图层
if (!selectedNode.fills || !Array.isArray(selectedNode.fills) || selectedNode.fills.length === 0) {
    console.log('No fills found');
    figma.ui.postMessage({ type: 'no-image' });
    return;
}

// 找到最上方的未隐藏的图片填充(fill.visible !== false)
const visibleImageFill = selectedNode.fills
  .slice()
  .reverse()
  .find(fill => fill.type === 'IMAGE' && fill.visible !== false);

// 通过 .filters 拿到调整参数
const filters = visibleImageFill.filters || {};

// 发给 ui.html 以调用
figma.ui.postMessage({
      type: 'process-image',
      data: Array.from(bytes),
      filters: filters
});

// 这里省去了 .on 用来监视属性变化的代码

感觉是一件挺简单的事但是 Figma 的文档搞得特别抽象,生怕多写一个单词就会扣钱一样。配合 REST API Get file 会更好理解一些,比如一张图片节点其实这样的(截取):

{
"id": "18:94",
"name": "image 2",
"type": "RECTANGLE",
"fills": [
  {
    "blendMode": "NORMAL",
    "type": "IMAGE",
    "scaleMode": "FILL",
    "imageRef": "*ref*",
    "filters": {
      "exposure": 0.49000000953674316,
      "contrast": 0.12999999523162842,
      "saturation": -0.6000000238418579,
      "temperature": 0.4399999976158142
    }
  },
  {
    "blendMode": "NORMAL",
    "visible": false,
    "type": "IMAGE",
    "scaleMode": "FILL",
    "imageRef": "*ref*"
  }
],

可以看到,只有图层是否隐藏由 “visible”控制,而且只有调整参数不是 0 了,才会有 filters 项。这些调整参数都是取 -1 到 1 区间的值,中间为 0,即默认未调整状态。顺便吐槽一句,Figma 在面向用户时管这个叫 adjustments,实际上用的还是 filter 命名。

到了复制粘贴操作,我采用了 figma.clientStorage 来存储复制后参数,相当于本地的缓存数据,清除后会消失。很适合存储这种临时复制粘贴的数据,又不涉及隐私。

储存形式是按照一定顺序和格式生成 FIA4P[0, 0, 0, 0, 0, 0, 0] 这样的字段。由于 figma.clientStorage 能储存很多类型变量所以保险起见还是加了标识符,毕竟 AI 很擅长写正则。

// 声明
const filterOrder = ['exposure', 'contrast', 'saturation', 'temperature', 'tint', 'highlights', 'shadows'];
const values = filterOrder.map(key => currentFilters[key] || 0);
const text = `FIA4P[${values.map(v => v.toFixed(2)).join(',')}]`;
      
// 保存到 clientStorage 的消息传递
parent.postMessage({ 
  pluginMessage: { 
    type: 'save-filters',
    filterText: text
  }
}, '*');

// 同上,取出
parent.postMessage({ 
  pluginMessage: { 
    type: 'load-filters'
  }
}, '*');

// (前面有 msg.type 类型判断) 保存成功后在控制台和 Figma 底部输出提示
await figma.clientStorage.setAsync('latest_filters', msg.filterText);
console.log('Filters saved:', msg.filterText);
figma.ui.postMessage({ type: 'save-success' });

// (前面有 msg.type 类型判断) 取出参数与提示
const filterText = await figma.clientStorage.getAsync('latest_filters');
console.log('Loaded filters:', filterText);
figma.ui.postMessage({ 
  type: 'load-filters-result',
  filterText: filterText
});

.fills 貌似直接带了覆写功能,所以遍历找到相应参数可以直接修改:

const newFills = selectedNode.fills.map(f => {
  if (f.type === 'IMAGE') {
    return {
      ...f,  //浅拷贝所有属性
      filters: msg.filters  //更新 filters 属性
    };
  }
  return f;  //如果填充不是图像,则返回原始填充;
});
  
// 更新节点的 fills
selectedNode.fills = newFills;

// 更新直方图显示
await updateHistogram();

参数重置同理,直接将全部 filter 参数赋予 0 即可。

到这里,插件的两项功能已经实现的七七八八了。

其他通用需求

Figma 中要想实现深色和浅色主题模式切换,需要用到官方提供的一套 color tokens。告诉 AI 这个网址让他们自己看去吧:https://www.figma.com/plugin-docs/css-variables/。你可以拉到最下方手动微调颜色。

中英翻译一般是临近上线版本再去做,这类需求 AI 基本也会一遍成功。找到了声明的 translations 对象,里面中英内容可自行校对和调整。

闲暇时间可以把下面商店页信息定下来:

完成上线版本后可以第一时间送审。因为排队和时差原因,至少 24h 后才会收到邮件通知让你上传录屏或通知核对结果。Figma 的网页环境基本不会出什么岔子。着急的话尽量周一等早些时候提审,因为那边双休。

总结

由于这次算法不复杂,所以成型时间很快。再一次感受到,AI 无法实现我的需求时,大概率是我自己没有表述清楚。日常人与人对话中自带了很多「上下文」,很多时候会出自本能地消歧义。AI (Claude-3.5-sonnet) 显然尚未达到这个水平,但是理解地越来越好了。所以一般对话 8-10 轮没有实现需求时,我便会读取需求前的存档重新再来。下次也会试试 .cursorrules 会带来什么改变。

在了解照片直方图的过程中,我也在思考有没有一种适用 UI 界面的「直方图」,去量化看到界面的视觉体验。此时只是单纯地去列出像素数值没有意义,可能还需要考虑颜色之间的「间距」……总之这也是个有意思的话题,值得长期探索。

## #87 制作一款给位图增加描边的 Figma 插件 Author: fenx | Link: https://fenx.work/figma-plugin-add-stroke-to-bitmaps/ Article:
🌐
You can read the English version of this article here.

Image Stroke 插件的主要目的是,为 Figma 中的位图图层添加矢量描边。

Image Stroke | Figma
直接在 Figma 中为位图添加矢量描边。 只专注于生成图像的轮廓描边,不处理图像内部细节;开箱即用,一切都在 Figma 内部完成;中文英文语言切换;符合 Figma 的浅色和深色主题模式;免费; 详细使用说明可在插件中查看。 --- Add Vector Strokes to Bitmaps Directly in Figma Focuses solely on generating the outline stroke of the image, without processing internal details;Ready to use out of the box,…

就像 PhotoShop 等软件中常见的 Layer Style - Stroke 效果一样,但在 Figma 中一直尚不支持。

做这个插件,一方面是为了自己的需求,另一方面是熟悉 Cursor 的开发流程。

在 Figma 中给草莓透明 PNG 添加描边对比

如图所示,左图是一张透明背景的位图,直接对它添加 Stroke 的话会产生中图的效果,即对该图层而非图像内容添加描边。右图则是本插件预期达到的效果。

为什么使用 Image Stroke

传统添加描边方法有:

Image Stroke 可以:

当然 Image Stroke 也不是万能的:

额外注意事项:

以上我会在下面详细说明。

过程与原理

首先我必须要声明,我不懂代码,插件大部分的代码都由 Cursor 完成。我的工作是提出需求,并在正确的方向上引导 Cursor 生成代码。

一开始我想的也是直接写一个矢量化功能,但是发现过于复杂,且效果不好。我开始寻找 PhotoShop 生成描边的算法,转而发现了 Distance Field 系列算法,Valve 曾改进此算法矢量化贴图并将应用到自家起源引擎中(Improved Alpha-Tested Magnification for Vector Textures and Special Effects)。然而这依然不是想要的,它应该很简洁才对。

经过一番搜索发现,Marching ants 算法可能是我想要的答案。这似乎是一种很古老又很常见的算法,只是我不身处这个领域尚未得知。经朋友点拨,找了些资料发给 Cursor,最终形成现在代码:

  1. 首先从图片的左上角开始,按列扫描像素,每列从上到下扫描;
  2. 找到第一个非透明像素点作为起始点;
  3. 以起始点为准,对其 8 方向像素透明度探索,直到找到下个轮廓点;
  4. 当检查的点本身不是透明像素且周围至少有一个有透明像素,即可认为是轮廓点;
  5. 按照一定方向和采样率追踪其他轮廓点,使路径单一且封闭;
  6. 在这个过程中使用 SVG 的 M 命令生成锚点,使用 L 连接下一个锚点。当首尾距离小于某个参数时,使用 Z 封闭路径;

经过一些黑盒测试后,设定了自动 threshold 所用到的参数与检测到第一个像素点透明度系数大约为2。不正确输出的话则 +1 直到 255。

值得一提的是,图片处理需要事先 decode 到 RGBA 数据。这在文档 Working with Images 有提到,但 Cursor 默认是不知道的。所以作为 AI 辅助代码的使用者,即使不亲自写代码,也还是需要知道代码逻辑关系的。而 Cursor 感觉也可以往这个方向迭代一下,比如在 notepads 之外选择自带的一些知名库的文档一键投喂。

参数说明

描边宽度和描边颜色

不多赘述,按照 Figma 中 Storke 属性数值填写就好。描边拐角已默认设置圆滑;

采样率

值越大,轮廓越简单(1-10),锚点越少,细节更少。如下图所示。

采样率对比测试

简化容差

值越大,路径越平滑。如下图所示。

简化容差对比测试

透明度阈值

类似矢量化算法中的 threshold,与图片边缘清晰度有关系。如下图所示,我用 blur 手动降低了图片边缘清晰度,边缘越模糊越需要减小阈值来贴合边缘。

阈值对比测试

闭合路径阈值

当收尾锚点距离小于这个值时闭合路径,否则继续追踪轮廓。在原理中会提到。一般不用改,所以作为高级设置隐藏起来了。

缺点说明

轮廓识别算法依然有优化空间

目前是从上到下从左到右线性地开始扫描,如果能从四周开始扫描可能效率会更高?我不确定,而且可能多个追踪方向会影响路径的闭合。


有时自动寻找透明度阈值(threshold)会失效,需要手动调节参数

就是这样。如果要手动设置的话,你可以放大图片直到看到方形像素。找到左上方第一个像素点,试着判断一下这个像素块的透明度阈值多少。像素透明度越低,需要设置的阈值参数就越低。


有些特殊的图片会失效,需要额外手动调整图片属性

目前仅支持 PNG 图片,你可以手动导出一下(或 ⇧⌘C)。因为测试图片数量有限,所以难免会出现怎么调节参数都不行的状况。有精力的话我会再优化一下。


插件识别的是原图,所以无法生成裁剪后的图像路径。正确方式是裁剪后导出 png 再使用插件;

同上,有时间的话再修复。


没有开源仓库

由于代码大部分是 AI 生成的,而且难度不高,我觉得上传 GitHub 多少有些不合适。如果有多个人好奇源码,我再考虑一下开 repo。


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## Creating a Figma Plugin to Add Stroke to Bitmaps Author: fenx | Link: https://fenx.work/figma-plugin-add-stroke-to-bitmaps-en/ Article:
🌐
你可以阅读本篇文章的中文版本

The primary purpose of the Image Stroke plugin is to add vector strokes to bitmap layers in Figma. Similar to the Layer Style - Stroke effect commonly found in software like Photoshop, this functionality has been missing in Figma. 

Image Stroke | Figma
直接在 Figma 中为位图添加矢量描边。 只专注于生成图像的轮廓描边,不处理图像内部细节;开箱即用,一切都在 Figma 内部完成;中文英文语言切换;符合 Figma 的浅色和深色主题模式;免费; 详细使用说明可在插件中查看。 --- Add Vector Strokes to Bitmaps Directly in Figma Focuses solely on generating the outline stroke of the image, without processing internal details;Ready to use out of the box,…

Creating this plugin was driven by two main goals: one was to fulfill my own needs, and the other was to familiarize myself with the development workflow of Cursor.

As shown, the left image is a bitmap with a transparent background. Applying a stroke directly to it results in the middle image, where the stroke is applied to the layer rather than the image content. The right image demonstrates the desired effect achieved by this plugin.

https://figma plugin url

Why Use Image Stroke?

Traditional methods for adding strokes :

Image Stroke‘s advantages:

However, Image Stroke has its limitations:

Additional Notes:

The following sections will provide detailed explanations of these points.

Parameter Explanation

Stroke Width and Stroke Color

Sampling Rate

A higher value results in a simpler contour (1-10), with fewer anchor points and less detail.

Simplify Tolerance

A higher value results in a smoother path.

Edge Threshold

Similar to the threshold in vectorization algorithms, this parameter is related to the clarity of the image edges. As shown in the illustration, I manually reduced the edge clarity using a blur effect. The more blurred the edges, the lower the threshold needs to be to accurately follow the edges.

Closed Path Threshold

Closes the path when the distance between the starting and ending anchor points is less than this value; otherwise, the contour tracing continues. This will be discussed in the "Process and Principle" section.

This parameter is generally not modified, so hidden in the advanced settings.

Process and Principle

First, I must declare that I am not a programmer. Most of the plugin's code was generated by Cursor. My role was to propose requirements and guide Cursor in generating the code in the right direction.

Initially, I considered writing a vectorization function, but found it too complex and ineffective. I then researched the algorithm Photoshop uses to generate strokes and discovered the Distance Field series of algorithms. Valve had improved this algorithm for vectorizing textures and applied it in their Source engine (Improved Alpha-Tested Magnification for Vector Textures and Special Effects). However, this still wasn't what I was looking for—it needed to be simpler.

After further research, I found that the "Marching Ants" algorithm might be the answer. This seems to be an old yet common algorithm, though I was unaware of it due to my lack of expertise in this field. With some guidance from a friend, I gathered some materials and shared them with Cursor, which eventually led to the current code.

  1. Start scanning the image from the top-left corner, column by column, and from top to bottom within each column.
  2. Identify the first non-transparent pixel as the starting point.
  3. From the starting point, explore the transparency of the 8 surrounding pixels until the next contour point is found.
  4. A pixel is considered a contour point if it is not transparent and has at least one transparent pixel nearby.
  5. Trace other contour points in a specific direction and sampling rate to create a single, closed path.
  6. Use SVG's M command to generate anchor points and L to connect to the next anchor point. When the distance between the start and end points is less than a certain parameter, use Z to close the path.

After some black-box testing, I set the parameters for the automatic threshold, with the detected first pixel's transparency coefficient approximately set to 2. If the output is incorrect, increment the value by 1 until it reaches 255.

It's worth noting that image processing requires decoding the image into RGBA data, as mentioned in the Working with Images documentation. However, Cursor was not initially aware of this. As a user of AI-assisted coding, even if you're not writing the code yourself, it's important to understand the logical relationships in the code. Cursor could also evolve in this direction, such as offering one-click access to documentation from well-known libraries, in addition to notepads.

Limitations

Contour Detection Algorithm Still Has Room for Improvement

Currently, the scanning starts linearly from top to bottom and left to right. Scanning from all sides might be more efficient, though I'm not certain, and multiple tracing directions might affect path closure.

Automatic Edge Threshold Detection May Fail

As mentioned, manual parameter adjustments may be necessary. To manually set the threshold, zoom in on the image until you can see the square pixels. Locate the first pixel in the top-left corner and estimate its transparency threshold. The lower the pixel's transparency, the lower the threshold parameter should be.

Some Special Images May Not Work

Currently, only PNG images are supported. You may need to manually export the image or use ⇧⌘C. Due to the limited number of test images, there may be cases where adjusting parameters doesn't work. I will consider further optimizations if time permits.

Only Supports Single-Image Content Strokes

If an image contains multiple disconnected elements, only the leftmost one will be stroked.

Plugin Recognizes the Original Image

The plugin cannot generate paths for cropped images. The correct approach is to crop the image, export it as a PNG, and then use the plugin.

No Open-Source Repository

Since most of the code was generated by AI and is not particularly complex, I feel that uploading it to GitHub might not be appropriate. If there's significant interest in the source code, I may consider opening a repository.

## #SP2 iii 计划背后的 40 家独立游戏厂商介绍 Author: fenx | Link: https://fenx.work/the-triple-i-initiative-introduce/ Article:

iii 计划(The Triple-i Initiative)是一场专为独立游戏及创作者打造的线上展示会。当日凌晨 1 点就到了展会播片时间,也趁此机会介绍一下背后的 40 家独立游戏厂商(名单来自官方稿件)。

由于非设计相关内容,本文没有邮件推送。

📕
内容原发布于我的小红书账号。如果你也喜欢游戏相关,可以关注:game_gems
iii 计划参展游戏

国内厂商

首先是三家国内的:《戴森球计划》的开发商柚子猫游戏工作室以及发行商 Gamera Games。柚子猫目前还在进一步完善处于 EA 状态的游戏。Gamera Games 除此之外还发行过《火山的女儿》、《烟火》等销量很不错的游戏。选品很广且品味不错(比轻语好点哈哈)。特别是发行维斯塔利亚传说系列时印象比较深,如此「冷门」的 SRPG 多少有点用爱发电为大家带来汉化的感觉……

玩过或得知《波西亚时光》系列的,对帕斯亚科技应该也很熟悉了。国内他们是自发行,西方的话,有 Focus 和 PM Studios,后者也发行了很多国产开发的游戏(下期再讲),推测和 Focus 区别在于偏向实体游戏的发行工作。


Red Hook Studios

Red Hook Studios 以暗黑地牢系列闻名。一代气人难度和优秀的 mod 社区都给肉鸽玩家留下了很深的印象,二代有点取其糟粕去其精华加上 Epic 独占惹恼不少人,但美术依然在线。


Poncle

吸血鬼幸存者可以说开创了一个「新时代」,站在时代风口的幸存者类游戏无不赚得盆满钵满。开发者 Galante 曾在失业后独自开发一年,目前组成了 15 人的游戏开发团队 poncle,依然在完善更新游戏。


Heart Machine

Heart Machine 2016 年发布光明旅者走进大家视野(梦幻的 2016 年)。HLD 以高水准的美术、战斗和地图甚至当时被媲美 2D 塞尔达系列转世。从 Solar Ash 积累的一些 3D 经验后,最新作 Hyper Light Breaker 已有不错的成果展示,整个期待了。


Mega Crit

Mega Crit 是杀戮尖塔的开发商。肉鸽 deck-building 虽然不是 STS 首创,但却很好地继承 Dream Quest 衣钵并发扬光大。后续推出 mod 平台进一步延长了游戏寿命。


Blobfish Games

Blobfish Games 是幸存者类游戏土豆兄弟的的开发商。这家低调的开发商总是喜欢用土豆当做主角,并用世界上「最丑的」水滴鱼当做 logo。


Ghost Ship

Ghost Ship 是大名鼎鼎的多人合作 FPS 类游戏深岩银河的开发商,游戏每个标签都是热卖的代言词。发家后也开始做发行,本次也是以发行商名义参加。


Evil Empire

Evil Empire 是一家法国独立游戏厂商,和 Team Cherry 一样仅靠一款类银河战士恶魔城游戏走红。团队这几年都在做死亡细胞的系列作。终于在育碧那边接了笔大单,波斯王子:Rogue。Evil Empire 也是此次 iii 计划的几位发起人之一。虽然他们自称「邪恶帝国」,但一直倡导工作生活平衡,强烈反对加班。


The Gentlebro

The Gentlebros 是一家新加坡独立游戏厂商,主要是开发猫咪斗恶龙系列(Cat Quest),第一作有捏他勇者斗恶龙系列,但是后面有了自己的独立玩法,在这次 iii 计划展会也是发布了 Cat Quest III。工作室一直都是核心 4 人完成所有游戏开发工作。直到前年 8 月份某位成员身体原因人手不足才开始扩招新人,也搬进了新的办公室。


tinyBuild

tinyBuild 是一家美国的游戏发行商。发行游戏风格复杂多样。2011 年时以做 No Time To Explain 起家(logo左边就是游戏中角色),首次发行了外科医生模拟器(Surgeon Simulator)赚了不少银子,再后来 SpeedRunners、拳击俱乐部(Punch Club)、你好邻居(Hello Neighbor)等游戏,让 tinyBuild 跻身一流发行商并开始并购其他开发和发行团队。

扩张之后,然而近几年除了药剂工艺:炼金模拟器以外几乎没什么爆款。去年年末解散了千万美元收购的发行商 Versus Evil。最新上线的 Broken Roads 目前基本凉凉(95评测41%好评),地痞街区2(Streets of Rogue 2)也在 iii 计划公布。


PM Studios

PM Studios 入行很早,09 年就开始在北美发行 PSP 游戏,比如 DJ Max 系列。发行过一段手游。后来和华语开发商交好,在多平台面向西方发行了许多中文游戏,像是彩虹坠入、Deemo、OPUS 系列、WILL:美好世界、沙石镇时光等等。当然其他方向游戏也有发行,由于大多是主机平台和实体游戏,所以在页面上并不常出现。


Focus

Focus 是一家法国游戏发行商。像原子之心、贪婪之秋、瘟疫传说、Vampyr、迸发和暗邪西部,包括最近发布的驱灵者,都是他们家发行的游戏。看多了就能感受到他们家所专注于的西式写实魔幻风格味太冲了,以至于很奇怪能帮帕西亚发行XXX时光系列哈哈。

Focus 一开始全名是 Focus Home Interactive,后来改成 Focus Entertainment。现在是 4 月份了,集团又更名为 Pullup Entertainment,重组为三部分:原发行部门、专注于复古游戏的 Dotemu 以及 Deck13、Leikir Studio 等收购的开发工作室。


Gearbox Publishing

Gearbox Publishing 来路曲折。最开始是完美世界在北美有个分公司 Perfect World Entertainment 负责海外游戏运营和发行,后来被搅屎棍 Embracer 收购并入到 Gearbox,并更名为 Gearbox Publishing San Francisco。这个 Gearbox 就是做无主之地系列的那个开发商,早年给 Valve 打工开发半条命系列跨平台衍生作,给起源引擎也出了一份力。今年 3 月,Embracer 将 Gearbox 相关资产出售给 Take-Two 和 2K,但唯独留下了发行业务……然后在 4 月,Gearbox Publishing 更名为 Arc Games(iii 第一次露面是3月份)。

虽然 Gearbox Publishing 发行过 Godfall 这样的冤种游戏,但实则发行阵容很强。前有从完美那边继承 Runic Games 的火炬之光系列,后有 Hopoo Games 的雨中冒险系列。也押中了遗迹系列(Remnant)大卖,还有白之旅、We happy few、Have a nice death 这样的不俗之作。在此次 iii 计划展会上与 Heart Machine 一起公布了 Hyper Light Breaker,也是未来的重点项目。


Humble Games

Humble Games 起家于 Humble Bundle,也就是早年大家热衷的 Steam 慈善包卖家之一(HB 包)。此后 HB 建立了自己的商店以及周包和月包业务。在这个过程中,HB 积累的游戏业界资源越来越多,并在 2017 年 2 月开始游戏发行业务。8 个月后整个 HB 被 IGN 收购。

不得不说 HB 发行的游戏颇具有「独立游戏」的味道,最早是帮时光之帽(A Hat in Time)发行主机版,次年就押中了传说法师(Wizard of Legend,iii 计划公布了续作),隔年浮岛冒险(Forager)都是销量百万级别的独立游戏(当年还帮助 Mega Crit 发行杀戮尖塔主机版)。其他像是Wandersong、伊甸之路、Signalis、Unpacking、Ring of Pain、Unsighted 等游戏我个人也都挺喜欢的。虽然发行业务看起来顺风顺水,但实则也暗流涌动。去年的游戏行业裁员大潮中,HB 也没能幸免。


PlaySide

PlaySide 是一家墨尔本主打娱乐业务的公司,感觉澳洲本地人应该比较熟悉。虽然他们近年自称澳洲最大的独立开发厂商(股票上讲),但实际业务属于泛娱乐类型而非这里独立游戏。像是购买和运作 Dumb Ways to Die、行尸走肉、教父等 IP,给各种媒体大亨打工,开发各种休闲手游,连 web3 和元宇宙也都掺了一脚。这次 iii 计划上展示了 2 款发行游戏:MOUSE(上世纪30年代卡通风格FPS)和射戮骑士(街机动作,可能并非标准的幸存者类或者肉鸽类预游戏),后者更是自研自发行,看样子终于想认真做「独立游戏」了。


Fumi Games

Fumi Games 是 MOUSE 的开发者。比起做游戏,感觉这家厂商更擅长制作卡通……之前在 Switch 上发布的 Kids Party Checkers 也是包含了可爱小动画,还给神笔谈兵(Inkulinati)帮忙做了些角色动画。另一款在做的 Galaxi Taxi 也是卡通风格。


Kepler Interactive

Kepler Interactive 是一家伦敦/新加坡游戏发行商。创始人 Garavaryan 来自Kowloon Nights(九龙之夜,据说募资来自亚洲,资助了上田文人,也投了 Godfall),之前在育碧、WeGame 工作,其他初始人员都从不同独游工作室出来共同创建,属于共同创建,共享资源并分别负责工作室的发行业务。2021 年 Kepler 还从网易融了 1.2 亿美元。所以从发行 Sifu 不难看出来他们对中国市场的理解。他们和 The Gentlebros 达成合作发行了猫咪斗恶龙系列(包括最新作第三部)。其他已发行的游戏包括超自然车旅、奥特罗斯、Score 都有着不错的评价。新发行的游戏燧石枪:黎明之围还和 B 站合作了独家线下试玩。


A44

Kepler Interactive 成立于 2021 年,A44 也是其中的成员。燧石枪:黎明之围就是 A44 的新作。他们之前的作品是 2018 年的 Ashen,由 Annapurna Interactive 发行。一开始说要登录 Windows 各个平台,后来被 Epic 半路杀出直接变为限时独占。


Awaceb

Awaceb 也是 Kepler Interactive 成员工作室,2019 年受九龙之夜资助开发 Tchia。游戏推迟到 2023 年发行,并且也是 Epic 限时独占,今年 3 月份刚刚登陆 Steam。


Triband

Triband 是「万物皆可高尔夫」的游戏开发商。从他们后续的作品就能看出来,工作室主打谐星风格,创意先行,制作 “comedy games”。Golf? 和 Car? 两部作品都是先登陆的 Apple Arcade 平台再转为其他平台,Golf? 当年还拿了不少奖项和提名,像是 2020 年 GDC 奖的最佳手游。


Thunder Lotus

Thunder Lotus 是灵魂旅者(Spiritfarer)的开发商,在 iii 计划上了公布了工作室第四部作品 33 IMMORTALS 的最新情报——一部33人联机肉鸽动作游戏。Thunder Lotus 之前的每部游戏都没有走相同的风格,第一部游戏 Jotun 是偏 boss rush 的动作类,第二部 Sundered 是 metroidvania。这两部游戏都是创始人 William Dubé 一手推动的,在众筹、开发和上架售卖上开展顺利,(长尾)销量很不错。

从第三部游戏灵魂旅者开始,Thunder Lotus 没有再一次选择众筹,而是和 Kowloon Nights 签约获得充足的资金(九龙之夜详见上期介绍)。这次选择了模拟经营类的路线,并请来了 Nicolas Guérin 作为创意总监。Guérin 之前在育碧数年担任刺客信条系列游戏的关卡设计,这次首席文案也是他。配合 Thunder Lotus 一如既往的动画强项,仅 Steam 平台售出 100 多万份,名利双收。

目前 33 IMMORTALS 处于测试招募阶段,有兴趣的可到官网查看。


Thorium

Thorium 是一家仅有 3 人全职的小型工作室。之前的作品是 UnderMine,在 iii 计划上公布了 UnderMine 第二部。对于独立游戏来说,延续成功是保守而又稳健的做法。工作室的设计和开发两位大哥都入行已久,并在战锤 40K、英雄连、FIFA 等大型项目工作过数年,经验非常丰富,算是出来想做些自己喜欢的东西,并且还成功了。我没玩过 UnderMine,这里也不便介绍了。


Realm Archive

Realm Archive 是幸存者类游戏 Death Must Die 的开发商,在 iii 计划上公布了游戏新内容。Realm Archive 非常神秘,官网上除了包括 DMD 在内的两款游戏外没有任何介绍信息,Credits 中也没发现成员过往履历,只知道美术是 Aganchyan 家兄弟俩。Kamifuda 是工作室的第一款游戏,类型是打牌+视觉小说,卖了几千份挺不错的。Death Must Die 也算是幸存者类偏美术比较优秀的一边,玩法还有很多空间待挖掘。乘上了幸存者这辆班车,也是属于闷声发大财。


Assemble Entertainment

Assemble Entertainment 是一家德国发行商,给人一种既熟悉又陌生的感觉。虽然发行了诸多游戏,但是貌似只有末日地带(Endzone)和 FAR:Lone Sails 有点眼熟,顺便一提小小贴纸铺也是他们发行的。所以基本能看出来,Assemble 最擅长发行的是文字 AVG 和模拟经营类游戏。

Assemble 最早是在 Steam 发行了 Leisure Suit Larry 系列 AVG。这个系列可以说是欧美黄油 AVG 鼻祖,由雪乐山开发,就是那个制作了世界上第一款图形文字冒险游戏谜之屋(Mystery House),又做出了国王密使的公司。虽然开发阵容豪华但依然是黄油……后来 Assemble 发行了数款游戏 AVG 和模拟类游戏,但销量很少有过 10w 的。直到他们自己组建了工作室 Gentlymad Studios 开发出末日地带才算大卖。

Assemble 每年会组织德国游戏开发者大会(GermanDevDays),该项目作为促进文化创意产业机构也得到了公司所在地黑森州的政府支持。说起来他们的 CEO Stefan Marcinek 是男同,并且曾在 2021 年公开表示你不接受 LGBTQ+ 群体就不要买我们家的游戏🤔 Assemble 的 slogan 是 SAVING THE WORLD, GAME BY GAME,和米哈游的 OTAKU 拯救世界不同,Assemble 也有自己的想法。


Digital Sun

Digital Sun 是夜勤人(Moonlighter)的开发商,这款并没有好评如潮的游戏在 11-bit 的帮助下有着百万级别的销量。第二款游戏搜魔人(The Mageseeker)是拳头的LOL宇宙改编游戏之一,主角是塞拉斯。和夜勤人一样有着相当不错的销量但是评价并非一片大好。但这两款游戏在商业上对于独游工作室来说已经相当成功,以至于工作室有 30 人之多。iii 上他们展示了新作灾厄堡垒(Cataclismo)的最新情报。这是一款 RTS 塔防游戏,emmm不在我关注的领域内。


ColePowered Games

ColePowered Games 是凶影疑云(Shadows of Doubt)的开发工作室,本次 iii 上也展示了该游戏最新情报。这游戏特色是侦探模拟+城市沙盒,虽然后期没有达到沙盒化随机耐玩的预期,但独特的品类使之独树一帜(黑色洛城后还有什么?天堂岛?)至于工作室,也基本由 Cole Jefferies 一个人打理。 从领英上看 Jefferies 应该是一毕业就开始做独立游戏(太勇了),早年做了一些全平台 flash 游戏。他的工作室官网就是他的开发日志博客,内容输出很稳定。


Drop Bear Bytes

Drop Bear Bytes 是一家来自的澳洲的开发商。他们目前制作的唯一游戏是在 iii 上展示的 Broken Roads——一部叙事上很有野心但是现实很骨感的游戏(目前 Steam 上褒贬不一,42% 好评)。Drop Bear Bytes 由 Craig Ritchie 于 2019 年创立。当年 1 月份开始开发 Broken Roads,10 月份发布了预告片,获得了澳洲多个州的艺术款项支持(原本游戏背景知识末日废土,后来改为了后末日澳洲,游戏中一些特色配音就是找的原住民)……直到 2023 年 6 月才有第一个 demo,6 个月后该游戏第一个发行商 Versus Evil 倒闭了(发行过 The Banner Saga 系列和永恒之柱系列)……随后 VE 的母公司 tinyBuild 接手该游戏发行。

就像很多独立游戏开发者一样,我很好奇 Craig Ritchie 是怎么度过这几年的。因为看游戏阵容其实时有几位大咖的:叙事总监 Leanne,参与过看门狗2、杀出重围系列和神偷的脚本;Colin McComb 这位更是重量级,参与过异域镇魂曲和辐射2的设计、博德之门3和消光2的叙事。算上其他拓展外包的 staff 人数能有上百人。在一档播客中 Craig 透露,最初资金来源于自己,家人、朋友和天使投资,见了很多投资人也被拒了很多次。现实是这个成本以目前游戏评价估计很难回收了。


Fireshine Games

Fireshine Games 是地心护核者、凶影疑云(上期介绍过)等独立游戏的发行商,同时也负责过匹诺曹的谎言、狙击精英5、战锤40K:暗潮和波西亚时光的主机版发行。这家来自伦敦的发行商前身名为 Sold Out Sales and Marketing Limited(这名字…),在 04 年与 The Producers 合并成Mastertronic 集团(事实上源头更复杂),涉足了很多类型领域的游戏,大到辐射3、上古卷轴3小到不知名游戏都有。2015 年破产后,负责独立游戏发行的 Sold Out 厂牌拆分出来,从 Team17 和 Rebellion 拉来不少人,并在 2022 年重组更名为 Fireshine Games。目前完全发行的游戏中,地心护核者和凶影疑云都取得了相当不错的销量成绩(特别是前者)。主机版应该是之前的接触实体游戏业务的经验,整体上看活得比之前好多了。


Hooded Horse

Hooded Horse 是一家位于德州的游戏发行商(不在山东)。成立于 2019 年,专注于策略、建造模拟类游戏发行,前阵子因发行 Manor Lords 风头正盛。去年发布了风暴之城也是百万级销量,而且好评率高达 95% 以上。早年发行的工人与资源和地下蚁国积累了不少资金,且好评率都不低。熟悉这类游戏的玩家对 HH 肯定不陌生。

那位操着流畅中文普通话的老板 Tim Bender 有着斯坦福法学博士和哈佛东亚区域研究硕士学位,名副其实的学霸。他曾在麦肯锡当过一年助理顾问,后来在 P 社大佬 Shams Jorjani (原 CBDO)的投资下担任 HH 的 CEO,他也是游戏风投 Griffin Gaming Partners 的 Operating Partner,对待 web3 游戏也是亲临一线(呃)。但不得不说,HH的独游眼光相当独道,对游戏开发行业也是一腔热枕。他们放弃了传统发行商 100% 利润回收的协议,而是专注于合作,帮助开发者找到受众。由于策略和建造类游戏大多有着独特而又忠诚的受众,对大部分 3A 游戏发布的抗风险能力比较高。但唯独博德之门3发布时,HH 的 CFO 觉得拉瑞安这款旷世之作肯定影响了他们的销量。


Quite OK Games

Quite OK Games 是一个年轻的游戏开发工作室,成立于 2021 年。工作室一共三名成员,两名来自 The Farm 51(做了 Get Even),特别是美术 Michał Kubas 还在 Techland(做了消逝的光芒系列)待了 4 年做到了 Lead 3D Artist。在 iii 上展示了他们的模拟建造类游戏肋萨拉:顶峰王国(Laysara: Summit Kingdom),由 Future Friends Games 发行,目前 82% 好评率。


Stunlock Studios

Stunlock Studios 是一家瑞典游戏开发商,据说一开始是由一群喜欢游戏的学生创建。这家公司以多人游戏起家,后来因战争仪式(Battlerite)大火,被腾讯看上,陆陆续续买了千万股权成了人家最大股东。在战争仪式的吃鸡模式(Battlerite Royale)没落后,大家本以为这家厂商没活了,结果夜族崛起(V Rising)又火得不行(生存建造分类受众真的离谱)。鉴于可能有腾讯的资方干预,我不确定他们来 iii 做什么……


Passtech Games

Passtech Games 是鸦卫奇旅和无间冥寺等游戏的开发商,由 Sylvain Passot 于 2012 年在法国里昂成立。Sylvain 是老程序员了,自己一直作为开发 lead 参与作品。他们曾经还和 OSome Studio 合作开发过一款游戏引擎 OEngine。

Passtech Games 前四款游戏都是 Focus 发行的,2021 年被 Nacon 收购,发行商也随之更换。Nacon 是法国一家老牌的游戏主机代理和游戏分销公司,最早是在欧洲帮世嘉卖 Dreamcast。后来买买买游戏工作室,包括 Daedalic 这样重量级的都被收入囊中(然后就做出了魔戒:咕噜,最后直接不做游戏了唉)。Nacon 的收购一般带有很强的垂直性,Passtech Games 大概率也会一直专注肉鸽类型游戏做下去。


其他

剩下最后三位产商,Northplay 之前是专注于手游,iii 上新作 Dinolords 由 Ghost Ship Publishing 发行(深岩银河的发发行业务,之前也介绍过)。Ishtar Games 前身是 CCCP,开发了 Death in 系列,The Last Spell 应该是他们最成功的作品。这对发行商 The Arcade Crew 也是一样,它是 Dotemu 的发行子品牌,Dotemu 后来被 Focus 收购。鉴于这三家信息过少,这里就让我草草收尾吧。

第一届 iii 计划的 40 家厂商介绍就到此为止,期待第二届带来哪些惊喜。说不定有些游戏到第二届还没发售。


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## #86 做一款 Delta 模拟器皮肤 Author: fenx | Link: https://fenx.work/make-a-delta-simulator-skin/ Article:

TL;DR: 我做了一款 Delta 模拟器的 NDS 皮肤,适配了灵动岛以及多项优化体验。目前只支持全面屏 iPhone 型号。可以在此下载:

yourOldNDS
标准版
download-circle
yourOIdNDS4touch
横屏触控优化版
download-circle

或者 Google Drive / 腾讯微云(密码cw69st)。详情见下文。


自 App Store 放开模拟器应用政策后,Delta 已被下载百万次。我也下载玩了几款游戏,体验很流畅。自带皮肤已经比较精美,但美中不足的是没有为灵动岛做适配。我最近想玩 NDS 上的超执刀系列,默认 NDS 皮肤会遮住上屏幕一部分。总觉得有点不舒服,看了下官方文档,决定自己做一款 NDS 皮肤。

皮肤展示

Delta 模拟器 NDS 竖屏皮肤

竖屏皮肤我让屏幕下移了几个像素,稍微放大了十字键(D-Pad)和 ABXY 按键尺寸,由于皮肤可以忽略 iOS 的安全区,所以我将菜单键挪到了上方,减少误触。右上方加如了一个彩色 Delta logo,使用了 SNES 的 ABXY 颜色。

Delta 模拟器 NDS 横屏皮肤

横屏皮肤主要以「人体工程」为主,怎么舒服怎么来。由于 NDS 是双屏输出,按键也不能太小,所以导致屏幕空间不购大。上方款式作为突出主屏信息用,并加入了快捷存/读档按键。下方则突出了触屏信息。某些游戏(比如超执刀)主要玩法以触屏为主时,就可以切换这个皮肤来玩。

皮肤的设计源文件已发布到 Figma 社区

使用皮肤

皮肤文件格式是 .deltaskin,传到 iPhone 后直接使用 Delta 打开。

打开设置,找到了 controller skins 项,选择相应平台,点击要更换的横屏或竖屏皮肤,在选择刚才安装的皮肤即可。

皮肤制作

皮肤制作很简单,出完设计图,然后再映射键位位置信息即可。详情可见文档说明

……

懒得看的话,我在下文简单说明下。

以官方皮肤文件为例,将后缀改为 .zip 直接解压,得到如下文件列表:

info.json
iphone_edgetoedge_landscape.pdf
iphone_edgetoedge_portrait.pdf
iphone_landscape.pdf
iphone_portrait.pdf

这里去掉了 iPad 一些视图文件,只留下了 iPhone 端的。info.json 里面是键位映射信息,下面 带有 edgetoedge 的是全面屏的横竖屏皮肤贴图文件,不带此后缀的是非全面屏 iPhone 的贴图文件。定制皮肤要做的就是,按照相应尺寸设计文件导出,然后在 info.json 映射键位。

皮肤设计

皮肤的设计整体上还是很自由的,但要遵从 iPhone 设备尺寸和原主机屏幕的尺寸比例。以此皮肤为例,全面屏的比例我按照 375*812 设计,如果要导出 png 格式的话,就导出为 @3x 大小。NDS 屏幕为两块 4:3 的屏幕,合一起输出比例为 2:3,也就是 256×384,皮肤的屏幕尺寸也尽量保持整数缩放。

除此之外还需要注意的是,按键与按键之间、按键与触屏之间不要重合。除非是想故意做成一键按下多按键的效果。

键位映射

位置数据只需要两个,左上角坐标和键位区域的长宽尺寸,Figma 中可以轻松查看。如下图。

十字键和 ABXY 键位映射的区域

十字键是一整个区域,ABXY 是单独的按键区域。前者 json 中格式为:

{
  "inputs": {
    "up": "up",
    "down": "down",
    "left": "left",
    "right": "right"
  },
  "frame": {
    "x": 15,
    "y": 500,
    "width": 128,
    "height": 128
  },
  "extendedEdges": {
    "top": 15,
    "bottom": 15,
    "left": 15,
    "right": 15
  }
}

Delta 这样处理是为了与摇杆实现方式通用,也可以用于定位八向(斜上斜下)的键位映射(所以你可以给任何主机装个摇杆)。extendedEdges 顾名思义是拓展边缘(图中绿色部分),在空间足够的情况下,用于额外拓展键位区域,特别是用于激烈的按键交互时,稍微按歪一点也能触发。不同键位的拓展键位应从实际出发,比如 ABXY 的拓展可能更适合各方向的延展。

除此之外文档还列举了几个额外的功能按键:

quickSave: 即时存档
quickLoad: 即时读档(最近储存的)
fastForward:长按加速
toggleFastForward: 切换加速
---
Multiple Inputs: 映射多键

屏幕的映射也是同理。Delta 还支持苹果的 CoreImage Filters 库,你可以随意调用里面的参数赋予屏幕效果。有一个 macOS 应用叫 Filter Magic 里面举了每个方法的例子,有时间可以研究研究。

在 json 头部可以开启 debug 模式查看实机中键位映射是否准确。

打包皮肤

填写映射参数和皮肤名字后,将所有文件打包成 .zip(不包括文件夹),然后后缀重命名为 .deltaskin 即可安装使用。

## #85 偷窥 Ferret-UI Author: fenx | Link: https://fenx.work/peep-at-ferret-ui/ Article:

为什么说「偷窥」呢?本人既不懂模型技术,也未紧跟 LLM 潮流。不做论文分析,单纯以看一篇文章的角度,摘记吐槽二三。可能有错误,不吝赐教。

阅读原论文

自我介绍

Ferret-UI 是专为理解用户界面用的多模态 LLM,能够识别一些界面元素,感知交互以及一定的推理能力。

Ferret-UI 任务执行概览。使用了App Store 商店截图

上图能很清晰地表示 Ferret-UI 的能力,包括:

但就这些功能来看,可在测试和无障碍中大放光彩。未来加强对多界面的识别度提升,用自然语言生成快捷指令将大大提高系统使用效率。

Anyres

Apple 团队不想让 Ferret-UI 懂代码,只想让它做一名单纯的设计师——只从视觉去理解 UI。之前发布的 Ferret 和其他 SoTA 模型一般都只擅长识别自然图片,对移动端 UI 这种细长比例的图经常眼前一黑。而且 UI 图中经常包含 caption 和图标等又小又抽象的低分辨率区块,在识别和定位上都差强人意。

有问题就要解决,针对分辨率这里,团队提出了基于Ferret 的 Ferret-UI-anyres 架构:

Ferret-UI-anyres 架构

与 ScreenAI 采用的基于输入图像的形状和预定义的网格分块技术 Pix2struct 不同,anyres 这里先直接挤压图片(到正方形)通过预训练的图片编码器和投影层得到一个图像特征,然后将图片分成 1 x 2 的网格裁剪成两个 sub-image 获得额外特征(上图左侧流程)。当前的全面屏界面大多是接近 1:2 的比例,贴合实际。

Visual Sampler 是一种独特的混合技术,用来管理不同区域的连续特征,方便 LLM 处理数据。一切为了 LLM!

Text Embeddings 大概就是猛灌专家知识,从零开始学 UI!以及定义一些任务,任务的数据从之前的 Spotlight 借鉴(也是 Google 的)并重新用 GPT-3.5 Turbo 格式化。原论文第 4 部分详细描述了这些任务。

制定任务

基本任务生成流程图

如上图,检测器先列出了屏幕中的所含元素,从 0-17 编好序号,这些组件的类型、文本以及坐标都被记录用于后续的基础任务。

坐标数据指的是区域内左上角和右下角的坐标值,如下图格式便是 [700, 230, 1700, 730],比较符合设计直觉。

坐标参数说明

然后利用 GPT 的 QA 列举 widget 塞进一个采样里。这里的 widget 并非 HIG 中的小组件, 而是各种组件(components),很奇怪这里的术语歧义。

Listing 这里让我想起了正在用的 Tana 笔记,对于复杂格式的数据,Tana 也鼓励直接使用 AI 来按照一定格式输出整理。

根据右上角的 overview ,可以看到模型把每一类型数据(文本、图标等)都塞进了不同采样,为后续 QA 做准备:

这里面的采样分别对应上述的识别和定位基础任务,每个采样对应的任务同样使用了 GPT-3.5 Turbo 扩写。数据类型划分的很简单,毕竟我们做 UI 的成天就那点事。

推理时间

高级任务生成流程图

虽然识别和定位任务已经优于 GPT-4V,但没有推理能力的话,对界面的理解还是不够深,一个文本既可以是按钮也可以是图标也可以是组件。高级任务这里是站在 LLaVA 的肩膀上,额外使用 GPT-4 新建了 4 个任务,并专注搜集训练 iPhone 界面数据。这 4 个任务分类为:

还是看图容易,图示上首先将检测到的元素坐标格式化为百分比定位,然后搭配一个公共 prompts(You are an AI visual assistance that can analyze mobile screens)、多个各种类型任务专用 prompts 以及专为对话准备的示意文本,统统交给 GPT-4,所以实际 QA 环节还是 GPT-4 在发挥哈,输出的源数据交回给 Ferret 处理。结果如上图右下角展示。在论文最后的附录中还能看到更多案例。

附录中提供的任务执行结果:在 Apple Store 界面首页执行任务测试

可以看到一些小字也识别的很精准。而且理解界面的过程完全是用户角度的,比如它会从商品图、Accessories 文案和购物袋图标识别推理出这是一个购物应用界面。🤔试想一下我们可以建立多个不同用户类型的心智模型,这些模型代表不同的画像,对界面的「观感」和理解也不同,这样设计早期阶段就能获得真实而又多面的反馈角度,商业化的名字我都想好了,就叫 Jury AI 吧。

数据集

毕竟是要公开的论文,Android 界面也要研究的。团队使用了 Rico 数据子集。Rico 是一个公共的用户界面数据集,囊括了大量 Android 截图和元数据,甚至还有交互路径和动画的数据,十分详细,就是老界面挺多的……里面有一项数据集叫 UI LAYOUT VECTORS,界面布局向量?它是这样的:

根据红蓝位置召回相似布局的界面

看到这图顿时一幕幕场景涌上心头——需求方发过来一堆风格和布局参考图,我一看这不都是同一种设计吗?对方还非常不解,这又是黑的白的又是彩色的,哪里一样呢?现在可以继续回答对面说,「你找的图布局向量都是相同的」,然后挥一挥衣袖扬长而去,瞥着对面什么时候上来追问「啥是布局向量啊」。

iOS 的数据集使用的是 AMP 一个随机子集。AMP 是苹果团队之前 Screen Recognition: Creating Accessibility Metadata for Mobile Applications from Pixels 项目产物。这个项目的目的是如何从 UI 中自动生成辅助功能元数据,来提高无障碍可访问性,毕竟目前的 VoiceOver 还只是无情的读屏机器。所以我一直有强调:

无障碍也可以提高无障碍人士的体验

Screen Recognition 项目结果是成功的,9 名视障用户甚至可以使用之前用不了的 App(很多 App 没有为无障碍标签做优化,比如大部分手机游戏)。一位参与者对发现「滚动条」很吃惊,因为这是他们第一次在 UI 中感受到这个组件存在。但当时 Screen Recognition 无法执行推理任务,不能识别一些图标的含义,以及执行一些手势交互。我不确定当年 iOS 14 VoiceOver 升级了一系列「识别」功能是否与这个有关。时光境迁,接力棒传到了 Ferret-UI 这边。

Android 和 iOS 界面训练和测试数据

比起还在流行汉堡包菜单的 Android 界面数据集 [1],iOS 这边现代了一些,375 宽度和 414 宽度都有包含,并且包含了横屏界面截图。右侧任务数则是刚才提到的基础、高级两种任务,与之前的 Spotlight 流程任务相对比(后面会有说明)。

[1]: 实际上都是 1080*1920 分辨率截图,只不是指定了 bounding box 是 1440*2560。

结论

Spotlight、Ferret、Ferret-UI-base、Ferret-UI-anyres 和 GPT-4V  测试结果对比

这里面 Public Benchmark 作为和 Spotlight 对比,团队也让 GPT-3.5 Turbo 对应生成了一系列 prompts 交给 Ferret-UI。基础 prompt 如下:

结果显示 Ferret-UI 在这三项任务重取得了相当很有潜力的成绩,GPT-4V 则对此一脸懵,但在高级任务中,GPT-4V 背靠 GPT-4 这座大山依然占有不小的优势。而 Ferret-UI 对比 Fuyu 和 CogAgent 两位前辈已有不少进步。

Ferret、Fuyu、CogAgent、Ferret-UI-base、Ferret-UI-anyres 和 GPT-4V  高级任务测试结果对比

在消融实验中,对小号、手写和被挡住的文字识别依然较为准确。高级任务中对比 Spotlight 甚至略逊一筹,团队推测是训练数据不足,以及需要更复杂的 UI 术语理解。基础任务中的识别和定位也并非完美,一些复杂的组件结构和多次出现的文本都会扰乱模型的判断。”UI detection model is a bottleneck.”,检测器也是一大改进点。详细结果总结还是请看原论文。总之,Ferret-UI 还是相当有潜力的。


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## #84 从删除线到默默无闻 MVAR Author: fenx | Link: https://fenx.work/from-strikeout-to-obscure-mvar-table/ Article:

先看看删除线发生了什么:

苹方和 SF Pro 字体在 Adobe Illustrator 和 Figma 中的删除线样式

苹方和 SF Pro 都是界面设计时常用的字体。在 Figma 中添加删除线样式时,上图中可以看到苹方和 SF 字体的删除线位置不一,一个偏下一个居中。原因是,删除线属性由字体内的信息 OS/2 表内两个参数控制:

<yStrikeoutSize value="120"/>
<!-- 删除线尺寸 -->
<yStrikeoutPosition value="620"/>
<!-- 删除线位置 -->

yStrikeoutPosition 的值是相对于 baseline 的高度,比如苹方字体(PingFang.ttc)的 yStrikeoutPosition 是 200,x-height 是 600 [1],所以苹方的默认删除线处于 x-height 1/3 处位置而不是近似 1/2 处。而像 Adobe illustrator 等软件,虽然和 Figma 同用 Harfbuzz 塑形引擎,但 Adobe 有着自己的一套渲染规则。如上图左一,同为苹方字体但删除线位置却不同。

[1] 苹方中,全身字宽单位是 1000,并非现在新新字体常见的 2048。

这会引出很多问题,比如:

本文的目的就是探索这些问题的背后还隐藏了什么信息。


修正删除线位置?

如果使用默认删除线属性,那么不同应用环境之间删除线的位置和粗细也会不同。Henrique Beier 在六年前写过一篇文章,对比了不同软件中下划线和删除线的位置和粗细,结果很让人意外。虽然六年后各大应用环境里做了不少统一,但是正如开头所提,差异依然存在。

The state of underlines and strikethroughs - Harbor Type | Fonts made in Brazil
Some weeks ago, while developing a custom typeface for a client, I was asked about adjusting the position and thickness of the underlines and strikethroughs. I knew it was possible to customize these values in Glyphs, Fontlab or any other font editor. But honestly, I didn’t usually bother to configure them because I knew they weren’t really enforced by most graphic software. This was a custom project, so fine-tuned underlines and strikethroughs could potentially save my client from a few headaches when typesetting documents. With that in mind, I decided to investigate further.

一般来说,像苹方那样看着默认删除线位置不爽。设计师可以直接划一条杠,开发就要想方设法再画出一条线。以前端为例,无论是 <s> 还是 <del>,实际浏览器都开始用 text-decoration-line调取字体内信息渲染。如要自己划线——

说起手打 Unicode,macOS 自带了 U+16 输入法,只需选择后,按下 option 键输入几位万国码,松开 option 键后即可完成输入。其他系统可详见这篇文章

应该在什么位置?

我没有接受过专业的字体设计教育,也还没有读到如何摆放删除线位置的文章。我的猜测是,适用于正文的字体,删除线的预期出现位置应该在 x-height 垂直居中处。为了视觉平衡,实际删除线位置会高于 x-height 一半。且粗细小于字干(stem),与字横(bar)相当。

为了验证,我在 Coze 上建立个 bot🤖 [2],让它帮我写个 python 脚本,需求是识别一个文件夹内多个字体的 x-height 和 yStrikeoutPosition 数值,将其除以全身字宽 ,并导出为 csv 文件。font tools 速度拖进 Font Table Viewer 快多了。最终结果我汇聚到了飞书文档

各个字体的删除线相对位置百分比分布散点图
各个字体的删除线相对位置百分比折线图

如图我选用了一些经典的开源、企业和古典字体,以无衬线体居多。可以看到大部分字体的删除线位置居于 x-height 的 50%-60% 处,符合上面推测。

另外一提,OPPO Sans 的 x-height 有很大问题,数值上比实际 x-height 小得多,所以这里有很大偏差……Palatino 的删除线则低到了脚下,都是比较极端的。Optima 则是没有定义 x-Height 参数,自测得知。

[2] 随后把这个 bot 布置到了 Discord,辅助完成本文。非广告,白嫖 GPT-4 (8K) 还是很爽的。

bot 在 Discord 里向文章读者打招呼

可变删除线?

除了删除线位置,尺寸粗细(yStrikeoutSize)也让人在意。很多静态字体无论字重只会定义一个删除线位置和尺寸信息。这就会导致字体在很粗的情况下,删除线却很细这种情况。

Work Sans 静态字体不同字重下的删除线样式

目前很多可变字体,删除线信息仅跟着母版走。母版之外的字重,会取相近字重对应的数值,而不是随着可变轴一起插值变化。

可变字体 inter 不同字重下的删除线样式

很久很久以前,2016 年华沙,OpenType 1.8 发布,OpenType Variable Fonts 归来,其中一个特性是增加了 MVAR 表,用于微调各种字体参数,以及 Delta [3] 的峰值插值。原本 OS/2, hhea, vhea and post 表中固定数值都可以对字体进行微调,其中就包括删除线位置和尺寸。你可以在下方微软这篇文档中查看详细。

MVAR — Metrics Variations Table (OpenType 1.9) - Typography
Metrics Variations Table (OpenType 1.9)

[3] Delta 可理解为可变字形的矢量锚点所变化的路径,如下图。

OpenType Font Variations Overview 中的 delta 介绍

顺便一提,目前几乎所有 OpenType 相关文章都绕不开 Microsoft Typography 的内容引用,微软虽然界面一直做得不怎么样,文档这方面还是很负责任。

但即便文档已足够详细,恕我愚钝,大多数篇章依然读起来很吃力。如果想简单了解一下 MVAR 这个小 XML 表,不妨继续阅读下去。

MVAR Table

MVAR 继承自苹果  TrueType GX 数据表中的 fmtx 表,属于可选内容,即使不存在也不会影响一款可变字体的正常使用。但如果想要解决上面提出的删除线可变位置和尺寸问题,就必须要用到它。

MVAR 能达成的效果如下面动图所示:

Design Scenes 删除线和下划线样式的可变尺寸演示

上图是我从 Inter 字体作者 Rasmus Andersson 的过往推文 threads 上下载下来的测试包。里面只有一条控制字重的可变轴,原本是测试下划线可变样式,我稍做修改加入了删除线样式,并且修复了一个 text-decoration shorthand 写法初始化的小 bug💦[4]

[4] 出问题的源代码在这里,貌似 text-decoration-thickness 写在 body 里不会生效(被继承)。

解析字体文件里的 MVAR 表,能看到:

<Version value="0x00010000"/>
<Reserved value="0"/>
<ValueRecordSize value="8"/>

<!-- ValueRecordCount=3 -->
<VarStore Format="1">
  <Format value="1"/>
  <VarRegionList>
    <!-- RegionAxisCount=1 -->
    <!-- RegionCount=1 -->
    <Region index="0">
      <VarRegionAxis index="0">
        <StartCoord value="0.0"/>
        <PeakCoord value="1.0"/>
        <EndCoord value="1.0"/>
      </VarRegionAxis>
    </Region>
  </VarRegionList>
  <!-- VarDataCount=1 -->
  <VarData index="0">
    <!-- ItemCount=2 -->
    <NumShorts value="0"/>
    <!-- VarRegionCount=1 -->
    <VarRegionIndex index="0" value="0"/>
    <Item index="0" value="[-64]"/>
    <Item index="1" value="[120]"/>
  </VarData>
</VarStore>

<ValueRecord index="0">
  <ValueTag value="strs"/>
  <VarIdx value="1"/>
</ValueRecord>
<ValueRecord index="1">
  <ValueTag value="undo"/>
  <VarIdx value="0"/>
</ValueRecord>
<ValueRecord index="2">
  <ValueTag value="unds"/>
  <VarIdx value="1"/>
</ValueRecord>

整段代码分为三部分:

由于轴数不多,Delta set 在下一个例子中再看。VarData 中储存着两条 Item 值,索引为0和1。ValueRecord 有三条,其 ValueTag 分别为:

以第 0 条 ValueRecord 为例,VarIdx 值为 1。这不是简单的10进制,而是由两个 unit16 数据类型缝合而成:

0000 0000 0000 0000 | 0000 0000 0000 0001

这两个整数值 deltaSetOuterIndex 和 deltaSetInnerIndex 分别对应 VarData 和里面 Item 的索引值,即第 0 个 VarData 里面第1个 Item 值,是 120。也就是说该字体删除线尺寸,随着字重可变轴从头到尾变换会增加 120 单位(虽然数据类型是数组但是只有1 个轴),yStrikeoutSize 从 160 升到了 280。这也是上方动图的效果。下划线属性同理。

#84 test font
download-circle

俯视 Delta Set

再来看一个多轴的例子。我找到了 Recursive 1.077 版本的可变字体文件,里面保留了 MVAR 表。Rescrive 有五个可变轴, delta-set 有22条,比较复杂。好在有 Laurence Penney 这样的好心人做了 Samsa 这个可变字体检视网站,非常 respect。

将下载好的 Recursive_VF_1.077.ttf 拖拽进 Samsa 网页中,就能看到丰富的可变字体参数。将 UI 预览模式勾选 ,你能看到字体框架的这个样式便是 delta 的可视化。

Recursive 1.077 的 A 字形,在 design space (0.8, 0.8) 附近的 Split deltas 样式

点开 delta sets 一栏能看到各种「三角形」。只调节 wght 轴(字重)到 800 (ExtraBold)后,Delta sets 中的 wght 一列开始有红线指示。

Recursive 1.077 delta sets

可以看到第 2 行变白,代表当前正在使用该 delta 参数的状态。通常该表存储在 gvar 中,该表不单独存在时,会调用 MVAR 或 HVAR 中的 VarRegionList。源文件中第 2 条是:

<Region index="2">
      <VarRegionAxis index="0">
        <StartCoord value="0.0"/>
        <PeakCoord value="0.0"/>
        <EndCoord value="0.0"/>
      </VarRegionAxis>
      <VarRegionAxis index="1">
        <StartCoord value="0.0"/>
        <PeakCoord value="0.0"/>
        <EndCoord value="0.0"/>
      </VarRegionAxis>
      <VarRegionAxis index="2"> <!-- 轴排名 -->
        <StartCoord value="0.0"/> <!-- 起始坐标 -->
        <PeakCoord value="0.6057"/> <!-- 峰值坐标 -->
        <EndCoord value="1.0"/> <!-- 终点坐标 -->
      </VarRegionAxis>
      <VarRegionAxis index="3">
        <StartCoord value="0.0"/>
        <PeakCoord value="0.0"/>
        <EndCoord value="0.0"/>
      </VarRegionAxis>
      <VarRegionAxis index="4">
        <StartCoord value="0.0"/>
        <PeakCoord value="0.0"/>
        <EndCoord value="0.0"/>
      </VarRegionAxis>
    </Region>

第 2 条轴的 delta 变量从 0 开始,到 1 结束,并在 0.6075 处达到峰值。

delta set 对应坐标

这便是上图黑色小三角的由来。每当共同调整不同可变轴时,会根据实际字型微调每个锚点的 delta 大小。比如该参数代表的是, Recursive 从 300 字重调到 800 是一种粗细变化,从 800 到 1000 是另一种粗细变化。

至于为什么 0.6075 是 800,而不是 [0.6075 * (1000-300)] + 300 = 725.25。因为在标准化的流程中,作者在 avar 表定义了映射:

<segment axis="wght">
  <mapping from="-1.0" to="-1.0"/>
  <mapping from="0.0" to="0.0"/>
  <mapping from="0.1429" to="0.2514"/>
  <mapping from="0.2857" to="0.34"/>
  <mapping from="0.4286" to="0.4286"/>
  <mapping from="0.5714" to="0.51715"/>
  <mapping from="0.7143" to="0.6057"/>
  <mapping from="0.8571" to="0.8686"/>
  <mapping from="1.0" to="1.0"/>
</segment>

0.7143 对应的是 800 也就是 ExtraBold 字重,但实际上 725.25 字重便可满足 ExtraBold 需求,所以会重新映射为 0.6075,也间接影响到坐标的定义。

当有多个轴变化并进入相应 StartCoord 和 EndCoord 之间时,就会触发多个 scalar 值,delta 变得极为灵活,相乘然后叠加便是受到多轴影响的可变字体最终形态。Delta 数量不设限。对此感兴趣的话,十分建议在 Samsa 中亲自体验一下。

调整多个可变轴时的 delta set 值

再看多轴 MVAR

之后再看 Recursive 的多轴 MVAR 表是什么样子(删去了部分代码):

<VarStore Format="1">
  <Format value="1"/>
  <VarData index="0"> ... </VarData>
  <VarData index="1"> ... </VarData>
  
  <VarData index="2">
    <!-- ItemCount=1 -->
    <NumShorts value="0"/>
    <!-- VarRegionCount=5 -->
    <VarRegionIndex index="0" value="1"/>
    <VarRegionIndex index="1" value="2"/>
    <VarRegionIndex index="2" value="3"/>
    <VarRegionIndex index="3" value="9"/>
    <VarRegionIndex index="4" value="10"/>
    <Item index="0" value="[-2, 40, 30, -2, -3]"/>
  </VarData>
  
</VarStore>

<ValueRecord index="0"> ... </ValueRecord>
<ValueRecord index="1"> ... </ValueRecord>
<ValueRecord index="2"> ... </ValueRecord>
<ValueRecord index="3"> ... </ValueRecord>

<ValueRecord index="4">
  <ValueTag value="stro"/>
  <VarIdx value="131072"/>
</ValueRecord>

stro 的索引值是 131072,拆分并补位到 32 位为:

0000 0000 0000 0010 | 0000 0000 0000 0000

前面是 2 后面是 0,即删除线位置使用了第 2 个 VarData 中第 0 个 Item 的值 [-2, 40, 30, -2, -3]。5 个 delta 值对应该 VarData 中引用的 5 条 Region 变化,这 5 条 Region 也对应着 VarRegionList 的 delta set——正如上小节所言。这里索引值是 1、2、3、9、10,对应的 delta set 是:

<Region index="1"> ... </Region>
<Region index="2"> ... </Region>
<Region index="3"> ... </Region>
<Region index="9"> ... </Region>
<Region index="10"> ... </Region>

可以看出,Recursive 删除线位置只跟字重(wght)和 Casual (CASL)两条轴有关。

所幸计算方面也是简单的加减乘除。假设把 wght 调到 900,CASL 调到 0.5,根据上一节的计算原理,5 个区域对应的 scalar 为 0.5、0.333、0.667、0.167 和 0.333。实际删除线位置变化为:

0.5 x (-2) + 0.333 x 40 + 0.667 x 30 + 0.167 x (-2) + 0.333 x (-3) = 30.997

yStrikeoutPosition 从 284 升到了 315。其他 tag 同理。

默默无闻 MVAR?

由于种种渲染和显示原因,MVAR 虽早早诞生但落地一直很艰难。

作为可变字体中一个可有可无的表,属于互联网犄角旮旯半成品的边角料,修复优先级自然不高。字体设计师见此也大都暂且搁置微调,久而久之变得默默无闻。但互联网也从不缺少怜悯之心,20212022 年都有人呼吁停止将 MVAR 拒之门外。随着 foottools 提案和更新 designspace v5,有设计师又将 MVAR 带回到字体中

MVAR 未来依然「生死未卜」,但我很期冀不要让 bug 磨灭了字体设计中的匠人精神之光辉。

附录

以下是一些探索过程中发现的其他文章,以供拓展阅读:

以上文章可通过点击这里添加到你的 Arc 浏览器文件夹,或使用其他浏览器在线查看。

另,图中所用灰色说明字体均为 Recursive,作为开源字体真的花了很多心思。


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## #83 探索 Switch 游戏字体大小 Author: fenx | Link: https://fenx.work/nintendo-switch-font-size-note/ Article:

在交互设计不为人知的角落,Nintendo Switch 掌机玩家一直饱受「有障碍设计」的折磨,其中最为之诟病的是字体大小问题。你可以看到游戏媒体都曾报道过相关新闻,Reddit 上还有一条统计字体过小的游戏清单。直到现在,玩家除了祈祷移植厂商能大发慈悲推出改善补丁外,也只能使用系统自带的放大镜(zoom)功能来暂时自救,游戏体验大打折扣,终究不是解决方案。在这块 6.2 英寸的屏幕上发生了什么?

这次以初版 Switch 的掌机模式为例,探索一下其屏幕规格所对应的界面字号规则。

字体哪里小了

我们游玩 Switch 时,和使用手机时眼睛与屏幕的距离相似,所以这里先假设复用 UI 设计经验作为评判标准。一般来说(参照 Apple 的 Human Interface Guidelines),默认字号为 16pt,小一点可以是 14pt,最小不超过10pt。由于 Switch 的屏幕素质堪忧,字体笔画较细的时候都渲染到子像素上会很模糊,所以一般最小 12pt 看起来较为友好一些。

参数上 Switch 按分辨率 720p、6.2 寸屏幕计算 PPI 为 236.87。为了直观地方便对比,使 1pt = 1px,这里先都用 1 倍的逻辑分辨率计算 PPI。

保持物理尺寸一致时,Switch 字号是普通界面设计字号的 0.77 倍 (118.44/153.81),也就是说,12px 在 Switch 上约为 9px,这便是理论上对于 Switch 而言的最小合适字号。

当然并不是界面字号都比 9 大就可以,涉及到字体在不同功能下的不同排印逻辑,实际上标准要复杂一些。

验证

接下来找几个游戏佐证一下,都使用 360p 下的尺寸测量。

首先是极为糟糕的 Need for Speed™ Hot Pursuit Remastered,下面是中文截图(来源网图)。

Need for Speed™ Hot Pursuit Remastered 游戏繁体中文截图

「车手详细资料」一行小字,有 7px,相当于界面设计中的 9px,相当小的数值。再加上繁体字笔画复杂,字重偏细(已经是 1px 粗细),所以理所当然看不清。另外看到其他玩家的菜单截图,目测字号是已经小于7px,以至于屏幕已经无法渲染出完整的字形,笔画大量丢失,非常离谱。


《塞尔达 王国之泪》游戏菜单界面中文截图

妇孺皆知的《塞尔达 王国之泪》也有轻微的问题,菜单字体大小是 9px (界面 11.7px),一眼 light 字重,处于可识别度边缘,严格来说也对无障碍不友好。

上图:《塞尔达 王国之泪》游戏设置、按键说明和加载界

王国之泪的中文很喜欢使用细斜体,上图设置、按键说明和加载界面中的字体,都和主界面一样,不是非常易读。在 Switch Lite 上可能效果更差。

《塞尔达 王国之泪》弓箭装备界面

到装备界面字号整体都大了一些,右下角装备描述字号为10px(界面 13px),勉强能看,但是因为字重偏细依然处理得不是很好,比如白色鞋上的前景文字就需要定睛细看。


另一方面任天堂第一方的本地化也喜欢使用字重较粗的综艺体(确切说是华康新综艺体,任天堂买了大量华康字体作为本地化使用),比如下面《密特罗德 究极 复刻版》的设置菜单截图:

《密特罗德 究极 复刻版》的设置菜单截图

图中右侧灰字字号为 11px(界面 14px),但综艺体字面大,字腔小,字体再不大的话一样难以阅读,「量」的底下横划都消失了。红色系背景加上红色字体,OMG。


从左到右为《星之卡比 Wii 豪华版》、《超级马力欧兄弟 惊奇》、《火焰之纹章 Engage》

像是卡比、马力欧兄弟这种界面文字信息少的游戏,字号应用比较正常。像马力欧惊奇的联机昵称字号是 11px(界面 14px),并且还会赋予外描边样式强化对比度。右一图的火纹 Engage 角色信息表格字体为 10px(界面 13px) ,也算及格。

《火焰纹章 Engage》战斗中截图

在火纹 Engage 的战斗界面中,最小字号为底部「物攻」一类 8px(界面 10px),也是属于较小的字体,但是在布局关系上整体找到了一种平衡。

而上一代的火纹风花雪月就没有那么「好运」了,如下图我在 Resetera 论坛找到的一张截图:

《火焰之纹章 风花雪月》食堂用餐提示界面截图

图中英文为 7px(界面 9px)字体,明明框内还有很充足的显示区域,依然使用了小号字体,可能从一开始就没有注意到 i18n 背后的无障碍设计。

搭建 Switch设计界面

涉及到现实物料尺寸的,在 Adobe Illustrator 或 Photoshop 改变单位便可搭建出框架,能所见即所得地看到界面上各元素的实际大小。但如果在 Figma 中,我们需要计算屏幕 PPI 比例做一下缩放。

上面提到,720p 下的 Switch PPI 为 236.87。以我使用的 16 寸 MacBook Pro  为例,不算「刘海」的逻辑分辨率为 1728*1079,对角尺寸为 16 寸,PPI 为 127.33。所以当我们在 Figma 建立一个 1280*720 的 frame 后,将屏幕缩放调整至 127.33/236.87 ≈ 54% 即是 Switch 在屏幕上呈现的实际大小。

如果你像我一样调整了 Figma 的界面缩放(interface scale),比如我调整到 110% 缩放,那么还需要再除以该比例,也就是 127.33/236.87/1.1 ≈ 49% 的画布缩放。

Figma 中的Switch 屏幕示例。实际缩放为48.8%时尺寸更准确,但 Figma 不支持

Switch 插入底座后的主机模式大多数情况为 1080p 渲染,这样仿佛又看到了界面设计中的 @2x、@3x 等缩放因子。如果想在 @1x 下做设计的话,缩放再乘 2 即可。640*360,这和以前 16:9 的手机界面设计倒是差不多。

值得注意的是,像异度神剑 2 这种吃性能的游戏,主机模式下是 720p 的分辨率,掌机模式是 360p 到500p 左右的动态分辨率。但字体部分似乎是独立渲染,无论场景多么模糊依然保持字体的清晰度。所以异度神剑 2 无论是字体布局排布还是可辨识度都做得很不错。顺便一提,到异度神剑 3 时游戏似乎自带了上采样至  720p 的技术,但界面字体方面不如上一代。

总结

之前我一直连着电视在玩 Nintendo Switch 的主机模式,所以这个方面没太在意。直到前阵子我去帮忙改善一款 Switch 游戏的界面才发现这个现象是多么普遍和无奈,即便是在 Switch 服役末期!听说任天堂开发文档中默认不提供 Pro 手柄的适配,那么像是界面这些细枝末节的东西可能更不会提到。我使用了上述方法在 Figma 中完成了游戏界面体验的优化,效果不错,特来分享。

对于游戏而言,即便是无 HUD 场景也需要玩家读取界面上的信息,所以在诸多信息层级之下,一些字号只能作为妥协后的产物。但妥协不代表无底限的妥协,看看 3DS 时代的怪物猎人XX 截图,400*240的分辨率下实打实的文字信息优先。

传闻中的 Switch 2 代机型大概率在今年公布,待上手体验时我也会着重注意字号细节是否有所提升🧐。真诚希望所有厂商有余力的情况下,多注重掌机模式的体验吧。

## #SP1 过去那些启发 Author: fenx | Link: https://fenx.work/bygone-inspirations/ Article:

新年快乐!去年在找工作时受到不少热心人的帮助,也给一些人带来了麻烦……另外上次邮件忘记设置文章 url,导致从邮件打开会 404,都是我的疏忽。在这里容我先传达诚挚的谢意和歉意。

我对年终总结没什么想法……不过我想可以写写,受到哪些作者的启发。寥寥数笔,无法概全。优秀的作者有很多,本文未提到之处也有很多优秀的作者在耕耘。


Design Scenes 名从何处而来已不记得,只是这个写作想法脱出于 Designer News(后简称 DN)。彼时刚入行不久,见惯了各种 showcase 网站的甜言蜜语,DN 评论区的尖锐让我着迷,原来大家的设计思维也有这么多异化的一面。此后我也养成了以「批评角度」入主的习惯,但始终怯于表达——

后来我读了艾略特的《批评批评家》一书,不仅对文学评论界有了一个模糊轮廓,更主要的是从文学大家的思维中找到了可依赖的角落。艾略特开头引用:

批评,或可套用弗·赫·布拉德利讲形而上学的说法,就是“为我们靠直觉相信的东西勉强找些理由,但找这些理由本身也就是直觉”。

从艾略特的这几篇演讲中,可以看到其背后的一些「朴素」。他觉得批评这东西也是直觉向的,也是个人并带有私心的,也有虚荣、傲慢一面,也会在意他人的评价。同时他也认同文学批评的必要性,是「文学鉴赏标准的转变速度的调节嵌齿轮」:

当嵌齿梗塞住了,评论者牢牢地嵌在上一代人的鉴赏标准里,机器必须无情地加以拆卸和重新安装;当嵌齿松开了,评论者接受了新鲜风尚作为衡量文学作品优良的充分标准时,机器必须停住,收紧。

当机器发生了这两种毛病的任何一种时,毛病的效果是在人们当中制造分裂:一种人在任何新事物中看不出任何好处,另一种人却除了新事物外,在任何别的东西里面看不出任何好处。由于这个缘故,旧事物的古老和新事物的怪诞、甚至于招摇撞骗的性质,二者都加剧了。

这里将文学鉴赏替换为设计鉴赏貌似也没有违和。我们的职业随互联网一起发展,从平面设计大河中分支生出一条充满工业化标准的溪流。为了提高效率和减少交互门槛,界面设计追求同化、消减个体表达,建设统一的秩序美感。更不用说前有绩效数据施压,后有可行性的难堪,我们的呼吸空间其实很少——而这里的批评,我希望更能提供多一些讨论空间,多一些道路。说你想说,做你想做。

批评批评家
《批评批评家:艾略特文集·论文》收录了艾略特从1917年开始到1961年间的9篇评论文章和演讲稿。这位在文学界享有极高声誉的评论家有着独到的眼光,对众多文化现象进行了评论,如对文学批评的运用的论述,另…

艾略特总结了四种批评家:

将文学批评替换为设计批评,再上升到互联网,我们能发现很多这四种类型以及之外的作者。除了启发,其实也饱含羡慕。

Money Staff

第一次得知 Matt Levine 是在 Elon Musk 上演 Twitter 大戏时期,作为杰出的「马斯克观察家」,Matt 的 “Oh Elon”栏目提供了不少有趣而独特的视角,也因此关注了 Money Staff。就像 Bloomberg 其他 newsletter 一样,主站新闻虽然需要付费订阅,但是 newsletter 可以(在邮箱里)免费阅读。

阅读数期后,我发现了 Matt 的游刃有余。他总是以一个非常简单易懂的类比开头,再引申到新闻事件本身的逻辑。这让很多信息繁多的金融类新闻阅读起来非常轻松有趣,易于消化。这背后抽象出来的类比模型离不开 Matt 丰富的金融知识和从业经验,以及画龙点睛的幽默感。所以要写出一篇有趣的文章,要付出比表面更多的精力去理解内容本身和拓展,厚积薄发。

The Eclectic Light Company

另一种角度,The Eclectic Light Company 的 Hoakley 对 macOS 的了解堪称入木三分。但互联网上并非总有 macOS 新闻,每周五天都在写 macOS 内部细节,娴熟的技术,敏锐的观察和持久的热情缺一不可。

Web Curios

当然并非每个人都是技术大能,行业资深,这些从来都不是写作的门槛。Web Curios 是一份分享各种奇怪链接的周刊。它的作者也叫 Matt。Web Curios 的内容大都处于互联网边缘,和 Naive Weekly 一样,对互联网文学情有独钟,挖掘出那些不为人知的网站,对不爽的事情直抒胸臆,感觉 Matt 总有聊不完的天。这导致每周的 Web Curios 内容非常充实,上万字词,阅读时间总是需要至少半小时。Matt 解释了他为何要这么做:

But, to be serious for a second, it’s because the web is obviously horrible and has ruined us as a species, but it is also amazing because of all the incredible, strange, mad, odd, obsessional, creative, interesting, sad, terrifying, stupid, evil, cruel, selfish, venal, poignant, funny, disturbing and utterly human content that people put on it. I think it’s A Good Thing that there are some places online that help spread the word about some of all of the amazing stuff that exists out there, outside of the walled gardens of many people’s online experience. You don’t get this shit on Insta, is what I’m saying.

至于读者该怎么阅读,他的观点是,请把 Web Curios 当作一场储备过剩的自助餐餐厅。厨师在后厨源源不断生产内容,至于谁来消费,如何消费都无所谓!来者皆是客。

Built for Mars

我很喜欢 User Onboarding 的内容形式,用幻灯片的方式分析产品的引导流程。讲述方式也很轻松幽默,充斥着 meme,后来停止更新,继而转型做其他事情了。所幸后继有人,Built for Mars 便是之一。

Built for Mars 将这种形式不再局限于 onboarding,同时流程保持单一任务不复杂。此外还增加了复盘总结内容,提炼知识点有助于消化。开通了 UX Bites 栏目来搜集设计细节案例。我之前大多数 app 开箱体验文章都是这个思路。

最近 Peter 推出了付费制度,他的第一件事就是发邮件告诉老用户“你不必付费订阅”,也算是志同道合了。

Design Lobster

有一段时间,我的周刊涵盖主题越来越多,展开主题消耗的精力也逐渐增加,导致多而不精。Design Lobster 每周关于设计的主题不到5个,但也能给予人启发,举一反三。后来写到顺手,也开始挑选哪些信息值得一提,哪些对于优秀的订阅者们是不必要再提的。

Creativerly & so on

有一种作者,对待每周的内容非常认真;即使工作繁忙再抱怨也未曾拖延过内容;他们将一些原则践行到底;他们尊重读者的知情权,会特别标记出佣金链接。Creativerly 是其中的典型代表,还有很多其他优秀的作者也是如此。之前写周刊每当想鸽的时候,总是会以这些作者作为精神动力,想看看自己的 # 后面变成数百期后会如何。虽然中间停笔一年,不过还好捡回来了。


展望新的一年,打算先把目前的一些灵感债还完(/写完),然后开始做一本小册子总结上段工作经历,加到作品集里开始寻找新的机会。也希望能有更多的时间学习一些开发、建模上的知识,以及通关更多的游戏!可能的话也想推进一下「迁出北京」,以考察名义到各个城市住上一小段时间,啧啧。

文章中提及的作者可点此查看


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像

## #82 重认识重设计 Author: fenx | Link: https://fenx.work/remeet-redesign/ Article:

三年前的 Airbnb 在经历了 8 周损失 80% 业务的「濒死体验」后,经过数月大刀阔斧调整,溯流而上成功上市。自那一早客厅中的气垫床和早餐至今已有 15 年,这家目前市值 912 亿美元的共享经济平台依然饱受用户政治光谱、当地政策与传统酒店行业的抵抗和阻挠。潮水褪去,回归初心,但暂不谈市场对其 Q4 预期下降,且谈一些 Brian Chesky 幕后之事。

本文基于 Lenny Rachitsky 对他的老东家 Brian Chesky 的采访成文,推荐收听观看原播客/视频。已经看完的读者不必继续读下去了。

原地址

Airbnb 不必多言,在互联网设计行业中领先潮流,多少人梦寐以求的设计驱动公司。他们最早收到了 Figma 使用邀请,开放了 lottie 库等众多实用的开源项目。2022 年 Airbnb 首次 Q1 盈利时,我在 Design Scenes Weekly#53 概括其变化:

今年 6 月的 Figma Config 2023,Brian 口若悬河,收尾一句 “I just think more designers should rise up and start companies” 引爆全场氛围,掌声阵阵,评论区与 X(前 Twitter)上满堂喝彩。旁边的 Dylan Field 总是顿口无言,仿佛在尽力找出一句合适的话语,来结束这段充斥会场的鼓舞能量。Lenny 这次播客采访则是对这边大会更详细的解读。

在以往的媒体采访印象中,Brian 谦逊温和,也有着与少年气息不符的稳重老成。我们无法得知 Brian 是否提前准备了讲辞,但这两场关于「设计」的侃侃而谈显然他不准备「给任何人插嘴的机会」。他的语速很快,体态不是很放松,俨然已经进入了不断输出设计思考的心流状态——我们唯一能确定的是,这些想法已在他的脑中千锤百炼。

重认识界限

一传十,十传百,Config 2023 后许多设计师欢呼 Airbnb 领头「取消」了产品管理职能 (product management),那以后设计师的地位是不是有所上升了?播客一开始 Brian 澄清了误读:本质上这不是人的问题,而是工作方式上的改变。

即便如硅谷,设计师也经常对产品开发流程感到失望。设计师作为第一位见到产品外观的人,却经常被视为一项服务(被告知做什么就要去做什么),无法融入开发流程中。

另一边,不同业务之间的的团队技术栈存在差异,逐渐累计为技术债。比如有 5 个团队都需要支付业务团队支持,但却只能挨个排队获得资源从而拖慢了整体业务。国外大企业不像国内一样铺面宽广,以 Airbnb 的前台来讲也不至于建立大中台。总之按照传统来说,这种对核心业务团队的依赖关系一旦打破,个别团队就会索要资源主张自己成了一个新(支付业务)团队,各占山头,继而演变进化到一个分支部门。

部门则需要优先考虑业绩目标,只顾及自身,并且需要为自己部门争取更多的资源。争取资源的过程中,便会产生人情往来。如此分裂、细分、分裂、细分……最终形成了一种科层官僚体制 (bureaucracy)。在这种环境下,人们都不知道谁正在做什么事。每个人都有自己的工作目标,责任感缺失,认为少自己一个人的工作量也无所谓。一家快速发展的公司,就这样变得缓慢拖拉。

最终造成了这样后果:公司做了成百上千次营销,但客户什么也没了解。营销人员和工程人员并非互相讨厌,但却从不沟通。想象一家饭店,营销人员是服务员,工程人员是厨师。服务员知道顾客想吃什么但是无法做菜,厨师会做菜但是不知道顾客想吃什么。

重认识品牌营销

Airbnb 联合创始人 Joe Gebbia 有一个照亮房间的比喻,你既可以用高性能的激光快速打亮房间,也可以使用优雅的枝形吊灯柔和地照耀到房间每一处。两者都能照亮房间但是最终房间呈现的感觉不同。前者可以理解为以短期增长为目的的激励营销(比如拉新给钱、网络广告),亮度高,但是房间依然有很多阴影。后者为渗透用户心智的品牌营销,亮度低但灯光柔和,房间几乎没有阴影。这里其实并没有取舍之分,两种营销在不同阶段都应占有不同的比例,而非完全不做某一种营销。

特别是品牌营销,Brian 认为这些营销大都没有告诉用户团队正式做和已经上线的新功能——总不能指望应用商店更新日志去做这些事。他望着数年不曾有过明显变化的 Airbnb 应用开始怀疑,如果不对用户说明这些东西,用户没有受到教育引导便不会了解,也不会使用。后面再迭代新的重磅功能时,累积的「营销债」越来越多,用户变得越来越谨慎,最后 App 的活跃和转化都会停滞不前,甚至下滑。从 2015 年到 2019 年,Airbnb 在 adwords (Google 广告平台) 上花费了 10 亿美元,但却没有真正去投资 Airbnb 这个品牌。

所以 Brian 觉得现在不应该是一个团队做三件事,而是三个团队做好一件事,整个公司共同努力朝同一方向划船。为此 Brian 制定了未来两年的 roadmap,每月更新。产品策略则是每半年更新一次,并在每年的 5 月、11 月或 12 月以发布会形式开诚布公。产品策略和营销场景合二为一,向广大房东住户讲述精心准备的新功能背后的一段段故事。

重设计职能

很多公司从创始到稳定运营都有这么一个周期:

Brian 总结出一个反直觉的结论是,当他参与公司具体事务越少,项目周折便会越多,随之而来的是目标越来越不明确,团队热情越少,推动力越弱,资源生产越少,产品迭代越慢,最终拖累了公司业务增长——这与本来想象的避免一言堂而权力下放所预期不符。

受 Hiroki Asai 和 Jony Ive 启发(两者都为 Apple 工作过),Brian 将航班、家庭等 10 个主要业务群重组为职能部门——产品、设计、工程、营销、市场、销售、运营——这也是初创公司的常见职能。

所以作为争议的回应,Brian 并没有取消产品管理,因为在 Airbnb 许多产品管理其实在做着项目管理的工作。他将传统的「对内的」产品管理职能和「对外的」产品营销相结合成规模更小的高级团队,组内最高级别的人称为 “product marketers”。Brian 认为所有的产品营销也需要懂产品,如果你不懂产品那么你也不会构建产品,如果你不精通于产品营销那么也没法精通于构建产品。不识酒香无佳酿,酒香也怕巷子深。

有必要存在但不必多——Airbnb 还精简了员工总人数特别是一些高层管理职位(参考 2020 年那次羡煞众人的裁员信)。就像 product marketers 一样,其他职能部门管理者也必须是该领域的专家。但这些高级别的人没有权利去指挥设计和工程人员。

They manage by influence.

Influence 来源于职业责任,听起来很抽象,但实际上是为了减弱人情和职能上的依赖关系。Jony 爵士认为设计管理者首先应是管理设计,其次是人,Airbnb 也是如此。上面提到的两年 roadmap 也是 Brian 拉着这些领域专家们制定的共识。由于有严格的时间约束,项目(产品)管理责任很重,权利也很大(不然怎么去 push 呢)。最终,除了一些基础设施外,基本所有更迭都要纳入roadmap,由 Brian 亲自审阅。

重认识细节

Brian 刚回归参与业务时,他让每个人把自己做的事都写到 Google 表格中。直到这时他才注意到,很多人提笔未动,支支吾吾。有些人告诉他「我们做的事要多了,没法记下来」,Brian 两眼一黑,what the fuck 几乎脱口而出。他也明白,关注细节才是公司创始人应做的事。

这和微观管理 (micromanagement) 不同,Brian 不会具体告诉员工应该如何如何做,不会压低决策权,而仅是注重,对产品每一个策略了熟于心。不光董事会和 CEO 应该注重细节,不同层级的管理者起码应知道其他人在做什么。

那么作为 CEO 要审阅这么多内容,如何平衡自己和员工的工作激情与倦怠呢?Brian 说这又是一个悖论,在改革的前一两年,确实更苦更累。但是在越过某一节点时,Brian 发现自己关注的细节越多,富余的时间就会越多。原因便是整个公司朝同一个方向努力,即便 Brian 本人不在场,所有人也知道该做什么,会议向哪个方向发展,从而形成了一股「顺流而上」的文化 (culture)。一切都如此流畅时,CEO 自然可以把重心放在其他事上。

重认识、重设计连接

2020 年 Airbnb 受挫于新冠疫情,Brian 找到 LoveFrom, 的 Jony Ive,希望合作一起设计下一代的 Airbnb 产品和服务。就像 Steve Jobs 所说的 “Design is not just what it looks like and feels like. Design is how it works”,Jony 告诉 Brian 要做的本质上和 Facebook 等产品没什么区别,都是一种连接。Airbnb 以「人之初性本善」为起点,连接着房东与房客,疫情改变了租房市场,今后的 Airbnb 也要探寻像是数字游牧人士这种新角色和新场景的连接。

互联网拉近了人与人距离。而连接这个词可以上升穿透互联网,存在于社会潜意识之中。上面也提到,Brian 认为公司都不是独立个体,也都在互相连接着;他寄予管理者是艺术与科学的连接节点,做的产品连接着房东与房客的情谊;工作之外联系旧友,连接着人际关系,抵抗孤独——这些平凡无奇的道理,在当今环境、经济和政治等宏观因素脱节的世界中,更值得去留意探寻。


原博客还有更多 Brian 分享的内容这里不再展开,也推荐一看。

本文引用源详见此处

如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## #81 Unbox: Quiche Browser Author: fenx | Link: https://fenx.work/unbox-quiche-browser/ Article:

现代用户体验设计一般默认会给予用户最好的统一体验,自定义、模块化等词都代表着面向专业用户的体验标准,这类产品会尽可能开放功能以供用户实现个性化界面和顺手的工作流。

在浏览器应用品类下,Vivaldi 浏览器的高度自定义化让它几乎可以在任何浏览器新人面前谈笑风生。然而到了移动端 Vivaldi 像其他浏览器一样收起了锋芒——主打起了去广告和隐私安全体验。

本文主角 Quiche Browser 则是一款可以自定义地址栏和菜单栏的移动端浏览器,极简主义并未制约它的功能,webkit 也可以如此夺目。

📱
写作时版本为 1.11.1,平台为 iOS 17.1,机型为 iPhone 14 Pro。

咸挞,启动!

Quiche 在 1.11 版本加入了 onboarding 流程,直接调用了设置界面里的选项。

Quiche 引导流程和默认首页

可以看到 Quiche 没有账户系统,天然地隐私保护,不放糖才是零糖。

Quiche 默认布局类似大多数底部地址栏的浏览器,平平无奇。巧合的是自带的几个网站我也订阅了,都有着不错的内容。整体没有冗长的动效,对于大部分常用操作反应非常敏捷。

核心特色

通过上面引导进入设置,终于发现了新天地。

Quiche 设置中的 Toolbar Gallery

开发者 Greg de J 开门见山地给出了多种 toolbar(地址栏+工具栏)默认样式,这在传达其实 toolbar 在这里不止一种形态。我们可以直接点选这里的成品 toolbar 套用,也可以在下面紧接的设置中继续自定义。

不过,作为位数不多的引导性质页面,我觉得也可以尝试在这里直接给到自定义布局的捷径。一种是在顶部直接提示,另一种是下滑一段距离后给出悬浮按钮的提示。

Quiche 设置中的 Toolbar Gallery 界面以及右边的更改界面

点击上面的按钮(如果存在的话),可以来到 Layout 设置页面。

Quiche 设置中的 Layout 界面

Quiche 支持如下定制:

Quiche 支持定制许多 toolbar 选项,但一些功能殊途同归,只是少了一些操作节点。上图是我的平时的一些常用操作选择,其他不常用的则放在菜单中。如果你有更多快捷操作想要加入,甚至可以再第二行再加入 7 个操作——这是一个 7x2 的布局网格,非常自由。

Quiche 设置的双行 toolbar 展示
Quiche 设置的双行 toolbar 展示

如果再 PRO 一点,感觉可以加入不同的 Mode / Work Space,这有点像 Arc 浏览器的思路,实质上是针对自由化产物的管理机制。目前 Quiche 并未区分。


上面定制 toolbar 的过程中可以看到地址栏和菜单右边有一个 > 的指示。点击即可进行进一步细节的定制化。先点击地址栏。

Quiche 地址栏设置项

地址栏中可以定义:

经常在 Figma 摸爬滚打的设计师此时已经看出来,这不是 component variants 吗?

toolbar 映射到 Figma 组件

有时候创新,可能就是把我们习以为常的东西「放错」了位置。

阅读时长是开发者自己的需求,开启后会根据页面内容来估算阅读用时。在标签的列表页可以按照阅读时间长短来排序所有标签页。


到了菜单的定制化,大体是上面 layout 定制的选项,只不过是以菜单形式出现。为了节省空间,菜单也从松到紧分 3 种布局,纵向看有点像以前 Firefox 浏览器的菜单形式,「横向看」也有 macOS 可定制的菜单栏之形。

Quiche 菜单设置项}

3 种布局的取舍形式——

——代表了不同的重要程度,使排序更加符和直觉。我自己感觉 medium 布局有点鸡肋,但总归是多一个选项。最上方带有一个预览按钮,也是方便之处。

Quiche 菜单设置项与预览

上下文菜单(长按菜单)同样可以定制,但只能添减选项和更换顺序;除此之外还有手势,新标签页和主题模式等设置,感兴趣可以自行下载体验。

不足的点

Quiche 的定制化之旅让人愉悦,但就其流程本身还有不少优化空间。当前定制操作基本都放在设置中,顺序并非如本文层层递进。这在一开始使用时难免会产生困扰,且相关设置比较分散。比如地址栏在主题手势显示有相关设置,标签页在新标签显示也有相关设置。Quiche 以属性分类——把手势、显示等属性单独作为设置项目分组——个人认为是打乱了信息架构,我更想在同一主体下看到全部设置。

Quiche 不索求用户信息固然好事,但也无法同步个人配置。目前版本并无导出配置文件功能,每次重新安装都需要重新配置。

Quiche 的起点是满足开发者个人,公开后它可能需要处理来自大众的需求。移动端浏览器几乎已经标配去广告和隐私保护功能,不知 Quiche 之后会如何实现,期待。

上面两点均已在开发 roadmap 里。

结语

Quiche 虽然简单小巧,但我们应该惊讶于市面上主流浏览器几乎没有如此体验。

各大主流浏览器底部toolbar

可定制化产品的一个难点是,将传统产品解构后的信息梳理,用toC的思路去做toB 的内容,还经常费力不讨好。我一直是定制化体验的拥趸者,作为用户,每当遇到这种体验都会感到一些尊重。

在 AI 领域高速发展的今天,层出不穷的新技术应用让人恍如梦寐。向下观望时,我们会发现基础的应用体验并非尽善尽美乃至完满。之前在做 Google 系设计业务时,会高强度使用 Google 文档产品。然后就会感受到一幅奇妙的景象:世纪初的交互体验融合了二十年后先进的 LLM,结果就是前者遏制了后者更多的创造力,每每思考至此都令人感叹。在这种「代沟」之下,我们要做的事还有很多。Quiche 这类应用便如繁星中不起眼的一点亮光,虽然微不足道,但与其他渺小共同映射出了这片正在大放光明的星空。

拓展


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像

## #80 DNS 记录如何帮助抵制垃圾邮件 Author: fenx | Link: https://fenx.work/anti-spam-dns-record-referral/ Article:

二十九年前,两位普通的移民律师 Laurence Canter 和 Martha Siegel 怎么也没想到他们的一个 Perl 脚本在日后开启了垃圾邮件魔盒。广告、钓鱼和诈骗总是和社会工程挂钩,让人防不胜防,其中邮件途径总是占据多数,以至于现在不得不使用多重手段来防止垃圾邮件入侵。

图表内容,折线图,代表逐年攀升的钓鱼邮件点击率

今年的 10 月初,Google 宣布将在 2024 年 2 月对每天群发 5,000 封邮件以上发件人做出限制,要求其必须严格遵守邮件身份验证限制,雅虎邮箱也是。趁此机会分享一下在 Mailgun 配置发送域名的一些见闻,在这个过程中也了解了现代收件服务商使用了哪些基础验证手段抵制垃圾邮件。

SPF

SPF 全称 Sender Policy Framework,世纪之初时为了防止 SMTP 协议随意伪造发件人而诞生。它是一串 TXT 类型的 DNS 解析记录,定义了发件域名和其 SPF 记录域名/地址是否一致。

上图:SPF 工作流程。图源:The basics of SPF records

比如你有一封邮件来自 IP 地址 123.456.7.89,并显示它来自 example.com,收件服务商(指 QQ邮箱、Gmail 等)在给你展示前,会验证该 IP 是否符合 example.com SPF 记录里的规则。如不通过会将其作为伪造邮件处理。关于 SPF 详细语法推荐阅读下面文章。

SPF 记录:原理、语法及配置方法简介 - Blog - Renfei Song

对于 Mailgun 这样的 ESP(邮件服务提供商),使用的是其内部规则:

v=spf1 include:mailgun.org ~all

意思是引用 mailgun.org 的 SPF 记录规则(mailgun 有很多 IP,而且分为两个区域),发件人 IP 来自 mailgun.org 的都会通过。

SPF 记录相当重要,几乎所有邮件域名都需要配置此项,所以你可以用一些 SPF 记录检查工具或者 nslookup 命令来查看邮送域,进而反推其背后的 ESP 是谁。比如 Substack 和 Quail 用的也是 Mailgun,WordPress 的邮件推送则是自建。

有时我们会配置一些邮箱之间的转发,此时就会在 DMARC 反馈里看到一些 SPF 失败,此时收件服务商会调用转发域名的 ARC 验证,如通过也能转发成功,但是不同收件服务商会有不同的转发提示来提醒用户。

DKIM

DKIM 全称 DomainKeys Identified Mail,简单讲就是非对称加密的邮件版本。

SPF 工作流程。图源:Understanding DKIM

依然以 Mailgun 为例,Mailgun 会给你的每个域名提供最多 3 个公钥,也是一串 TXT 记录。它的结构一般为:

k=rsa; p=[DKIM PUBLIC KEY]

从 Mailgun 正规发出的每封邮件都会在信头(header)包含一段 DKIM 信息,包括:

收件服务商那边会使用 DNS 里面的公钥对 b 签名解密,如果正确使用了私钥加密,且 hbh 信息没变,即可通过验证。

你可以在 QQ 邮箱帮助中心 DKIM 指引中看到更多参数。

DKIM 公钥分 1024 和 2048 长度两种,后者更难以被破解利用。但其长度会超过 TXT 记录 255 字符限制,此时可以拆分为两个记录,详见

DMARC

一般有上面两者验证足矣,DMARC 的作用是,在发生 SPF 和 DKIM 验证失败时,如何操作这封问题邮件。

DMARC 全称 Domain-based Message Authentication Reporting and Conformance,是的又是 Domain-based,所以又要往 DNS 解析里加东西了。以 Design Scenes 的 DMARC 政策为例:

v=DMARC1; p=quarantine; adkim=r; aspf=r; pct=100; rua=mailto:hi@fenx.work;

早期怕出现什么差错先用 r,之后看看改成 s 严格匹配。

你可以在 MX Tool Box 这篇文章有直观的说明。值得一提的是,在后面配置 BIMI 时会需要 DMARC 在一级域名处添加 TXT 记录。

DMARC 报告

配置后需要一段时间生效,之后就会有你发送的各大邮箱给你 DMARC 报告反馈。

收件箱里的 DMARC 报告邮件

每封报告邮件都会带有一个 XML 压缩包的附件,你可以按照XML 结构直接读取里面的信息,但非常不直观。我试过直接用开发模式 Excel 导入、pandoc 转换、在线转换,最后感觉都不如直接找个 DMARC 报告分析工具直观和准确,像 EASDMARC 这个工具就还可以,很多地方都会给出注解。

我个人的话也只会在大规模推送文章后抽时间简单查看一下。

不同邮箱还有自己的邮件数据服务,比如 QQ 邮箱有一个他域互通工具,可以查看收件率、投诉率等信息,详见官方说明。同理 Gmail 有 Postmaster Tools,Outlook 有 SNDS 和 JMRP

BIMI

BIMI 是基于 DMARC 的延伸概念,全称 Brand Indicators for Message Identification。2016 年从DMARC.org 独立出来,作为邮件品牌身份识别的新标准。简单说,使用这个标准的邮送域可以显示自定义的品牌头像。如下图:

Gmail 中是否有 BIMI 验证的对比

Steam 邮送域符合 BIMI 标准,会显示头像和一个认证徽章。未认证的 It's Nice That 没有相关设置,就是默认头像。

BIMI 同样是一条 TXT 记录,结构大致是:

v=BIMI1; l=<YOUR SVG URL>; a=<YOUR VMC URL>;

BIMI 生效的基础是 DMARC 中配置 pct=100 以及p≠none ,然后SVG 格式也一定要符合 SVG Tiny Portable/Secure (SVG P/S) 标准。这意味着从 Adobe Illustrator 导出 1.2 版本 SVG 是不够的,需要类似 SVG P/S Converter 这样的转换器以达到标准。你可以在本地使用 jingtrang 工具检验自己的 SVG 是否符合标准(需要提前配置 java 运行环境)。

#安装 jingtrang
pip3 install jingtrang 

#从该网址下载 rnc 校对文件(下载后改后缀名 .rnc)
http://bimigroup.org/resources/SVG_PS-latest.rnc.txt

#验证
pyjing -c <RNC FILE PATH> <YOUR SVG PATH> 

如果没有报错,即通过 SVG P/S 格式验证,你可以在这里查看详细

像 Gmail、iCloud、yahoo 等邮箱都支持 BIMI 标准(更多详见),但是只有在拥有 VMC 的情况下才会显示头像。所以这个思路和 𝕏 (前身 Twitter) 有些类似:你的真实性由你产出的价值不算,肯花大价钱出价格才算真实。

综合配置好了后,你可以使用一些 BIMI 检查器检查各项标准,比如 BIMI Inspector。fenx.work 是完全符合的。

IP 声誉

配置好上述域名记录验证,依然不足以让发件新人即刻开始群发邮件。广义上的垃圾邮件生产者依然有可能做到这些事,所以剩下的识别则是依靠 ISP(收件服务提供商)和接受人的反馈数据—— IP 声誉。

假设你课金新建一个 IP 专门发收邮件,那么你需要向 ISP 证明自己如何不会发布大量垃圾邮件。你的对应域名过去 30 天处于活动状态最好(否则新域名也应热身),然后你需要每天都向你的核心受众发送少许邮件,这些邮件要小心撰写,减少被直接误伤为垃圾邮件得概率。你的这些受众最好也是对方主动订阅接收你的邮件的,否则遭到拉黑投诉相当致命。在这个期间,ISP 会严格收集邮件发送数量、发送频率、投诉和退回率——也就是艰难的热身期,一般会维持 4-8 周——当然也可以继续课金购买自动热身服务。

一般非企业用户默认使用的是 ESP 提供的共享 IP。这个 IP 地址是很多发件人共享的,不用热身,但是会共享声誉。那么 ESP 这边就会限制新加入 IP 的账户,此时要做的事和 IP 热身差不多,但限制更多。以 Mailgun 为例,这段期间一个消息(即 Ghost 上一篇文章)只能发给 9 个收件人,每小时最多发 100 封邮件。Mailgun 官方的帮助和博客都写的很详细,可以参考。

The Ultimate Guide to Warm-Up Your IP and Domain Reputation | Mailgun
Domain reputation is important, but how do you go about building it? A proper domain warm-up will get you set on the right track to the inbox.
How do I warm up my IP ?
Build and protect your reputation from the start It’s important to remember that as you’re getting started with Mailgun, now is the best time to start off on the right foot. Your domains and IPs mi…

我自己的经历是:从竹白导出邮件列表,再群发是不可能的。Mailgun 的客服告诉我要和收件人建立起「双向关系」,于是我便从竹白那边发送「唤回」邮件,请以前的订阅者重新订阅新网站。然后自己筹集了 10 个测试账号,用 email only 测试文章向其中 9 个邮箱隔几个小时就发送一次,并且保持非常高的打开率和点击率。由于 Mailgun 后台完全没有解除限制的反馈,所以偶尔会给 10 个测试账号发送测试文章以查看是否解锁了限制。

这个过程 Mailgun 说一般会持续 3 天到 1 周,在和客服不断沟通后也还是用了 2 天多解锁。当时还是很心急的,毕竟网站和文章都准备好了就差解除发件限制,当时我在笔记写道:「Design Scenes 的受众们很大概率不会收到仿冒 fenx.work 域名的钓鱼/诈骗邮件,因为我自己都有概率发送不了邮件。」

作为共享发送 IP 的一员,后续邮件声誉也很重要——保持良好的参与度指标,尽量避免垃圾邮件常用排版(使用优惠文案,连续图片排版等等),以及清理那些失效的邮箱地址。如果你使用隐私优先的工具订阅了很多 newsletter,那么你肯定会收到过几封类似「我们发现您很久没有活跃,是否确认取消订阅本 newsletter」的邮件。这便是维护邮件到达率的手段之一,以防过多的退件影响 IP 声誉。

Stop emails from going to spam with these proven tips and strategies | Mailgun
Emails going to spam folders? Boost engagement & make it to the inbox. 11 proven tips from high-performing email senders. Root causes & solutions revealed!

其他

除上述需要配置的通用策略外,还有一些各大收件服务商自己配置的 ARC,即 Authenticated Received Chain,来调整邮件转发时的安全问题。ARC 在 2019 年才成为 RFC 标准,它的作用是收件服务商转发之间会附上自家的“担保”,当多次转发后就会形成一条带有各自担保的转发链。举个例子,你要开始长途星际旅行——

详细的原理可以参照这篇文章说明:

關於 email security 的大小事 — 延伸篇
隨著前幾篇 email security 的介紹,我陸陸續續收到了一些問題,在交流的過程中覺得有一些很重要或是很有趣的討論可以更延伸探討。以下採取 Q&A 的格式,記錄一些討論與延伸知識。

结语

作为邮箱用户,上面的信息你可以在原始邮件信息中悉数看到。不同邮件查看元数据方式不同,Gmail 网页版是在三个垂直点弹出的菜单中,点击显示原始邮件

其实在接触自建 newsletter 前,这些规则对我来说都是「兔子洞」。接触后发现这些规则又是熟悉邮件者的家常便饭,老生常谈。几乎每一家提供邮件营销服务的产品都将这些写得详细易懂。

邮件服务年头已久,无论是传输协议还是反垃圾政策都很久没有大幅更新。受众群体反馈基本拿捏了一个发件域名的命脉,比如我经常看到米哈游游戏的新版本营销和召回邮件进入到垃圾箱🚮,发送域是阿里云,八成是被多人标记了垃圾邮件甚至是举办。希望未来的 AGI 能带来更精准地内容识别和逻辑构建,让反垃圾政策从 DNS 记录中解放出来*。

*:和米哈游无利益关系,单纯展望。


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## #79 Starfield 界面设计简评 Author: fenx | Link: https://fenx.work/starfield-ui-review/ Article:

在交互领域,游戏这一载体有着可谓是最「宽容」的受众,许多复杂的交互都会被主动体验数十小时候然后得心应手,再不济还有 mod 自发改善游戏体验。玩家一方对交互和界面也并不像工业/互联网产品那样苛刻监责,他们更倾向在内部社区解决问题,对游戏开发商主要还是游戏机制和内容上的诉求。

高质量的游戏评测随处可见,但很难找到讨论游戏交互和界面相关的内容,只在 SUPERJUMPGDC 看到过一些。游戏的界面发展大多体现在美术提升,少部分有叙事辅助作用。我是一名玩家,也是一名界面设计师;不是游戏开发者,没有方法论,只有经验之谈。


Starfield(后简称星空)是由 Bethesda Game Studios  (后简称 BGS)开发的一款太空主题 RPG 游戏。上古卷轴和辐射系列都是 BGS 的知名作,旗下作品一大特征是相较其他游戏有着异样数量的 mod 支持。  这家有着 22 年历史的游戏工作室,在如今依然有漫长的路要走。

⚠️
本文只集中说明界面交互相关内容,但是一些截图会不可避免地轻微剧透。介意者可在游戏通关之后浏览。

从 mod 说起

Mod 社区蓬勃发展的原因之一是,它是玩家群体自产自销的循环体系,诞生于玩家的需求,并由玩家解决。虽然在不同游戏中有不少争议,但大部分时候都为官方所默许。本文写作时是星空发售的第 11 天,Nexus Mods  网站已有 200 多个界面相关 mod,这些 mod 作者几乎在无偿帮助 BGS 解决各种 UI 问题。它们大多集中在布局、流畅性、字体和本地化等方面。我安装了几个流行的 mod 后,星空依然有不少界面和交互问题。

字体

星空英文的默认字体是 Neubau 的 NB Architekt 和 NB Grotesk R 两款基于菱形网格的字体,是标准的科幻风格字体。

星空默认英文字体下的界面

但中文的默认字体却是华康综艺(标题)和华康圆体(正文),和 DEATHLOOP 当初的华康 POP 体一样离谱。

游戏文件 Starfield - Interface.ba2 解包中的中文字体映射
选择辅助放大字体后的游戏中飞船界面,默认字体。截图视频来源

可以看到综艺体的字重高,笔划粘连严重,辨识度很低。好在现在字体替换 mod 很多,自己制作 swf 字体也比较方便。

字体问题到此,后面均为使用字体 mod 后的截图。

界面细节的缺失

星空一些基础的交互存在很多优化空间,比如——

滑动组件缺斤少两

星空游戏中工业制造机界面

上图中工业制造机用于合成各种材料,如果只是看这张图的话,谁会想到这里面还藏着一个滑动条组件呢?我们先不管为什么 Tab 键是退出,在这个界面状态中,数量 1 显示的是当前制作材料数,16 是可制作的最多材料数,中间的横线是一个可拖动的条,进度用白色填充。

一开始我以为那是一条分割线,而且游戏中其他地方也有带箭头指示 ◀ ▶  的滑动条,这里箭头也直接省略了。

我稍微改动了一下:

改动后的材料制造确认界面

同时该材料可被用于什么用途,在左侧也没有显示。右侧制造 E 按钮中的横线也使用得很随意。这或许可以辩称为 NASA 朋克美术风格,但作为现代欧美游戏工业化作品,星空在基础交互环节的体验缺失让人大跌眼镜。

💡
组件风格化的基础是交互完整性。

状态显示飘忽不定

星空游戏中研究实验室界面

上图中,我们想要推动「枪管模组 1」的进度,可以看到右侧需要的材料。当材料满足数量时,会出现方形叹号图标和材料可用一起在底部文本上闪烁提示,而其右边的材料数量指示依然为 0/3。游戏其实想表达的是这个意思:

更改后的研究实验室界面

首先取消闪烁提示,变为常驻,黄色变为了绿色,右侧对齐并留出了 4 位数材料的间距。其次是数字,原本 0/3 代表的是当前材料交付后的状态。虽然你有 24 块铁,但是没有上交,这里就会一直是 0,而非代表你有 0 块铁。这个逆天的状态分为三种:

这里的改进方法还有很多,譬如条状背景变绿、数字颜色区分或图标前置绿色指示等等,但唯独这种闪烁提示让人摸不到头脑,也并非空间不足,最后增加了理解成本。

💡
常驻状态的提示更有助于扫描浏览界面而不分心。

物品交互粗枝大叶

博德之门 3 中你发现一本书可以直接阅读,也可以捡到背包中稍后打开阅读。食物也是如此,直接食用或者放到背包。但是在星空中,这样不行。

交互对象为书籍时的游戏界面

你只能捡起物品到背包里,翻找一阵,然后才能打开阅读。这样的物品实际游玩很频繁,但没有捷径导致所有食物本身的存在意义不大。

这也包括,有时登陆不同极端环境的星球,可能会受到各种疾病 debuff,需要不同的药物解除。这些负面状态会显示在数据菜单(个人信息首页)和状态页。但是这两个地方都没有直接服下对应药品的快捷操作。你需要点击物品 > 救援 > (茫茫物品中)寻找对应药品,同样很不方便。

💡
频繁操作应提供便利地,且符合经验直觉地交互。

按键连续性

输入器是桌面端游戏的特色交互之一。这里暂不谈及手柄,根据键鼠操作经验,一般会使:

星空在第一点上做得还行,但是第二点上比较灾难。首先星空默认 Tab 键位是大多路径的返回键。虽然 ESC 依然生效但依然让人摸不到头脑。

除了打开装备,其他快捷键打开相应功能后,都无法直接返回游戏主界面。想要直接返回也可以,但是需要长按。但长按一般是手柄键位不够时的替代做法(荒野大镖客 2 主机版中很常见),而且几乎没人喜欢这种游戏中的「延迟感」——这和游戏中提倡的「流畅感」背后的敏捷相悖。


这其中地图相关按键时最迷惑的:

打开地图按 Tab 时不会返回到数据菜单,而是会返回到上一层更大的地图,直到宇宙。那么这个过渡为什么不交给鼠标滑轮呢?其实 BGS 还真做了,只不过只做了一步,详见路径 2。


再看一些其他路径:

还有很多没有逻辑的按键安排,在前期或多或少有所不适应。

引导不足

星空的引导缺失应该时最严重的问题,导致前期经常发生看不懂 A、找不到 B 的情况。

星空装备、飞船升级、扫描器、对话说服界面截图

这些问题只有带着疑问继续玩下去、询问其他玩家或者安装 mod 才能得到解决,也难怪一些媒体直言十小时后才是游戏的开始。

最近另一款游戏 Cocoon 全程无 HUD 无文本,但依然引人入胜,浮想联翩(同为 Jeppe Carlsen 的作品,和 Inside 如出一辙,没有玩过的可以一试)。Cocoon 这类游戏阐述了如何使用纹理、颜色和动效等元素将游戏引导提升至一定高度。星空的重心明显不在此处。 

结语

我在星空中度过了 60 多小时,上面的评价在后期基本与正常体验无差。星空开发周期长达 8 年,从 Todd 的发言来看,可能存在数次推翻重做。或许 BGS 和其 QA 团队到后面对这些新玩家细节也感到麻痹,但同时也说明 BGS 对新玩家的体验没有重视,认为还有 modder 帮自己「兜底」?无论如何,星空的前期体验实在达不到及格线。游戏后面故事固然有精彩之处,但留不住玩家的话也甚是可惜。

另一方面,星空的平面设计花了不少心思。你能在游戏很多地方发现各派系符号以及背后的衍生设计。还有「无孔不入」的墙面海报,增加了不少趣味和人文。

游戏里的海报截图
游戏里的摄影模式,自带一些旧书籍的「样机」,我很喜欢

综合体验我觉得星空可以有 8 分,可能预期本来也不高,权当天外世界(The Outer Worlds)plus 版玩罢。

## #78 font-size-adjust 简谈 Author: fenx | Link: https://fenx.work/web-typography-font-size-adjust/ Article:

旧闻一则:Safari 17.0 支持 font-size-adjust 更多特性,可以直接使用 from-font 值和双值语法,包括 CJK 文字 ic-height 值。这个 CSS 特性解决的是网页字体 fallback 场景下不同字体的大小对齐问题,尽量让保持字体大小一致,增加可读性。

哆啦A梦中大雄表情包:特意把大家喊出来,就是为了这点事呀。

举个例子

调整前的一句英文:This is a static template, there is no bundler or bundling involved! 其中 static template 字体不同,且看起来较小

为了效果直接,我挑了个极端的对比。上图中衬线字体为 Google Sans,中间特殊字体为 Baskerville,默认都是 16px 字号,「调整前」Baskerville 视觉上明显小了一些。font-size-adjust 可以单独微调字号,让其按照 x-height 或其他标准对齐。

font-size: 16px;
font-size-adjust: 0.5;
是否使用 font-size-adjust 的文字对比,其中 static template 看起来和其他字母一样大小
是否使用 font-size-adjust 的文字对比

X-height 对一款西文字体的风格体现很重要,高于均值的 x-height 字体在小字号中更易读,低于均值的 x-height 字体更不落窠臼,而 x-height 过高和过低都会导致难以辨认字形。

在正文排版中,基线对齐是标准,按照相近的 x-height 高度排版便能起到对齐效果。font-size-adjust  默认值便是如此,双值语法即:

font-size-adjust: ex-height 0.5;

0.5 代表待调整字体的 x-height 高度之于 font-family 首选字体的 font-size 比例,在这里也就是,我们想让 Baskerville 的x-height 和 Google Sans 一样,即 16 * 0.5 = 8px 高。

至于0.5 怎么得出来的了——WWDC 23 的 What’s new in CSS 视频中会说,大部分比值都在 0.5 左右徘徊:

x-height / font size ≈ 0.5

所以只要试出一个自己满意的值即可。当然如果想精准一些,也可以自己到软件里手动测量。

Figma 中 Google Sans 和 Baskerville 各度量辅助线对比

如图,为了取整(和在 Figma 里演示)方便,均为 80px 字号的字体,实际量得Google Sans 的 x-height 为41(取整),Baskerville 的 x-height 为 32(取整)。一个比例 0.5 一个比例 0.4,相差甚远。

对 Baskerville 进行 font-size-adjust 时即可设置为 0.5,反之如果对 Google Sans 调整,即可设置为 0.4。

当然上述只是模糊的参考数值。如果懒得计算也没关系,可以直接试试:

font-size-adjust: from-font;

这样会自动调用字体度量(metrics)来计算,比如 Google Sans 的 x-height 度量为 510,em 为 1000,实际比例就是 0.51。像 Baskerville 这样的老 TTF 字体,em 为 2048,x-height 为 819,实际比例就是 0.3999。

其他特性介绍

除了 ex-heightfont-size-adjust 还支持 cap-height, ch-width, ic-width, ic-height 属性,如图。

各属性代表字体的部位,使用 Aa、0 和水作为示例
各属性代表字体的部位,截图自 What’s new in CSS

图很清晰:

💡
关于步进宽度(advance width)和高度(advance height),直接用图解释比较快。详见 freetype 原文
小写字母 g 的字形参数

所以这么一看,上面 WWDC 23 那张图多少有些不太严谨🤭。

中英混排

从实际出发,这次以 Noto Sans SC 和 Google Sans 混排为例,两者组合是 Google 大中华区不得不品尝的设计风味之一。

与西文字体不同的是,中文不能复用其度量标准。虽然都是在观察负空间和阅读轮廓线,但中文更看重字面、重心和中宫,中宫常与西文的 x-height 相比较,但只处于感知层面没有数值描述。不同结构的文字和其他语言组合也需要考究一番。

Noto Sans SC 中既有英文也有中文,网页实际渲染时会按照英文部分渲染基线和其他度量。所以单就 ic-height 来说并非特殊属性。比如下面一个常见的例子:使用数字和中文搭配成 label 文字的组合。

对「10 条回复」的文字大小调整
对「10 条回复」的文字大小调整

未调整情况下,我们会发现「条回复」明显大于数字,特别是顶部高出一头的感觉很明显。分别使用三种特性对中文进行调整,可以看到中文和数字对齐了一些,同时三种调整几乎没有区别。(注意:这里的汉字首选字体均单独设置为 Noto Sans SC)

「10 条回复」文字中数字和汉字的各处度量对比

如上图各度量使用 Figma 基础设置便可大致显现。

从设计角度看,我们可以先逆过来调整不同文本到一定和谐的比例,然后再去统计此时各个度量数值,从而计算出比例。


但是到环境更复杂的正文中,有时调整局部反而破坏了整体连续性。

使用五种方式调整一段文字对比效果

由于中文与英文衔接处不固定,所以难以大幅度改动排版。比如此例中的「使用 AC」处,一直最稳定的默认调整 from-font 翻车,AC 顶部明显比「用」字高出一头,显得整体中文反而较小。

「使用 AC」字样放大对比}

原因大概在于,设置 ic-height 的前提是基线位置不变,汉字的顶部趋近于英文顶部时,下方并不会相应对齐,从而比例不太和谐。所以此时,在默认比例基础上稍微增加一些数值会更好一些。

其他拓展实例

中文不同字体混排

像我们这种市井设计师,可能会碰到一些比较逆天的排版。这时候中文内不同字体也可以使用 font-size-adjust 调整排版。

使用 font-size-adjust 调整两种不同中文字体

上图中,「宇宙洪荒」使用了站酷快乐体,其他文字使用了 Noto Sans SC。可以看到快乐体整体偏小(当然字重也不匹配,这里先忽略)。到 Figma 里看了一下,同样都是 100 号字体时,快乐体字宽 92,字高 100。所以设置 ic-width 为 100 / 92 ≈ 1.087,效果如图所示。

阿拉伯字体实验

某个平行世界的我可能在研究 8 世纪的炼金文籍,但这个世界的我对阿拉伯文一无所知。在 W3C 的文档看到多层基线时我就先撤退了。直接看看效果:

使用阿拉伯文、中文和英文的对比

上图阿拉伯文是我随便在 Google Font 上找到的字体 Ruwudu,直接用 from-font 调整后高度有所改善。但 Ruwudu 并非是正宗使用 alef height、joining line 或 Meem Depth 等度量的阿拉伯文字体,本质上依靠的还是字体内西文字体的度量,所以实际效果待定。

兼容

目前 font-size-adjust 仅 Firefox (3-120)和 Safari (16.4-17.0)支持,Chrome 可在 flags 里开启。而双值语法特性 (Two-value syntax)仅 Firefox (92-120)和 Safari (17.0)支持,Chrome 不可兼容。

上文图例均在 Safari 17.0 实现。

“font-size-adjust” | Can I use... Support tables for HTML5, CSS3, etc

参考

本次成文离不开各方资料参考:

一些常见字体比例小工具:

延伸阅读

如感兴趣读者可以继续延伸阅读:


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## #77 搭建 Ghost 全流程 Author: fenx | Link: https://fenx.work/build-with-ghost-2023/ Article:

你好啊,这里是许久不见的 Design Scenes,别来无恙。

在重新开始写 newsletter 前,一件确定的事便是考虑更换平台。竹白的基础功能有些波动,最主要的是产品几乎不再更新,这在做产品的眼中基本已经☠️了。剩下的邮件服务平台很多都是以盈利目的使用,自身大多需要月供一定的费用。最终在 Substack 和自己搭建 Ghost 之间抉择。Telegram 上的 @informavore 告诉我可以试试 Fly.io + Ghost 组合,被我第一时间本能地拒绝了。但由于垂涎 Ghost 已久,本着「反正建不好有 Substack 兜底」,还是着手试了试,果然不会一帆风顺。

本文较长,可以访问原文使用目录功能查看。希望对像我一样少有代码接触也想建立自己的 Ghost 的读者有所帮助。

初次接触 Ghost

高中时我对 CMS 博客理解基本就是 WordPress,用过一小段时间 Typecho,后面就对 Hugo、Next.js 之类的没什么印象。时过境迁,CLI 中几行命令即可迅速部署好一个博客。推荐下面这篇文章中的流程。

在fly.io免费运行Ghost博客:安装、备份和恢复
这篇文章介绍了在fly.io上免费运行Ghost博客,备份和还原数据的方法。备份和恢复数据对其他服务器上的用例具有参考意义。

这步需要注意的是 Fly.io 的服务器节点区域选择,就近最好。依梯子那边线路延迟来看,我这里选择的是香港节点(hkg),更多区域节点详见文档

Fly.io 可以随时对配置升级,免费配置中只有 256MB 内存有点捉急。Ghost 官方推荐是 1GB,网站在刚搭建完基本没做什么时内存占用都在200MB 左右,偶尔还会出现 502 错误,Fly.io 官方也向我发送过崩溃报错邮件……所以我升级到了 512MB,算是稳定下来。

按照文章顺便配置了自己的域名,没有乱七八糟额外收费 Fly.io 这点也很不错。改域名后需要重新部署。

Ghost 后台很简洁,大致填写了一遍表单后,剩下的就是配置邮件和主题样式。

Mailgun 的小心思

Mailgun 是 Ghost 官方那个推荐的发件系统,后台也集成了相关功能,简单配置发送域名和 API 就可以使用——但没想象的那么简单。

首先 Mailgun 的注册就把我卡住了,账号需要邮箱和手机号验证。虽然支持 +86 短信发送,但是目前为止一封验证短信也没收到。之后搜到了 Her Blue 这篇文章。

终于解决了会员注册的问题
历经千辛万苦,终于在今晚,把网站的会员注册功能给解决了。 现在,你可以点击「成员资格>立即订阅」来注册自己的账号,就可以参与文章页的评论了。 一些过程 早在之前,这个功能是正常的,小站也有十几个人的注册用户,但是有一次在搬迁服务器之后,这个功能就出错了,大概原因是因为服务器的25端口被关闭了,无法解封。 之后一直想解决这个问题,找DIN帮忙,也找一间生活的博主讨论,最终放弃了之前的SMTP邮件方式,转到Mailgun的邮件服务商。 但这一转也是历经多重磨难: * 注册Mailgun账号,在没有搭梯子的情况下,注册页面的「验证码」模块是加载不出来的,所以「确认注册」的按钮是无法按…

最后我也是邮件客服,由于时区等了 10 几个小时后才解决。

Mailgun 套餐改过很多次,2023 年版本是:首先 Mailgun 有一定的免费配额,也就是 Free Plan,每月免费 5000 封邮件,看着很吸引人。但是默认只能使用 sandbox 前缀域名,而且只能发送给 Authorized Recipients(认证的接收者),且最多 5 个人。Mailgun 解释说这可以用做测试。完全体需要掏出信用卡升级。

然而点击 upgrade 后,却发现和 Ghost 官方说的不一样。

Mailgun 在 2023 年 9 月 30 日的价格页面截图

上来就 35 美元每月直接差点劝退我,我又找了几篇文章才发现,很多小型博主使用的是 Flex Plan,我擦亮眼镜也没发现这 8 个字母在哪。又是一番搜索,发现了这个帖子。

[solved] Mailgun has quietly removed their “pay as you go” offer. Any new option to be integrated with Ghost’s native newsletter?
Yes, you can. Start the trial, and then you can downgrade to the Flex pay as you go model.

原来 Mailgun 早就取消了直接升级 Flex Plan 的渠道,需要添加信用卡后,升级到 Foundation (Trial) 后再 downgrade 即可在下个月变为 Flex Plan。高,真的高。

接下来就是按照官方流程,添加新的自定义域名,生成新的 Mailgun API keys,填写到 Ghost 后台。这段流程中,无论是 DNS 配置还是 Ghost 调用 Mailgun 都有一定的延迟,所以测试邮件时别像我一样心急……

Ghost 会员邮件配置

后台配置好 Mailgun 后文章可正常推送到目标邮箱。但是前台会员注册/登录的 maigc link 验证邮件提示无法发送。回到 Ghost 安装的文章,原因是 fly.toml 一些字段没配置。

mail__from = "noreply@example.com"
mail__options__auth__pass = "<YourMailgunPassword>"
mail__options__auth__user = "postmaster@example.com"
mail__options__host = "smtp.mailgun.org"
mail__options__port = "465"
mail__transport = "SMTP"

在 Ghost 的 FAQ 页面可以看到,Ghost 的发信分为两种:

上面 toml 文件中的用户和密码使用的不是 Mailgun 账号密码,而是 Sending > Overview > SMTP credentials 中的账号密码。要查看密码,点击 Reset Password 才可以……这个交互逻辑也是闻所未闻。听说这还是 Mailgun 新版后台界面,到底改了什么😅。

Mailgun 后台 SMTP credentials 设置页面
Mailgun 后台 SMTP credentials 设置页面

这样 SMTP 部分便部署完毕, 过程中也加深了对配置文件 fly.toml  的认识 (毕竟是没用过 docker 的人🥹)。

Ghost 配置之路

不同于 Framer 设计师能亲自上手造车,Ghost 主题需要「老一套」代码编译。好在结构和其他博客系统差不多,官方文档覆盖比较全面,Handlebars 门槛也不高,多翻看几次也能稍微理解一些表面。

本地部署

我使用的是古典主义 Ghost 后台上传压缩包方式更换主题,为了改主题方便一点需要本地部署一个 Ghost,然后直接修改内部主题文件。

How to install Ghost locally on Mac, PC or Linux
A detailed local install guide for how to install the Ghost publishing platform on your computer running Mac, PC or Linux. Ideal for Ghost theme development.

这个很简单,安装 Ghost-CLI 后,cd 到新建好的目录直接安装。很快,都不用配置数据库。

主题本地化

挑选一个合适的主题很重要,Ghost 有很多优雅但又价格不菲的主题,但囊中羞涩。好在 Ghost 官方有一些不错的免费主题,便选择了 Journal 这一款。

Ghost 支持文本和一部分 handlebars 表达式的本地化。

Ghost Handlebars Theme Helpers: translate
Discover how to translate content using your Ghost theme and the translate helper. Read more about translations in Ghost 👻

主题根目录中 locales 文件夹里面的 json 文件便是主题中的可定制翻译文本。有最好,没有的话可能就得像我一样挨个找出来新建……还要把对应的文本用 {{t }} 方式包起来。

主题 Journal 的zhcn.json 翻译文本
主题 Journal 的 zhcn.json 翻译文本

之后在 Ghost 后台设置 > General > Publication Language 中填上 zhcn 即可生效。但有一些组件 Ghost 尚不支持定制,详见文档。这些文档都存在于核心服务中,还没有单拿出来定制。

比较遗憾的是 Ghost 无法深度定制邮件模板 CSS 样式,5.65.1 版本目前可以在 /versions/5.65.1/node_modules/@tryghost/email-service/lib/email-templates/template.hbs 找到模板,如果要更改其样式可在上层文件夹里的 ../partials/styles.hbs 修改,但也只能硬改,无法有效地编译。

另外在 /versions/5.65.1/core/server/services/mail/templates/ 中能发现很多邮件模板,/versions/5.65.1/core/server/services/newsletters/emails/verify-email.js 中能看到验证邮件的字段。理论上都可以更改,但是一旦更新版本这些改动又有概率重置,得不偿失。我只在本地测试了一下,没有上线改动。

改动这些文件需要执行 ghost restart 生效。

添加文章目录

Ghost 有一个栏目介绍自家系统的拓展用法。我比较纳闷为啥有些功能为啥不直接做进系统里……想起之前文档里有说过,如果要支持其他 ESP 请自己提 pull request,看来其他功能也是如此。

目前先打算添加一个比较基础的目录功能,使用的如下文章方法。

How to add a table of contents to your Ghost site
Let your readers know what to expect in your posts and give them quick links to navigate content quickly by adding a table of contents with the Tocbot library.

过程中碰到了两个点:

一是CSS 调用问题:主题调用的是 assets/built/screen.css 文件,而不是 assets/css/screen.css,前者需要使用 Yarn 和 Gulp 编译后者生成。为了省事我直接把样式写字了 hbs 文件头部。

二是布局问题:按照文章的代码会导致无法浮动固定在指定位置。

Arc 浏览器检查器中的主题 Journal 的文章头部结构

原文涉及侧栏的样式代码:

@media (min-width: 1300px) {
     .gh-sidebar {
        position: absolute; 
        top: 0;
        bottom: 0;
        margin-top: 4vmin;
        grid-column: wide-start / main-start; /* Place the TOC to the left of the content */
    }
   
    .gh-toc {
        position: sticky; /* On larger screens, TOC will stay in the same spot on the page */
        top: 4vmin;
    }
}

如果给 .gh-sidebar 设置 position: sticky; 时就会因 css grid 而影响文章的首段行高,后来去查阅了一下 MDN 发现是容器高度问题,.gh-sidebar 在主题默认样式中 height:max-content; 内容多高容器就多高,当然无法滚动固定。更换为 height: auto; 即可使容器高度和文章一致,solved。为了不妨碍主题样式,我直接重新起了个 class 名。

后面如果使顶部导航栏浮动的话,相对高度改变,.gh-toc 也要改一下数值,整体变为:

@media (min-width: 1300px) {
     .article-sidebar {
        position: absolute; 
        top: 0;
        bottom: 0;
        margin-top: 4vmin;
        height: auto;
        grid-column: wide-start / main-start; /* Place the TOC to the left of the content */
    }
   
    .gh-toc {
        position: sticky; /* On larger screens, TOC will stay in the same spot on the page */
        top: 16vmin;
    }
}

Ghost 这个 Do more 栏目还有实践,如果之后代码使用很多的话也考虑优化一下。阅读进度条先不打算加,毕竟不加 100% 不会减慢访问速度。

目前的主题已经传到了 GitHub 上,可自行参考。

GitHub - fenxer/Journal-zhcn: Ghost 主题 Journal 定制版
Ghost 主题 Journal 定制版. Contribute to fenxer/Journal-zhcn development by creating an account on GitHub.

加速 JsDelivr 和 Gravatar

Ghost 很多组件 js 托管在 JsDelivr,虽然现在也能访问,但是速度较慢,我这测试不用梯子时,有的 js 载入 10s 有余。

Ghost 配置文件 default.json
Ghost 配置文件 default.json

/versions/[当前版本号]/core/shared/config/ 文件中,default 配置了 Ghost 许多核心功能。如上图为 JsDelivr 托管的一些文件。可以把这些文件都下载下来放到自己服务器上,也可以把所有 cdn.jsdelivr.net 替换成 fastly.jsdelivr.net。两种方法可分别参考下面两篇文章。

📬
一文扫通Ghost会员系统邮件配置
Ghost 的两个优化点
Ghost 有一些默认的外部资源引用可能会造成部份访客的体验下降,为了让各地访客都能有一个不错的速度,纯小白用户建议修改两个地方。

我使用的是前者方法,速度更有保障,但每次更新版本可能需要覆盖更新。将这些 js 文件下载下来后(除了 editor,会导致后台编辑器不可用),如何传到 Fly.io 上又是一难题——所有文章都没写过。

这个时候就该说明一下为什么推荐第一篇文章安装 Ghost 了,原文第 6 节提到了如何备份和恢复 Ghost,其中使用了 SFTP,查询关键词,果然是这个。

fly sftp
Documentation and guides from the team at Fly.io.

照葫芦画瓢,原文有些方法不适用,但还是成功进入到 VM 文件内部:

cd [安装路径]
flyctl auth login

先登录,然后 shell 命令中,可以多种方法进入自己 VM 的存储卷,比如应用名、区域或组织名等等,详见帮助说明

flyctl ssh sftp shell -r [地区代码]
flyctl ssh sftp shell -a [应用名]
flyctl ssh sftp shell -o [组织名]

进入后会显示红色的 >> 符号,然后就可以执行各种命令了。

cd #改变目录
ls #列出当前文件列表
put #上传
get #下载
chmod #权限设置
将下载好的 js 文件传到 Fly.io 后的文件夹列表
将下载好的 js 文件传到 Fly.io 后的文件夹列表

然后打开 fly.toml,按照文章把上传后的 js 路径写到 [env] 里去。注意这里要加上双引号,以符合 Fly.io 这边语法。

到这里可以顺手把 gravatar 地址改了,官方源不用梯子无论如何也访问不了,裂图效果很糟心。这里使用的是这篇文章提到的镜像源:

[env]
gravatar__url = "https://use.sevencdn.com/avatar/{hash}?s={size}&r={rating}&d={_default}"

最后重新部署一遍就可以了。

客服邮件备置

Ghost 的 Portal settings设置中可配置客服邮箱,为了隐私性和统一性,我打算再开个域名邮箱。这一看才发现 QQ 的免费域名邮箱早已不再提供,被企业微信融合。鉴于目前是早期试运行阶段,付费域名邮箱也并非首选。考察一番后,发现飞书在提供免费域名邮箱,只需要新创建一个企业,未认证的也可以。

创建后在企业后台 > 产品设置 > 邮件中直接添加域名,跟着流程走就可以。最后在成员列表设置邮箱即可在飞书收信。如果更方便点,还可以在设置里自动转发到常用邮箱。

这里我使用 hi@fenx.work 作为客服邮箱。

客服邮箱配置好后,我在网站页面底部加上了「反馈」入口,它的本质是一段 mailto 链接:

mailto:hi@fenx.work?subject=[网站反馈]&body=请写下反馈页面地址和具体问题

其中 subject 和 body 作用是默认在标题和正文加上一段文字,辅助用户更好地反馈。

点此试试

更近一步优化的话,验证邮件也可以帮忙打开,Growth Design 有一期讲的是这事,利用邮箱的筛选功能配置网址直接找到验证邮件。

其他网站设置

Design Scenes 更换到塔状 logo,以塔的各种形式重新诠释。使用了 Newsagent 字体。颜色也比之前更亮了一点,以匹配网页上 sRGB 的低亮度。

Design Scenes 黑白色 logo

剩下就是一些页面文案撰写工作,花了一些时间。

哦对了,还有邮件批量导入。Ghost 提供了 csv 模板,也是比较方便的。

部署 umami 分析

如今已不是Google Analytics 横行的时代,大量隐私优先的统计工具出现。我在之前 newsletter 里提到过 creativerly 的文章:

A list of privacy-friendly Google Analytics alternatives.
If you want to get insights about the traffic happening on your website you probably came across Google Analytics. Let me tell you: it is bloated, hard to use, and scraping user data. In this blog post, I will show you a list of privacy-friendly and ethical Google Analytics alternatives.

大部分平台依然需要月供十几美元来支付「隐私费」,使用较为广泛且有开源的 Plausible 和 umami 成为半决赛选手。Plausible 使用 docker 设计搭建,无论在 DigitalOcean Droplet 还是 Fly.io 上都需要额外再掏钱。所以我选择了可以直接部署在 Vercel 的 umami

在 Vercel 部署 umami 网站统计及报错解决
Umami 是一种简单、快速的网站分析替代品,可替代 Google Analytics。

网上有很多相关文章,基本流程大同小异,建立 supabase 数据库,Fork 官方库后到 Vercel 配置环境变量,部署上线。推荐这篇文章是它帮我解决了一个啼笑皆非的错误。

我在最后一步部署时,Vercel 总是提示 unable to connect to the database 报错,因为要填写的 string 一共也没几个所以很纳闷……后来看到这篇文章时突然察觉到,是不是我填写 DATABASE_URL 时密码写错了!原本要求的是 [YOUR-PASSWORD] 全部替换为密码,但是我保留了两边方括号……呃。

更改分析域名与脚本名

在查找资料时发现有人提到 *.vercel.app 域名国内访问问题,无 cookie 统计已经不是很准确,我想尽量排除一些干扰要素。于是到 Vercel 项目设置 (Project Settings) 页面,加上了自己的二级域名。这下域名那边的 DNS 解析已经两页之多了😅。

Umami 现在默认使用的是 script.js,虽说不像之前 umami.js 那么容易被去广告插件识别,但说不准什么机制会有干扰。Umami 提供了更改 js 名这一服务,直接在 Vercel 项目设置 Environment Variables 中添加一条 TRACKER_SCRIPT_NAME 即可。命名规则如下:

TRACKER_SCRIPT_NAME=           # Unset. By default, the script is fetched from: /script.js
TRACKER_SCRIPT_NAME=custom     # Fetched from: /custom
TRACKER_SCRIPT_NAME=custom.js  # Fetched from: /custom.js

至此,跟踪代码已经自定义为:

<script async src="https://[自定义二级域名]/[自定义脚本名]" data-website-id="[网站id]"></script>

Umami 不使用 cookie,不搜集带有个人标识的数据,虽然不准确(比如我自己的活动数据也会计入),但这样你的数据隐私不会被跨站利用。Plausible 写过一篇统计原理可以看看:

58% of Hacker News, Reddit and tech-savvy audiences block Google Analytics
Is Google Analytics still useful and how accurate are its stats? How much data is missing from Google Analytics due to adblockers and privacy-friendly browsers?

另外 CloudFlare 也有一款免费的网站分析产品,同样是隐私优先,但省事许多,之后看看是否有机会尝试一下。

结语

至此 Ghost 平台首发版本初步搭建完毕,本文也将作为第一次群发邮件测试。7k 字内容不知道在邮箱哪里会被截断……实际工作量远没有文章中那样谈笑风生,期间至少有 3 次我觉得还是去 Substack 吧。但没到山穷水尽那一刻总觉得不是放弃的时机,磕磕绊绊也算前进了。

另外在本次流程中没有使用带有 LLM 的 AGI 对话工具,依然全程使用 Google 搜索。我之后在 Poe 和 Bing 试了下,不是出现「幻觉」就是重复信息,帮助不多。


如果你觉得文章对你有些帮助,可以请我的猫吃罐头 ↓

带有微信赞赏码和文字 A kind and compassionate act is often its own reward. 的横幅图像
## 欢迎 Author: fenx | Link: https://fenx.work/manual/ Article:
Design Scenes 第二版标识,非正常版本,为描边和辅助线的解构版

欢迎来到配置贫瘠的本站,遇到报错可以刷新几次,目前一切还在试运行中。


What’s New

本次 Design Scenes 不再以每周通讯形式发送,而是变为不定期文章推送,详见关于页面。如果想取消订阅可以拉到邮件最下方找到入口。

为了防止 spam,订阅/注册和登录都需要验证邮件。

发件邮箱为 noreply@fenx.work,为了防止成为垃圾邮件,你可以使用添加为联系人等手段让邮箱信任该发件地址,这对我很有帮助。


赞赏机制(试运行)

如果你喜欢我的文章可以微信扫描赞赏码打赏我,钱会首先用于服务器和邮件等基础开销,如有余下就请我家两只猫吃猫罐头🐱。

你在打赏的同时可以对我捎带一段文本,形式的话赞赏留言、邮件 hi@fenx.work 或者 Twitter 私信都可以,我会把文本放到赞助墙。这段文本可以随意说些什么,几百字以内,不用太长。

你可以说

不可以说

需要注意的是,由于文章发布时间不固定,我可能难以在正文中提及以上信息。

🫰
赞赏码在每篇文章底部。能读完文章已经是一种赞赏。

隐私

本站只会在你注册和登录时,需要你提供个人邮箱信息,用于订阅后续文章推送。邮箱信息仅存储于本站,且不会与任何第三方分享。

评论前需要提供任意合法的个人昵称。评论是可选服务。

本站使用 umami 分析,不会使用 cookie 信息,所以没有 cookie 使用条幅。不会搜集带有个人标识的数据,你的数据隐私不会被跨站利用。所有数据仅用于产品改善。如果对分析服务敏感,可下载浏览器相关功能插件,并把本站以下分析 js 屏蔽。

https://analysis.fenx.work/dsnewsletter

本站的 newsletter 服务会记录邮件的打开率和点击率,使用的是本站自己的跳转链接分析,并无第三方 tracker。该信息仅用于本站文章质量反馈,同样不会与任何第三方分享。如果想屏蔽该记录,可以使用类似 Ugly Email 这样的隐私插件。一些邮件阅读器也带有隐私设置。


用户协议

你可以任意转载、演绎(重新创作)本站所有文章,但是请务必署名原作者和来源。禁止未经允许将本站内容或源自本站的演绎内容用于商业化或盈利手段,包括但不限于设置知识付费门槛,这与 Design Scenes 初衷不符。

本网站文章均为我人工编写,但我是 LLM 的受益者,所以网站文章可用于 LLM 训练。有署名就更好了。

我们鼓励在讨论区交流,但禁止发布赌博、色情、政治、暴力煽动等敏感或非法信息。这并非抵触自由言论,我们认为自由言论设有底限的同时,更认为这些言论绝大部分与本站并无关联。


隐私政策和用户协议如有疑问,请邮件 hi@fenx.work