§ 背景
shoka 主题的音乐播放器有一项招牌视觉设计:专辑封面外围叠加一张黑胶唱片纹理,播放时唱片旋转、唱针落下,营造实体唱机的沉浸感。此前的实现中,所有专辑共用同一张默认盘面图 play_disc.png——一张通用黑色黑胶纹理。
问题在于:不同专辑的实体唱片往往有独特的盘面设计——彩色透明胶、画胶、异色标签、甚至签名刻纹。若能在数字播放器中还原每张专辑独特的黑胶视觉,便能大幅提升浏览体验的沉浸感和收藏感。
为此,主题新增了 {% media audio %} 标签的 disc= 参数,允许为每张专辑指定自定义黑胶盘面图像。同时配套提供两个 Python 脚本,用于从实体唱片照片中提取透明圆环素材。
§ 修改的文件
本次改动涉及主题 llxlr/hexo-theme-shoka 子模块,共 3 个文件:
| # | 文件 | 改动内容 |
|---|---|---|
| 1 | scripts/tags/media.js | 解析 disc= 参数,注入 data-disc HTML 属性 |
| 2 | source/css/_common/components/tags/player.styl | 盘面伪元素背景改为 CSS 变量 --disc-image,带默认 fallback |
| 3 | source/js/_app/player.js | 播放器创建时从 data-disc 读取路径,设置 --disc-image 行内样式 |
§ 架构设计
数据流:
Markdown disc= 参数
↓ Hexo generate
HTML data-disc 属性(内联于 <div class="player">)
↓ 页面加载 → player.js
CSS --disc-image 变量(行内 style)
↓ 浏览器渲染
.disc::after 伪元素背景图(叠加在封面 img 之上)
§ 实现细节
§ 1. Tag 参数解析(media.js)
在标签渲染函数中,解析第二个参数中的 disc= 前缀:
// scripts/tags/media.js(第 11 行) | |
var discAttr = args[1] && args[1].startsWith("disc=") ? ' data-disc="' + args[1].slice(5) + '"' : ""; | |
// 第 15 行 — 注入到 HTML | |
return `<div class="media-container"><div class="player" | |
data-type="${args[0]}"${discAttr} | |
data-src='${JSON.stringify(list).replace(/'/g, "'")}'> | |
</div></div>`; |
当 args[1] 以 disc= 开头时(如 disc=/images/vinyl/O1.png),裁剪前缀后作为 data-disc 属性值注入 .player 容器;否则不设置该属性,走默认盘面。
设计选择:用独立的
data-disc属性而非编码到data-srcJSON 中,是为了避免对现有 YAML 数据结构做侵入式修改。data-src承载的是第三方 API 元数据(歌单/专辑信息),盘面图片是前端视觉层的独立配置,二者职责分离。
§ 2. CSS 变量 Fallback(player.styl)
// source/css/_common/components/tags/player.styl(第 325 行)
.disc {
animation: rotate 20s linear infinite;
animation-play-state: paused;
padding: 1.5rem;
max-height: 144px;
&::after {
content: "";
position: absolute;
background: var(--disc-image, url("../images/play_disc.png"))
no-repeat center/contain;
z-index: 1;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
}
关键设计在于 var(--disc-image, url("../images/play_disc.png")):
- 设置
--disc-image时:使用自定义盘面图像 - 未设置时:自动 fallback 到默认的
play_disc.png
::after 伪元素覆盖在 .disc 容器(含 padding: 1.5rem 和 max-height: 144px)之上,通过 width/height: 100% 撑满容器,position: absolute 脱离文档流后覆盖在专辑封面 <img> 之上。由于 ::after 在堆叠上下文中处于 <img> 上方,盘面纹理自然覆盖封面。当专辑封面旋转时(.disc 的 rotate 动画),::after 作为 .disc 的子元素一起旋转,视觉上封面与盘面浑然一体。
为什么用 CSS 变量而非
background-image直接覆盖?
CSS 变量提供了声明式的 fallback 语义。如果直接在 JS 中操作background-image,当data-disc不存在时背景为空——因为行内style="background-image: ..."会覆盖 CSS 文件中的规则,即使值为空字符串也会覆盖。CSS 变量的 fallback 机制完美解决了这个「不设置就用默认值」的需求。
§ 3. JS 读取并注入(player.js)
// source/js/_app/player.js(第 547-548 行) | |
var discImg = t.getAttribute('data-disc') | |
var discStyle = discImg | |
? ' style="--disc-image: url(' + discImg + ')"' | |
: '' | |
// 第 550 行 — 注入到 .disc 元素 | |
this.el.innerHTML = '<div class="cover"><div class="disc"' + discStyle + '> | |
<img src="' + (current.cover) + '" class="blur" /></div></div>' |
在 preview.create() 中(播放器 DOM 创建的入口),从容器元素 t 读取 data-disc,存在则生成 style="--disc-image: url(...)" 内联样式,直接设置在 .disc div 上。
Pjax 兼容性:
preview.create()在每次切歌时被调用,而data-disc是静态 HTML 属性(由 Hexo 在生成阶段写入),不依赖 JS 全局状态。Pjax 导航时播放器组件会被重新初始化,preview.create()重新读取新页面的.player元素,自然获取该页的data-disc值。
§ 生成自定义黑胶盘面图像
自定义盘面图像需要满足两个条件:
- 正方形——播放器的
.disc容器和::after伪元素使用contain模式填充,图片需为正方形以避免拉伸变形 - 中心透明——黑胶盘面只有外环可见,中心标签区域需为透明,才能露出底下的专辑封面
为此提供了两个 Python 脚本。
§ 脚本一:square.py — 图片裁方
将任意比例的唱片照片裁成正方形,支持四种裁切模式:
# source/downloads/code/py/square.py | |
from PIL import Image | |
import argparse | |
def cover(img, size): | |
"""等比缩放+中心裁切(类似 CSS object-fit: cover,最常用)""" | |
w, h = img.size | |
ratio = max(size / w, size / h) | |
new_w, new_h = int(w * ratio), int(h * ratio) | |
img = img.resize((new_w, new_h), Image.LANCZOS) | |
left = (new_w - size) // 2 | |
top = (new_h - size) // 2 | |
return img.crop((left, top, left + size, top + size)) |
| 模式 | 行为 | 适用场景 |
|---|---|---|
cover | 等比缩放填满,裁剪溢出部分 | 唱片照片需要紧密贴边(默认推荐) |
center | 取长边中心最大正方形 | 照片已经大致正方形 |
contain | 等比缩放留边,可选背景色填充 | 需要保留完整唱片的边缘细节 |
stretch | 直接拉伸变形 | 不推荐,除非刻意追求扭曲效果 |
使用方法:
# 默认 cover 模式,输出 1024×1024 | |
python square.py vinyl_photo.jpg vinyl_square.png | |
# 指定尺寸和模式 | |
python square.py vinyl_photo.jpg vinyl_square.png --size 2048 --mode contain --bg 0,0,0 |
尺寸建议:播放器中
.disc的max-height: 144px,但考虑到 Retina 屏幕和未来可能的布局变化,推荐输出至少 1024×1024。过小会导致盘面纹理模糊,过大则浪费带宽。O1.png~O12.png实际使用的尺寸在 800~1200px 之间,文件大小在 20~60KB。













§ 脚本二:mask_center.py — 抠除中心标签
在正方形唱片图上抠出一个中心透明圆洞,仅保留外环纹理:
# source/downloads/code/py/mask_center.py | |
from PIL import Image | |
import math | |
import argparse | |
def remove_center(input_path, output_path, ratio=0.33): | |
""" | |
将图片中心圆形区域抠掉,仅保留外环。 | |
:param ratio: 内圆半径占外圆半径的比例,默认 0.33。 | |
如果抠多了就调小,抠少了就调大。 | |
""" | |
img = Image.open(input_path).convert("RGBA") | |
w, h = img.size | |
cx, cy = w / 2.0, h / 2.0 | |
# 外半径取中心到边缘的最小距离(即内接圆半径) | |
outer_r = min(cx, cy) | |
inner_r = outer_r * ratio | |
pixels = img.load() | |
for y in range(h): | |
for x in range(w): | |
dx = x - cx + 0.5 | |
dy = y - cy + 0.5 | |
if math.hypot(dx, dy) < inner_r: | |
r, g, b, a = pixels[x, y] | |
pixels[x, y] = (r, g, b, 0) # Alpha 置零 | |
img.save(output_path) |
关键参数 --ratio 控制内圆半径占外圆半径的比例:
| ratio | 效果 | 适用场景 |
|---|---|---|
0.33 | 内圆占外圆 1/3,约等于常见黑胶标签大小(默认) | 大多数标准黑胶 |
0.25 | 更小的中心孔,保留更多纹理 | 画胶、彩胶等纹理铺满的唱片 |
0.40 | 更大的中心孔,纹理仅在外缘 | 标签特别大的老唱片 |
使用方法:
# 基础用法(ratio = 0.33) | ||
python mask_center.py vinyl_square.png disc_ring.png | ||
# 微调内圆大小 | ||
python mask_center.py vinyl_square.png disc_ring.png --ratio 0.28 |













§ 完整工作流程
实际示例——从原始照片到最终效果:
# 1. 将唱片照片裁成正方形 | ||
python square.py raw_vinyl_ts1.jpg ts1_square.png --size 1024 | ||
# 2. 抠除中心标签,保留黑胶外环 | ||
python mask_center.py ts1_square.png O1.png --ratio 0.33 | ||
# 3. 放入博客目录 | ||
cp O1.png source/images/vinyl/O1.png | ||
# 4. 在 Markdown 中使用 |
{% media audio disc=/images/vinyl/O1.png %} | |
- title: Taylor Swift - Debut | |
list: | |
- https://music.163.com/#/playlist?id=17962685264 | |
{% endmedia %} |
§ 视觉效果说明
在播放器中,视觉层次从上到下依次为:
| 层级 | 元素 | 说明 |
|---|---|---|
| 最上层 | .cover::before(唱针) | play_needle.png,播放时顺时针旋转落下 |
| 第二层 | .disc::after(盘面纹理) | 自定义 PNG(或默认 play_disc.png),中心透明 |
| 第三层 | .disc > img(专辑封面) | 圆形裁剪(border-radius: 50%),透过中心透明区域可见 |
| 底层 | .disc 容器 | 旋转动画载体,padding: 1.5rem 提供盘面扩展空间 |
盘面纹理的透明中心圆孔恰好露出底下的圆形专辑封面,而外环的黑胶纹理与沟槽则覆盖在封面四周的 padding 区域。加上圆形的封面 <img> 使用 border-radius: 50%,整体视觉上就像一张真实的黑胶唱片——中心是专辑标签,外围是黑胶纹路。
默认盘面
play_disc.png是一张 14KB 的 PNG,中间透明圆洞、外环为深灰色黑胶纹理。所有自定义盘面图像遵循相同的格式约定:正方形 PNG,中心圆形透明区域。
§ 相关提交
| 仓库 | Commit | 日期 | 说明 |
|---|---|---|---|
| 主题 | 8c9c569 | 2026-07-15 | ✨ 添加对媒体标签的支持,优化播放器样式以支持自定义唱片封面图像 |