diff --git a/src/components/hero.tsx b/src/components/hero.tsx index 56bca33..f3c5da5 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -4,12 +4,26 @@ import { useEffect, useRef, useState, useCallback } from "react" import { site } from "@/data/site" const phrases = [ - { prefix: "折腾 Linux 的", highlight: "乐趣" }, - { prefix: "搭建博客的", highlight: "心得" }, - { prefix: "学习 Docker 的", highlight: "过程" }, - { prefix: "抵抗遗忘的", highlight: "文字" }, + { prefix: "一个人只拥有此生此世", highlight: "是不够的" }, + { prefix: "他还需要一个", highlight: "诗意的世界" }, + { prefix: "记录", highlight: "思考" }, + { prefix: "总结", highlight: "生活" }, ] +/** + * 首屏大字的「设计字号」:手机 / sm / md / lg 四档。 + * 自适应只会在此基础上**缩小**,不会放大 —— 短句保持原来的视觉分量, + * 只有放不下的长句才让步。改这里两处要同步(h1 和下面量尺寸的探针)。 + */ +const HERO_SIZE_CLASSES = "text-[min(10.5vw,3rem)] sm:text-6xl md:text-7xl lg:text-8xl" + +/** 缩放的绝对下限,防止句子太长时缩到读不清 */ +const HERO_MIN_FONT = 20 + +/** 光标的占位:ml-2 的 8px + w-[0.08em],算字号时要一起扣掉 */ +const CURSOR_GAP_PX = 8 +const CURSOR_EM = 0.08 + export function Hero() { const [mounted, setMounted] = useState(false) const [phraseIndex, setPhraseIndex] = useState(0) @@ -133,6 +147,66 @@ export function Hero() { } }, []) + /* + * 字号自适应。 + * + * 原来写死 text-[min(10.5vw,3rem)] sm:text-6xl …,配合 whitespace-nowrap: + * 句子一长(旧文案约 9.5 字宽就已经临界)就会横向溢出,手机上尤其明显。 + * + * 现在按「这一句的完整文本」实际量一次宽度来定字号,所以: + * - 任何长度的句子都恰好占满可用宽度,不溢出不换行 + * - 打字机逐字出现时不会重排(量的是完整句子,不是当前已打出的部分) + * - 字号只在句子切换时变一次,那一刻 h1 是空的,看不出跳动 + */ + const fullText = phrases[phraseIndex].prefix + phrases[phraseIndex].highlight + + const h1Ref = useRef(null) + const measureRef = useRef(null) + const designRef = useRef(null) + const [fontSize, setFontSize] = useState(undefined) + + const fitFontSize = useCallback(() => { + const h1 = h1Ref.current + const meas = measureRef.current + const probe = designRef.current + if (!h1 || !meas || !probe) return + + const available = h1.parentElement?.clientWidth ?? 0 + if (available <= 0) return + + // 测量用的 span 固定按 100px 渲染,所以 宽度/100 就是「1 个字号单位占几 px」 + const widthAt100 = meas.getBoundingClientRect().width + if (widthAt100 <= 0) return + + // 探针带着和 h1 一样的响应式字号类,用它的计算值当上限, + // 这样断点逻辑只写在 class 里一处,不用在 JS 里硬编码 + const design = Number.parseFloat(window.getComputedStyle(probe).fontSize) + if (!Number.isFinite(design) || design <= 0) return + + // 光标也占宽度(ml-2 + w-[0.08em]),不扣掉的话右边会顶出去一点 + const perEm = widthAt100 / 100 + CURSOR_EM + const fitted = Math.max(0, (available - CURSOR_GAP_PX) / perEm) + + const size = Math.min(design, Math.max(HERO_MIN_FONT, fitted)) + setFontSize(`${size.toFixed(1)}px`) + }, []) + + useEffect(() => { + fitFontSize() + + const parent = h1Ref.current?.parentElement + let observer: ResizeObserver | null = null + if (parent && typeof ResizeObserver !== "undefined") { + observer = new ResizeObserver(fitFontSize) + observer.observe(parent) + } + + // 网络字体加载完成后字宽会变,重新量一次 + document.fonts?.ready.then(fitFontSize).catch(() => {}) + + return () => observer?.disconnect() + }, [fitFontSize, fullText]) + return (
@@ -145,13 +219,39 @@ export function Hero() { {site.tagline}

-

+

{renderText()}

+ {/* + 两个量尺寸用的隐藏元素,外层 h-0 + overflow-hidden 保证它们 + 不参与布局、也不会撑出横向滚动条: + - measureRef:按 100px 渲染当前这句完整文本,用来算实际字宽 + - designRef :带着和 h1 相同的响应式字号类,用来读当前断点的设计字号 + */} + +

「我不知道我在想什么,直到我读到我写的东西。」 diff --git a/src/data/reading.json b/src/data/reading.json index d2e8de4..a3e57b8 100644 --- a/src/data/reading.json +++ b/src/data/reading.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-09-15T04:44:09.927Z", + "updatedAt": "2026-09-15T04:49:54.266Z", "source": "weread", "books": [ {