AI 正在绞尽脑汁想思路 ING···
AI 摘要
DeepSeek & Kimi

§ 背景

shoka 主题的音乐播放器有一项招牌视觉设计:专辑封面外围叠加一张黑胶唱片纹理,播放时唱片旋转、唱针落下,营造实体唱机的沉浸感。此前的实现中,所有专辑共用同一张默认盘面图 play_disc.png——一张通用黑色黑胶纹理。

问题在于:不同专辑的实体唱片往往有独特的盘面设计——彩色透明胶、画胶、异色标签、甚至签名刻纹。若能在数字播放器中还原每张专辑独特的黑胶视觉,便能大幅提升浏览体验的沉浸感和收藏感。

为此,主题新增了 {% media audio %} 标签的 disc= 参数,允许为每张专辑指定自定义黑胶盘面图像。同时配套提供两个 Python 脚本,用于从实体唱片照片中提取透明圆环素材。

§ 修改的文件

本次改动涉及主题 llxlr/hexo-theme-shoka 子模块,共 3 个文件:

#文件改动内容
1scripts/tags/media.js解析 disc= 参数,注入 data-disc HTML 属性
2source/css/_common/components/tags/player.styl盘面伪元素背景改为 CSS 变量 --disc-image,带默认 fallback
3source/js/_app/player.js播放器创建时从 data-disc 读取路径,设置 --disc-image 行内样式

§ 架构设计

📝 Markdown 文章
📜 scripts/tags/media.js<i>Hexo tag 插件</i>解析 disc= 参数🖥️ HTML 输出<div class='player' data-disc='/images/vinyl/O1.png' ...>⚙️ player.js:preview.create()读取 data-disc → 设置 --disc-image🎨 player.styl.disc::after {background: var(--disc-image, url(play_disc.png))}💿 最终效果自定义黑胶盘面覆盖在专辑封面之上,一起旋转

数据流

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, "&#39;")}'>
</div></div>`;

args[1]disc= 开头时(如 disc=/images/vinyl/O1.png),裁剪前缀后作为 data-disc 属性值注入 .player 容器;否则不设置该属性,走默认盘面。

设计选择:用独立的 data-disc 属性而非编码到 data-src JSON 中,是为了避免对现有 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.5remmax-height: 144px)之上,通过 width/height: 100% 撑满容器,position: absolute 脱离文档流后覆盖在专辑封面 <img> 之上。由于 ::after 在堆叠上下文中处于 <img> 上方,盘面纹理自然覆盖封面。当专辑封面旋转时(.discrotate 动画),::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 值。

§ 生成自定义黑胶盘面图像

自定义盘面图像需要满足两个条件:

  1. 正方形——播放器的 .disc 容器和 ::after 伪元素使用 contain 模式填充,图片需为正方形以避免拉伸变形
  2. 中心透明——黑胶盘面只有外环可见,中心标签区域需为透明,才能露出底下的专辑封面

为此提供了两个 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

尺寸建议:播放器中 .discmax-height: 144px,但考虑到 Retina 屏幕和未来可能的布局变化,推荐输出至少 1024×1024。过小会导致盘面纹理模糊,过大则浪费带宽。O1.pngO12.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

§ 完整工作流程

不满意满意📷 找一张实体唱片盘面照片(俯拍)🟩 square.py裁成正方形1024×1024⭕ mask_center.py抠除中心标签ratio=0.33👀 预览输出 PNG检查透明圆洞位置🔧 调整 --ratio或换素材重新裁💾 保存到source/images/vinyl/📝 文章中使用

实际示例——从原始照片到最终效果:

# 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日期说明
主题8c9c5692026-07-15✨ 添加对媒体标签的支持,优化播放器样式以支持自定义唱片封面图像

§ 参考链接

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

星旅人 微信支付

微信支付

星旅人 支付宝

支付宝


在提问之前,你应该学会如何提问