当前位置:首页 > 单站 > 正文

用Go语言实现PR出生365天视频数字滚动效果,其实没你想的那么难

  • 单站
  • 2026-06-28 16:02:44
  • 800
摘要: 前两天我朋友在群里晒娃,发了个出生365天的纪念视频,数字从“第1天”一直滚动到“第365天”,那效果看着挺酷的,他问我能不能帮...

前两天我朋友在群里晒娃,发了个出生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.pngframe_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帧,万一中间挂了,心态容易崩。

用Go语言实现PR出生365天视频数字滚动效果,其实没你想的那么难