Mac Themes Garden 网站发布记
damien's zone
Introducing Mac Themes Garden!
简短版本
我“发布”了 Mac Themes Garden! 这是一个展示来自 Classic Mac 时代的 3000 多个(且数量还在增加) Kaleidoscope 主题的网站,随时可以查看、下载和探索! 快去看看吧! 哦,还有一个 RSS feed,你可以订阅它来查看新增/更新的主题!
是的,还有一个按钮,你可以把它放在你的网站上! 在这里获取! 是不是很可爱?
现在,开始唠叨了。 注意:如果你是在 RSS 阅读器中阅读这篇文章,你可能想要直接在我的网站上打开这篇文章,因为它包含大量的 CSS 演示,这些在 RSS 阅读器中无法正常工作。
一些背景
如果你在网上认识我,你可能知道我已经在 Bluesky/Mastodon/Cohost 上运行了几年 Mac Themes Bot。
这个 Bot 的想法很简单,以每小时一次的频率展示来自 Mac OS X 和 Classic Mac 时代的自定义主题。因为我受到了 Twitter 上的 @kaleidoscopemac 的启发,所以我一开始只展示为 ShapeShifter by Unsanity 制作的主题。 不久之后,那个 Twitter 帐户被暂停了。 因为我已经有了 现成的工具 来抓取和发布 OS X 的主题,所以我想我可以将 Kaleidoscope (OS 7/8/9) 的主题也加入其中。 在尝试(但失败)从 Bot 的作者那里获取原始数据集后,我开始在 Wayback Machine 中抓取 Kaleidoscope Scheme Archive 的记录。 从那时起,我的 Bot 每小时都会发布主题,偶数小时发布经典主题,奇数小时发布 OS X 主题[1]。
这些年过去了,Bot 一直在运行,让我和许多其他人在我运行它的各个网站上感到高兴。 Cohost 的化身尤其特别,因为 Cohost 是一个特别的网站,而且它充满了书呆子(褒义),人们会对其中的随机宝石感到疯狂。
大约在 2023 年初,我对这样一个事实感到沮丧:我拥有的 Kaleidoscope ~~主题~~ schemes 的图像只是那些来自 90 年代的 tiny .gif files。
“记录” 25 年前的 schemes
所以我做了合理的事情:建立了一个非常手动的过程来截图和记录 大约 4,000 个可用主题的作者信息,目的是在完成后创建一个网站来展示所有内容。
无需赘述细节(你可以在 这里 阅读),这个过程是这样的:
- 在 UTM.app (QEMU frontend) 中打开我的 Mac OS 9 VM
- 浏览 schemes 文件夹
- 选择一个给定的 scheme,应用它
- 拍摄 3 张截图:
- 一张 scheme 的关于框。
- 一张在 Finder/常规桌面情况下使用 scheme 的截图。
- 最后,一张应用程序 KSA Sampler 的截图,以模仿 Kaleidoscope Scheme Archive 中的原始截图。 该特定截图将被粘贴到 Photoshop 中,使用 Photoshop Actions 裁剪/去除其不透明背景。
- 写下 scheme 的名称、作者和发布年份
- 找到相应的 .sit 归档文件
- 将所有这些记录在 Airtable 数据库的记录中。
这是它在 Airtable 中的样子:
Airtable 数据库中的 6 行,显示了截图和 scheme 信息
在顺利的一天,我大概可以记录每小时十几个主题。
但是 Damien,你为什么不自动化这个过程?!
很高兴你问了! 我试过了! 但是,至少到目前为止,我找不到一种好的方法来可靠地执行我需要执行的操作。 这甚至还没有涉及到这样一个事实:有时一个 scheme 没有任何作者或年份信息! 有时 Kaleidoscope scheme 文件中的作者完全是错误的,而实际作者写在一个单独的“Read me”文件中。 有时几乎没有任何信息可以参考,有时 scheme 有 Bug,会导致我的 VM 崩溃。
在这一点上,我拥有的唯一自动化是:
- 一个 Keyboard Maestro macro,它调用运行中的 VM 的
qemu-monitor
,拍摄无损截图并将其放入我的剪贴板中,准备粘贴 - Photoshop actions 来:
- 从剪贴板创建一个文档
- 在裁剪后,删除文档中给定颜色的所有像素并修剪它以删除额外的 alpha 像素(对于上面的 KSA Sampler 截图)
- 一个 Quickeys 快捷方式,用于在 Mac OS 9 中调用“Hide Others”。 事实证明,在 Mac OS X 之前,这个操作没有键盘快捷键? 我不知道!
除此之外,恐怕这都是手动的,因为我想记录的数据不是通过简单地查看文件就能直接提取的 😔。
无论如何, 我当时想“我将完成所有这些 schemes 的记录,然后 我将制作这个网站”……
亲爱的读者,我还没有完成。 根据我自己的估计,我大概完成了一半。 我不知道我什么时候会完全完成,但我的好朋友 Sage 敦促我:赶紧把该死的网站做好。
他们是对的,我应该更早地制作网站! 这是一个有趣且具有挑战性的消遣,而且 该死的 这些天我可以用消遣来放松一下。
制作网站
因此,大约在今年一月份,我开始了网站的工作。
第一步是与 Airtable API 接口,以便从我的数据库下载数据并将所有资产存储到我的存储库中的一个文件夹中。 没什么太疯狂的,但必须完成。 我确实需要小心缓存并且不要重新下载我已经存储在磁盘上的图像。 我们谈论的是大约 2,500 行,每行包含 3 个图像,你不想每次脚本需要运行时都重新下载 7,500 个 PNG 文件。
有了手头的数据,真正的乐趣就开始了。
这个概念验证使用了 Eleventy,我选择它的唯一原因是因为我刚刚使用它重新制作了我的网站(你现在正在阅读它!),所以我认为它会很适合,因为它简单且快速。
如果不是因为 WebC 如此不稳定,它可能就是了。 你看,我知道我想在网站的布局中使用多个类似 OS9 的“窗口”。 所以我想,当然,WebC 可以让我制作一个“组件”并以这种方式制作 UI,而且,我对天发誓,我无法以这种方式使其工作。 似乎 WebC 纯粹是为了 W eb C omponents,仅此而已,这本身很好,但这行不通。 我简要地试验了一个 paired short code,但我不会在 JavaScript 字符串中编写 HTML。 我想在这个项目中获得乐趣。
细节
所以我切换到了 Astro,它的 components 概念更接近我想要做的事情,而且我已经知道如何使用它,因为 erambert.me 使用它。
我不会对网站的制作过程进行逐个播放的描述,因为一旦球开始滚动,我的大部分时间都花在了玩 UI 创意和使用 Astro 的 content collections 上,这样将整个网站构建为一堆静态页面就不会花费太长时间。 不要误会我的意思,Astro 已经足够快了,没有 Eleventy 那么快,但仍然很快!
“问题”是我在处理很大的数字。 让我们考虑一下它们:
- 3,942 个主题
- 868 位作者
目前,如果我们假设每个 scheme 一个页面,每个作者一个页面(这将列出该作者的所有主题),我们将得到: 3,942 + 868 = 4,810 页。 这并不可怕,但已经有很多页面了,而且我们没有对它做任何花哨的事情。
让我们开始变得花哨,让我们添加分页,假设我们想每页显示 51 个主题。 其中:
- T 是集合中的主题数 (3,942)
- A 是集合中的作者数 (868)
- P 是页面大小 (51)
const totalPages = (T * A) + Math.ceil(T / P)
// 4,888 pages
那是 4,888 页! 除非我想添加一些花哨的东西,比如作者页面 (A / 26 (每个字母一个)),这又是大约 33 页。 这使我们接近 5,000 页。 这很好,但这意味着每个页面最好都生成得非常快,以便构建时间不会失控。
这就是我必须小心的地方。 我希望作者页面显示给定作者制作的所有主题,我最初以这种方式天真地实现它:
export const getStaticPaths = (async () => {
const authors = await getCollection("authors");
return authors.map((a) => {
return {
props: { author: a },
params: {
author: a.data.slug,
},
};
});
}) satisfies GetStaticPaths;
const { author } = Astro.props;
const themesByAuthor = (await getCollection("themes")).filter((t) => {
return t.data.authors.some((a) => a.id === author.id);
});
当然,这看起来不错。 毕竟,它在开发中“只”需要大约 30 毫秒才能运行! 但是,根据我们之前的计算,这将为每个作者页面运行。 突然我们看到了:
30ms × 868 = 26,040 ms (26s) 😱!
毕竟,我们正在 868 次迭代 3,942 个 schemes,这不太好!
那么解决方案是什么?
当涉及到性能时,情况几乎总是如此:减少工作量,只做一次困难的工作! 我利用了 Astro 的 collection references。 我首先在我的 themes
schema 中声明一个 authors 的引用:
const themes = defineCollection({
loader: themesLoader,
schema: z.object({
name: z.string(),
+ authors: z.array(reference("authors")),
year: z.string().optional(),
mainThumbnail: z.string(),
thumbnails: z.array(z.string()),
archiveFile: z.string(),
// ...
}),
});
这意味着我可以简单地调用 getEntries 方法来获取给定主题的作者。
export const getStaticPaths = (async () => {
const authors = await getCollection("authors");
return authors.map((a) => {
return {
props: { author: a },
params: {
author: a.data.slug,
},
};
});
}) satisfies GetStaticPaths;
const { author } = Astro.props;
const themesByAuthor = await getEntries(author.data.themes);
根据主题的数量,最多需要大约 10 毫秒,对于大多数页面,它需要不到 5 毫秒! 这好多了。
通过应用这种技术,我设法控制了站点的构建时间,并且 Astro 在不到 16 秒的时间内构建了几乎 5,000 个页面,并在彼此之间进行了各种查询!
变得可爱
如上所述,我知道我想为网站模拟 Mac OS 9 UI。 现在,我 可以 只是使用图像来制作 UI... 但是那样还有什么乐趣呢?
所以当然,我使用了本书中的每一个 CSS 技巧来实现这个外观。 让我介绍一下一些 UI 组件,并解释我是如何重新创建它们的。
窗口框架
这显然是 UI 的一个重要组成部分,所以我希望尽可能接近 OS 9 的实际外观。 让我们看一个简单的空例子:
Welcome!
许多样式都涉及使用多个 box shadows 来实现主 UI chrome 的不同区域中的“broken border”效果。 这是主窗口主体的样式(在上面的预览中为白色):
.macos9-window-body {
border: 1px solid var(--primary-black);
box-shadow:
-1px -1px 0 rgb(from var(--primary-black) r g b / 40%),
1px 1px 0 var(--primary-white);
--top-left-shadow: var(--grays-600);
--bottom-right-shadow: var(--primary-white);
box-shadow:
-1px -1px 0 var(--top-left-shadow),
-1px 0px 0 var(--top-left-shadow),
0 -1px 0 var(--top-left-shadow),
1px 1px 0 var(--bottom-right-shadow),
1px 0 0 var(--bottom-right-shadow),
0 1px 0 var(--bottom-right-shadow);
background-color: var(--primary-white);
}
标题栏
那部分很有趣,有很多事情要做,所以让我们一步一步地看。 这是它的外观和 HTML 标记:
Welcome!
<div class="macos9-window-titlebar">
<button class="button close" data-action="close">
<span class="button-dots"></span>
</button>
<span class="filler"></span>
<span class="title-text">Welcome!</span>
<span class="filler"></span>
<button class="button zoom" data-action="zoom">
<span class="button-dots"></span>
</button>
<button class="button collapse" data-action="collapse">
<span class="button-dots"></span>
</button>
</div>
“条纹”图案是使用重复的 CSS 渐变和每个侧面带有略微不同渐变的两个伪元素完成的:
.macos9-window-titlebar > span.filler {
flex: 1;
background-color: #dddddd;
background-image: linear-gradient(#ffffff, #ffffff 50%, #777777 50%, #777777);
background-repeat: repeat;
background-size: 100% 2px;
height: 12px;
position: relative;
&::before,
&::after {
content: "";
position: absolute;
width: 1px;
background-size: 100% 2px;
display: block;
}
&::before {
left: 0;
top: 0;
bottom: 0;
background-image: linear-gradient(#fff, #fff 50%, #cccccc 50%, #cccccc);
border-bottom: 1px solid #cccccc;
}
&::after {
right: 0;
top: 0;
bottom: 0;
background-image: linear-gradient(#ccc, #ccc 50%, #777777 50%, #777777);
}
}
然后我们有了窗口按钮,它们是……你猜对了,很多 box-shadows 和 borders 放在一起:
.macos9-window-titlebar > button {
appearance: none;
border: none;
height: 13px;
width: 13px;
background: transparent;
background-image: linear-gradient(135deg, #9a9a9a 0%, #f1f1f1 100%);
background-size: 9px 9px;
background-position: center;
box-shadow:
inset 1px 1px 0 var(--grays-700),
inset -1px -1px 0 var(--primary-white),
inset 0 0 0 2px var(--primary-black),
inset 3px 3px 0 var(--primary-white),
inset -3px -3px 0 var(--grays-700);
position: relative;
z-index: 0;
&:active::before {
content: "";
inset: 2px;
background-image: linear-gradient(
135deg,
rgba(53, 53, 53, 0.8) 0%,
rgba(156, 156, 156, 0.8) 100%
);
display: block;
position: absolute;
z-index: 1;
}
}
.macos9-window-titlebar > button .button-dots {
position: absolute;
inset: 0;
display: block;
&::before {
content: "";
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 1px;
background-color: #cccccc;
}
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 1px;
height: 1px;
background-color: #cccccc;
}
}
哦,还有一个巧妙的技巧:我正在使用 :has()
来有时重新对齐标题,以便在每一侧的按钮数量不均匀时,它实际上在视觉上居中:
.macos9-window-titlebar:has(.button.close, .button.zoom, .button.close)
.button.close + .filler {
padding-left: calc(13px + var(--macos9-window-titlebar-gap));
}
按钮
这是设计的一部分,让我质疑我的理智和我对这个位元的承诺。 让我们看一下一个简单的按钮:
Press me!
看起来很简单,对吧? 好吧。 你如何在 CSS 中保留像素化的外观而不使用图像? 为什么,你当然会发疯,并使用 CSS Grid areas 自己绘制像素! 这是来自 Sage 的一个 suggestion。
这需要一些计划,因为 CSS Grid areas 必须是矩形的。 那时我去了 Photoshop,并为每个区域绘制了一堆带有独特颜色的彩色矩形:
内部/外部阴影需要覆盖 38 个区域,以及纯背景需要 4 个区域! 使用 JSX 生成它很容易,我们注意添加一个 data-n
属性,这将帮助我们在 CSS 中定位这些区域:
<a className="os9-button"><div className="grid">{Array.from({ length: 38 }).map((_, index) => (
<div
key={index}
className="shadow"
data-n={String(index + 1).padStart(2, "0")}
/>
))}<div className="bgd" data-n="1"></div><div className="bgd" data-n="2"></div><div className="bgd" data-n="3"></div><div className="bgd" data-n="4"></div><div className="label">{children}</div></div></a>
然后,我们必须使用 Sass 来创建必要的选择器:
@use "sass:math";
@for $i from 1 through 38 {
$n: $i;
@if $n < 10 {
$n: "0#{$n}";
}
.shadow[data-n="#{$n}"] {
grid-area: s#{$n};
}
}
@for $i from 1 through 4 {
.bgd[data-n="#{$i}"] {
grid-area: bg#{$i};
}
}
然后……我们用代码“绘制”我们的 CSS areas:
.grid {
display: grid;
grid-template-areas:
"... ... s01 s02 s02 s02 s03 ... ..."
"... s04 s05 s06 s06 s06 s07 s08 ..."
"s09 s10 s38 s11 s11 s11 s12 s13 s14"
"s15 s16 s17 s17 bg1 bg1 s18 s19 s20"
"s15 s16 s37 bg2 txt bg3 s18 s19 s20"
"s15 s16 s37 bg2 bg4 s21 s18 s19 s20"
"s22 s23 s24 s25 s26 s26 s27 s28 s29"
"... s30 s31 s32 s32 s32 s32 s33 ..."
"... ... s34 s35 s35 s35 s36 ... ...";
grid-template-columns: repeat(4, max-content) 1fr repeat(5, max-content);
grid-template-rows: repeat(4, max-content) 1fr repeat(4, max-content);
}
是的,这花了一段时间,并且尝试了多次才得到正确的 LMAO。 如果你很好奇,完整的 stylesheet is here。
随机花絮
窗口控制
这些窗口实际上是交互式的! 你可以通过单击正确的按钮/双击标题栏来“zoom”(展开)主窗口并折叠它!
Welcome!
Play with the buttons!
const windowElement = document.querySelector('#demo-window');
windowElement.querySelectorAll("button").forEach((button) => {
if (button.dataset.action === "collapse") {
const titlebar = windowElement.querySelector(".macos9-window-titlebar");
if (titlebar) {
titlebar.addEventListener("dblclick", (e) => {
if (e.target instanceof HTMLButtonElement) {
return;
}
windowElement.classList.toggle("collapsed");
window.getSelection()?.empty();
});
}
}
button.addEventListener("click", () => {
const action = button.dataset.action;
if (action === "collapse") {
windowElement.classList.toggle("collapsed");
} else if (action === "zoom") {
windowElement.classList.toggle("zoomed");
}
});
});
Open Graph 图像
在为每个主题制作 open graph 图像时,我可以采取简单的路线并简单地删除主窗口的 PNG,如下所示:
这在 Discord 等应用程序中非常可爱,但由于错误的纵横比和对 alpha 通道的不支持,在其他任何地方看起来都很糟糕。
所以我采取了另一种方法:
但是,显然,手动进行这种合成将是一个糟糕的主意,而且我对 ImageMagick 不够熟练,所以我最终使用了 Vercel 的 satori 来布局这两个图像并为每个主题生成一个图像:
import type { InferEntrySchema } from "astro:content";
import satori from "satori";
import sharp from "sharp";
export async function generateOpenGraphImageForTheme(
theme: InferEntrySchema<"themes">,
) {
let blurredImageData: Buffer | undefined;
const margin = 20;
const imageDimension = {
width: 1200,
height: 630,
};
if (theme.thumbnails.length > 1) {
blurredImageData = await sharp("public/" + theme.thumbnails[1])
.resize(imageDimension.width, imageDimension.height, {
fit: "cover",
position: "top",
})
.blur(5)
.toBuffer();
}
const mainThumbnailSharp = sharp("public" + theme.mainThumbnail);
const mainThumbnail = await mainThumbnailSharp.png().toBuffer();
const svg = await satori(
<div
style={{
display: "flex",
alignItems: "center",
justifyItems: "center",
width: "100%",
height: "100%",
position: "relative",
backgroundColor: "white",
}}
>{blurredImageData && (
<img
src={toArrayBuffer(blurredImageData)}
style={{
position: "absolute",
filter: "brightness(40%)",
inset: 0,
}}
/>
)}<img
src={toArrayBuffer(mainThumbnail)}
style={{
padding: margin,
width: "100%",
height: "100%",
boxSizing: "border-box",
objectFit: "contain",
objectPosition: "center center",
}}
/></div>,
{
width: imageDimension.width,
height: imageDimension.height,
fonts: [],
},
);
return sharp(Buffer.from(svg));
}
function toArrayBuffer(buffer: Buffer) {
const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
view[i] = buffer[i];
}
return arrayBuffer;
}
然后我把一个 script 放在一起,通过生成多个进程来处理所有图像,整个脚本在我的 M1 Max Mac Studio 上需要大约 2 分 30 秒才能运行并生成大约 3900 个图像。
作为奖励,这些图像以后也将在 Mac Themes Bot 可用时使用。
接下来是什么
我不知道需要多长时间,但我希望继续/完成“记录”我可以访问的 schemes。 希望明年我能完成 lolsob。 你应该订阅 RSS feed 以查看我在更新它们时的情况!
除此之外,我还有很多想法:
- 一个“按颜色搜索”功能
- 一种在适用时查看/展示每个 scheme 中包含的自定义图标的方法。 相信我,里面有一些宝石!
- 以某种方式找到一种将该站点连接到 InfiniteMac 的方法,以快速“live”查看 scheme
- 一个用户提交的运行站点中 schemes 的旧 Macs 图库。 如果你对此感兴趣,请与我联系 :)
就这样,伙计们,祝你生活愉快。 - damien
- 或者反之亦然,我真的不记得,也不想检查 Git 历史。 ↩︎
[ret