用Go语言实现PR出生365天视频数字滚动效果,其实没你想的那么难
- 单站
- 2026-06-28 16:02:44
- 800
前两天我朋友在群里晒娃,发了个出生365天的纪念视频,数字从“第1天”一直滚动到“第365天”,那效果看着挺酷的,他问我能不能帮他做个类似的HTML页面,我就琢磨着用Go语言来搞一个后端生成方案,你别说,折腾下来发现这东西看着花哨,底层逻辑其实挺朴素的。
先说清楚:这个数字滚动到底是个啥?
说白了,就是你需要一个 “数字在某段时间内从0变到365” 的动画效果,放在视频里,可能是一帧一帧地变化,放在网页上,可能就是JavaScript配合CSS的transition或者requestAnimationFrame来实现,但如果你想要服务端生成视频帧,再合成MP4文件的话,那Go语言就派上大用场了。
我用的方案不是直接让Go去操作浏览器DOM,而是用Go生成一系列图片帧,然后用FFmpeg把这些帧合并成视频,这样做的好处是:最终输出的是一个标准的视频文件,你可以在任何剪辑软件里继续调整。
第一步:搭环境,装库
Go这边需要几个库:
import (
"image"
"image/color"
"image/draw"
"image/font"
"image/png"
"os"
"strconv"
"strings"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font/gofont/goregular"
)
image和相关子包:用来画图freetype:用来在图片上写字,而且是那种带抗锯齿的漂亮字goregular:Go官方提供的一个免费字体,省得去下载
装的时候注意:go get github.com/golang/freetype 可能需要网络比较稳,我前两天在公司内网装了半天没装上去,后来换了手机热点才搞定,所以如果你也遇到类似问题,别急。
第二步:生成数字滚动的每一帧
关键逻辑其实就一个循环:
totalFrames := 365 // 一共生成多少帧
framesPerDigit := 10 // 每个数字停留多少帧(让滚动看起来有停顿感)
outputDir := "./frames"
for i := 0; i <= totalFrames; i++ {
// 每个数字重复生成 framesPerDigit 帧
for j := 0; j < framesPerDigit; j++ {
framePath := filepath.Join(outputDir, fmt.Sprintf("frame_%04d.png", i*framesPerDigit+j))
genFrame(i, framePath)
}
}
这里的 genFrame 函数做的事情很单纯:画背景、画数字、画辅助装饰,我加了点小心思:数字变成366的时候(也就是第366天),背景颜色会微微泛黄,表示一个完整年的结束。
func genFrame(day int, path string) {
width, height := 1920, 1080
img := image.NewRGBA(image.Rect(0, 0, width, height))
// 背景色渐变(带一点点生活气息的暖色)
bgColor := color.RGBA{30, 30, 40, 255}
if day == 365 {
bgColor = color.RGBA{50, 45, 35, 255}
}
draw.Draw(img, img.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src)
// 加载字体
fontBytes := goregular.TTF
f, _ := freetype.ParseFont(fontBytes)
// 画数字
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(f)
c.SetFontSize(180) // 大字号,滚动时才有气势
c.SetClip(img.Bounds())
c.SetDst(img)
c.SetSrc(image.NewUniform(color.RGBA{255, 255, 255, 255}))
// 把数字居中
text := strconv.Itoa(day)
// 注意:这里要自己算宽度,不同长度数字偏移不同
pt := freetype.Pt(960- int(float64(len(text))*float64(180)*0.6)/2, 540+60)
c.DrawString(text, pt)
// 保存
fout, _ := os.Create(path)
png.Encode(fout, img)
fout.Close()
}
上面那段代码里有个我临时想到的细节:字体宽度的估算,180像素的字体,每个英文字母大概占0.6个字符宽度,我瞎猜的0.6,结果跑出来居然差不多居中,你要是不放心可以用c.MeasureString()精确测量,不过说实话,肉眼看着差不多就行了,又不是出版印刷。
第三步:用FFmpeg合成视频
图片帧有了,接下来就是甩给FFmpeg,我直接写了个Go函数来调系统命令:
func renderVideo(outputFile string) error {
cmd := exec.Command("ffmpeg",
"-framerate", "30", // 30帧/秒
"-i", "./frames/frame_%04d.png",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-r", "30",
outputFile,
)
return cmd.Run()
}
这里我踩过一个坑:帧的命名要对齐,如果某一天漏了一帧,FFmpeg就直接罢工或者跳过那一部分,你看我前面fmt.Sprintf用了%04d,就是为了保证都是四位数,比如frame_0001.png、frame_3650.png这样。
如果帧数很多(比如365天,每个数字重复10帧,那就是3650帧),生成速度可能会有点慢,我测试下来,一帧大概0.2秒,全跑完大概12分钟,你可以在genFrame里把framesPerDigit改小一点,比如5,那就变成1825帧,6分钟搞定。
第四步:让滚动效果更“像”一下
纯数字滚动其实很单调,我加了个速度曲线,数字在刚开始和快结束的时候变化慢一点,中间快一点,这个思路来自视频剪辑里的“缓入缓出”。
func easedFrames(totalFrames int, maxDay int) []int {
var frames []int
for i := 0; i < totalFrames; i++ {
t := float64(i) / float64(totalFrames) // 0.0 ~ 1.0
// 缓入缓出公式:3t² - 2t³
eased := 3*t*t - 2*t*t*t
day := int(eased * float64(maxDay))
if day > maxDay {
day = maxDay
}
frames = append(frames, day)
}
return frames
}
这个函数替换掉之前的for i := 0; i <= totalFrames; i++循环,效果嘛,就是数字在前几帧变化慢悠悠的,中间嗖嗖地跳,最后又慢下来停在365,我第一次跑出来的时候,看着那数字从1慢慢变到2,心里还挺有成就感。
第五步:附加上文字和装饰
光秃秃一个数字不好看,我加了点文案,宝贝的第 X 天”、“成长记录”这些,用同样的freetype方法再画一行字,只是字号调小到60,颜色用浅灰色。
c.SetFontSize(60)
subText := fmt.Sprintf("成长的第 %d 天", day)
pt2 := freetype.Pt(960- int(float64(len([]rune(subText)))*float64(60)*0.7)/2, 700)
c.DrawString(subText, pt2)
注意中文的处理!len([]rune(subText))才能正确计数中文字符,否则会把一个汉字算成3个char,导致位置偏到姥姥家,我一开始就忘了,结果“成长的第 1 天”中间那个数字直接歪到了屏幕外面,笑死。
一些小坑和临时补救
说到坑,还有几个我必须说一下:
-
字体文件:
goregular是英文字体,不支持中文,如果你要写中文,得额外下载一个中文字体,比如NotoSansSC-Regular.ttf,然后fontBytes := ioutil.ReadFile("NotoSansSC-Regular.ttf"),我就是这么干的,下载完放到项目根目录。 -
内存占用:如果你生成几千帧图片,每一帧都是1920x1080的RGBA图像,内存占用会飙升,我建议每生成100帧就调用一次
runtime.GC(),或者直接一帧一帧写入磁盘,别全堆在内存里。 -
FFmpeg版本:我本地装的是4.4,公司服务器是3.4,结果
-framerate和-r的顺序不同版本的FFmpeg理解不一样,建议你在代码里加个错误重试或者版本检测,我懒得搞,直接统一写-framerate 30 -i ... -r 30,目前没出问题。
如果想在网页上直接显示这个滚动效果,不用视频呢?
那更简单了,Go这边只需要提供一个API,返回当前是第几天,前端用setInterval或者requestAnimationFrame来做数字动画即可,不过那就是另一个故事了,今天主要说服务端生成视频的方案。
// 一个极简的HTTP API示例
http.HandleFunc("/day", func(w http.ResponseWriter, r *http.Request) {
day := time.Now().Sub(birthDate).Hours() / 24
fmt.Fprintf(w, `{"day": %d}`, int(day))
})
然后前端用CSS的transition或者transform: translateY()来模拟数字滚动,这个实现起来代码量更少,但效果取决于前端性能。
最后说点有用的
这个方案最核心的点在于:你不需要任何视频剪辑软件,纯代码就能生成一个带数字滚动效果的纪念视频,Go的好处是跑起来快、跨平台、部署简单,我上次在树莓派4上跑过,一个晚上能生成完。
如果你不想生成完整的3650帧,也可以只生成关键帧,然后用FFmpeg的tblend或者minterpolate滤镜做插帧,这样帧数变少但流畅度不减,不过那个滤镜参数我调了半天没调好,暂时放弃了,等后面有空再试。
对了,如果你用我上面的代码跑出来发现数字位置偏左,别慌,把那个0.6的系数改一改就行,我的经验是:数字位数越多,偏移越明显,你可以根据数字长度动态算一个偏移量。
嗯……大概就这些了,你要是真动手做,遇到Go编译报错或者FFmpeg出奇怪提示,大概率是路径问题或者没装依赖,先跑个小测试,比如只生成前10帧,验证通了再全量跑,别一上来就搞3650帧,万一中间挂了,心态容易崩。
