
Frame Data Rollup
- 1 installs
- 4.3k repo stars
- Updated June 21, 2026
- nexu-io/html-video
A native Remotion data video frame where bars grow from zero by real data via spring physics while the figures roll from 0 to their target values.
About
A React/Remotion video-frame template that animates a small bar chart, growing bars from zero and counting numbers up to real values using the Remotion frame clock. A developer uses it to make weekly metrics or KPIs feel alive inside a video.
- Native Remotion (useCurrentFrame, interpolate, spring), not HTML+GSAP
- Static poster preview only; feed it real data with a handful of bars
Frame Data Rollup by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,200 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nexu-io/html-video --skill frame-data-rollupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 4.3k |
| Last updated | June 21, 2026 |
| Repository | nexu-io/html-video ↗ |
What it does
A native Remotion data video frame where bars grow from zero by real data via spring physics while the figures roll from 0 to their target values.
Files
【模板: 数据滚动帧 (Data Rollup)】 【类型】原生 Remotion 模板 (RFC-08 Phase 2) — 不是 HTML+GSAP,而是 React-tsx 直接吃 Remotion 的帧时钟 (useCurrentFrame + interpolate + spring)。这是让用户主动给数据帧挂上的动效增强:hyperframes 出静态/CSS 图表,本模板让柱子真的按数据长出来、数字真的滚动,一眼可辨。
【意图】数据展示帧 — 每周指标 / 增长柱 / KPI,数值应该动起来而不是干放着。适合塞进一条以 hyperframes 为主的视频里的某一个数据段。
【画布】默认 1920×1080,布局全部基于真实画布尺寸 (useVideoConfig) 计算,9:16 / 1:1 也不会压扁。
【数据契约 (inputProps)】
data.items[]— 每项{label, value},就是要画的柱子;最多约 7 根,多了会挤。data.title?— 左上标题。data.unit?— 数字后缀,如K/%。accent?/background?/foreground?— 配色,默认番橙#FF5A2C/ 近黑#0E0E10/ 纸白#F5F5F2。
【动效结构】
- 每根柱子按
spring()从 0 长到value/maxValue高度,逐根错峰 (0.12s 一根) 级联进场。 - 数字用与柱子同一条 growth 曲线从 0 滚到
value,柱子和数字同时落定。 - 标题 spring 淡入 + 上移;label 在柱子长到 40% 时淡入。
【内容纪律】
- 喂真实数据,别用占位数字。
value必须是数;非数会被当 0(enhanceFrameNative应在绑定前归一/校验)。- 柱子数控制在 7 根内。
【离线确定性】纯系统字体栈、内联样式,无外部字体 / 资源 / 网络 — 保证 headless 渲染确定、不黑屏 (对齐桥接的 neutralizeBlockingResources 教训)。
【原创】原创 React-tsx,标准动画柱状图词汇 (count-up + spring 生长) 从零实现,无特定上游来源 (provenance origin = none)。
Example — frame-data-rollup
A native Remotion data frame: five weekday bars grow from zero while their figures roll up to the real values, finishing together.
Inputs
{
"data": {
"title": "This week on GitHub",
"items": [
{ "label": "Mon", "value": 1200 },
{ "label": "Tue", "value": 2400 },
{ "label": "Wed", "value": 1800 },
{ "label": "Thu", "value": 4200 },
{ "label": "Fri", "value": 3600 }
]
}
}Variant — units + custom palette
{
"data": {
"title": "Stars by month",
"unit": "K",
"items": [
{ "label": "Mar", "value": 12 },
{ "label": "Apr", "value": 28 },
{ "label": "May", "value": 47 }
]
},
"accent": "#34D399",
"background": "#0B1120"
}How it renders
engine: remotion,mode: native, composition idDataRollup.- The data above arrives as Remotion
inputProps; the component animates it with
spring() (bar height) + interpolate() (rolling number).
- Bars cascade in with a per-bar stagger; each number and its bar settle together.
- This is the enhancement a user opts a single data frame into — neighbor frames
in the same video stay hyperframes HTML, and ffmpeg concatenates them into one MP4.
{
"name": "@html-video/template-frame-data-rollup",
"version": "0.1.0",
"description": "html-video template: frame-data-rollup (native Remotion data-animation — bars grow + numbers roll from real data; RFC-08 Phase 2)",
"license": "Apache-2.0",
"private": true,
"files": ["template.html-video.yaml", "source", "SKILL.md", "example.md", "preview.png"]
}
// frame-data-rollup — the first NATIVE Remotion template (RFC-08 Phase 2).
//
// This is what the HTML bridge can't do: bars GROW from real data via spring()
// and numbers ROLL from 0 to their target via interpolate(), every value driven
// by the actual data array fed through inputProps — not a static chart screenshot.
// That visible "the numbers are alive" difference is the whole point of letting a
// user opt a data frame into Remotion enhancement.
//
// Offline-deterministic by construction: system font stack only, no external
// fonts / assets / network (the same all-black-frame trap the bridge's
// neutralizeBlockingResources() guards against — here we just never introduce it).
import React from 'react';
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
export interface DataRollupItem {
label: string;
value: number;
}
export interface DataRollupProps {
data: {
title?: string;
/** Optional unit suffix shown after each rolled number, e.g. "K", "%". */
unit?: string;
items: DataRollupItem[];
};
/** Accent color for the bars + rolled numbers. */
accent?: string;
background?: string;
foreground?: string;
}
const SYSTEM_SANS =
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
const SYSTEM_MONO =
'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace';
const DEFAULTS: Required<Omit<DataRollupProps, 'data'>> = {
accent: '#FF5A2C',
background: '#0E0E10',
foreground: '#F5F5F2',
};
/** Format a rolled number: thousands separators, no decimals while counting. */
function fmt(n: number): string {
return Math.round(n).toLocaleString('en-US');
}
export const DataRollup: React.FC<DataRollupProps> = (props) => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
const data = props.data ?? { items: [] };
const items = Array.isArray(data.items) ? data.items : [];
const accent = props.accent ?? DEFAULTS.accent;
const background = props.background ?? DEFAULTS.background;
const foreground = props.foreground ?? DEFAULTS.foreground;
const unit = data.unit ?? '';
// Bar-height scaling. Normally each bar is linear against the largest value.
// But when one value dwarfs the rest (e.g. 61,059 stars next to 142 systems),
// linear scaling crushes the small bars to a 2px sliver. So if the spread is
// extreme (max ≥ 50× the smallest positive value), switch to a log scale so
// every bar stays legible. The rolled NUMBERS always show the true value —
// only the bar HEIGHT is remapped.
const values = items.map((it) => (Number.isFinite(it.value) ? it.value : 0));
const maxValue = Math.max(1, ...values);
const positives = values.filter((v) => v > 0);
const minPositive = positives.length > 0 ? Math.min(...positives) : maxValue;
const useLog = minPositive > 0 && maxValue / minPositive >= 50;
// Map a raw value → 0..1 fraction of the tallest bar.
const heightFrac = (value: number): number => {
if (value <= 0) return 0;
if (!useLog) return value / maxValue;
// Log scale: smallest positive bar reads ~25% tall, the tallest 100%.
const logMin = Math.log(minPositive);
const logMax = Math.log(maxValue);
const t = (Math.log(value) - logMin) / (logMax - logMin || 1);
return 0.25 + t * 0.75;
};
// Title fades + slides up over the first ~0.5s.
const titleProgress = spring({ frame, fps, config: { damping: 200 } });
const titleY = interpolate(titleProgress, [0, 1], [24, 0]);
// Layout maths (all in px, scaled off the real canvas so 9:16 / 1:1 work too).
const padX = Math.round(width * 0.08);
const chartTop = Math.round(height * (data.title ? 0.26 : 0.16));
const chartBottom = Math.round(height * 0.82);
const chartHeight = chartBottom - chartTop;
const slotW = items.length > 0 ? (width - padX * 2) / items.length : 0;
const barW = Math.min(slotW * 0.52, Math.round(width * 0.12));
return (
<AbsoluteFill style={{ backgroundColor: background, fontFamily: SYSTEM_SANS }}>
{data.title ? (
<div
style={{
position: 'absolute',
top: Math.round(height * 0.1),
left: padX,
right: padX,
color: foreground,
fontSize: Math.round(height * 0.058),
fontWeight: 800,
letterSpacing: '-0.02em',
opacity: titleProgress,
transform: `translateY(${titleY}px)`,
}}
>
{data.title}
</div>
) : null}
{items.map((it, i) => {
const value = Number.isFinite(it.value) ? it.value : 0;
// Each bar starts growing on a staggered delay so they cascade in.
const delay = i * Math.round(fps * 0.12);
const grow = spring({
frame: frame - delay,
fps,
config: { damping: 14, mass: 0.7, stiffness: 90 },
});
const barHeight = Math.max(2, heightFrac(value) * chartHeight * grow);
// The number rolls from 0 → value tracking the same growth curve, so the
// figure and the bar finish together.
const rolled = value * grow;
const cx = padX + slotW * i + slotW / 2;
const labelColor = foreground;
return (
<React.Fragment key={`${it.label}-${i}`}>
{/* rolled number, sits just above the bar top */}
<div
style={{
position: 'absolute',
left: cx - slotW / 2,
width: slotW,
top: chartBottom - barHeight - Math.round(height * 0.07),
textAlign: 'center',
color: accent,
fontFamily: SYSTEM_MONO,
fontSize: Math.round(height * 0.04),
fontWeight: 700,
opacity: grow,
}}
>
{fmt(rolled)}
{unit ? ` ${unit}` : ''}
</div>
{/* the bar */}
<div
style={{
position: 'absolute',
left: cx - barW / 2,
width: barW,
bottom: height - chartBottom,
height: barHeight,
backgroundColor: accent,
borderRadius: `${Math.round(barW * 0.12)}px ${Math.round(barW * 0.12)}px 0 0`,
}}
/>
{/* label under the baseline */}
<div
style={{
position: 'absolute',
left: cx - slotW / 2,
width: slotW,
top: chartBottom + Math.round(height * 0.025),
textAlign: 'center',
color: labelColor,
fontSize: Math.round(height * 0.028),
fontWeight: 500,
opacity: interpolate(grow, [0, 0.4], [0, 0.85], { extrapolateRight: 'clamp' }),
}}
>
{it.label}
</div>
</React.Fragment>
);
})}
{/* baseline rule */}
<div
style={{
position: 'absolute',
left: padX,
right: padX,
top: chartBottom,
height: 2,
backgroundColor: foreground,
opacity: 0.18,
}}
/>
</AbsoluteFill>
);
};
// Bundle entry point — passed to @remotion/bundler bundle() by the Remotion
// adapter's native render path. registerRoot wires the DataRollup composition
// into Remotion. (RFC-08 Phase 2)
import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';
registerRoot(RemotionRoot);
// Root — registers the native data-rollup composition. The adapter's render.ts
// overrides width/height/fps/durationInFrames per call via selectComposition()
// and feeds the real data through inputProps. Mirrors bridge/Root.tsx so a 9:16
// or 1:1 frame isn't squashed into 16:9. (RFC-08 Phase 2)
import React from 'react';
import { Composition } from 'remotion';
import { DataRollup, type DataRollupProps } from './DataRollup';
const SAMPLE: DataRollupProps = {
data: {
title: 'This week on GitHub',
unit: '',
items: [
{ label: 'Mon', value: 1200 },
{ label: 'Tue', value: 2400 },
{ label: 'Wed', value: 1800 },
{ label: 'Thu', value: 4200 },
{ label: 'Fri', value: 3600 },
],
},
};
export const RemotionRoot: React.FC = () => {
return (
<Composition
id="DataRollup"
component={DataRollup}
// Placeholder metadata; render.ts overrides all of these at render time.
durationInFrames={150}
fps={30}
width={1920}
height={1080}
defaultProps={SAMPLE}
calculateMetadata={({ props }) => ({
width: (props as { width?: number }).width ?? 1920,
height: (props as { height?: number }).height ?? 1080,
})}
/>
);
};
spec_version: 1
id: frame-data-rollup
name: Data Rollup
description: >
A native Remotion data-animation frame: bars grow from zero by real data via
spring physics while the figures roll up from 0 to their target in sync. The
first template that uses Remotion's frame clock directly (useCurrentFrame +
interpolate + spring) rather than the HTML bridge — this is what makes numbers
visibly come alive, which a static HTML chart can't. Meant to be dropped onto a
single data frame inside an otherwise hyperframes video, as a user-chosen
motion enhancement.
engine: remotion
engine_version: ^4.0.0
source_entry: source/entry.ts
native:
compositionId: DataRollup
category: data-viz
subcategory: bar-chart
tags: [data, chart, bar, rollup, count-up, remotion, native, animated, kpi]
best_for:
- "A data frame where the numbers should animate, not sit static"
- "Weekly metrics / growth bars driven by real values"
- "Enhancing one data segment of an otherwise hyperframes video"
not_for:
- "Decorative title cards or brand intros (use a hyperframes template)"
- "Datasets with more than ~7 bars (they get cramped)"
output:
formats: [mp4, webm]
default_format: mp4
resolution:
default: { width: 1920, height: 1080 }
supported_aspects: ["16:9", "9:16", "1:1"]
fps:
default: 30
supported: [30, 60]
duration:
type: variable
min_sec: 3
max_sec: 8
alpha: true
audio:
supported: false
expected_inputs: []
inputs:
schema:
type: object
required: [data]
properties:
data:
type: object
required: [items]
properties:
title:
type: "string"
maxLength: 48
description: "Optional heading shown top-left"
unit:
type: "string"
maxLength: 4
description: "Optional suffix after each rolled number, e.g. K or %"
items:
type: "array"
maxItems: 7
description: "The bars; each grows to value and rolls 0→value"
items:
type: object
required: [label, value]
properties:
label: { type: "string", maxLength: 16 }
value: { type: "number" }
accent:
type: "string"
description: "Bar + number color (hex). Default #FF5A2C"
background:
type: "string"
description: "Background color (hex). Default #0E0E10"
foreground:
type: "string"
description: "Title / label / baseline color (hex). Default #F5F5F2"
examples:
- {"data": {"title": "This week on GitHub", "items": [{"label": "Mon", "value": 1200}, {"label": "Tue", "value": 2400}, {"label": "Wed", "value": 1800}, {"label": "Thu", "value": 4200}, {"label": "Fri", "value": 3600}]}}
- {"data": {"title": "Stars by month", "unit": "K", "items": [{"label": "Mar", "value": 12}, {"label": "Apr", "value": 28}, {"label": "May", "value": 47}]}, "accent": "#34D399", "background": "#0B1120"}
license:
spdx: Apache-2.0
attribution_required: true
redistribution_allowed: true
commercial_use: true
provenance:
# L1 — no external design lineage; this is an original component.
origin:
name: "none"
kind: none
reference: >
Original work. Standard animated-bar-chart vocabulary (count-up figures,
spring-grown bars) implemented from scratch as React-tsx for Remotion; not
derived from any specific studio or upstream template.
# L2 — no upstream skill; written directly against the Remotion API.
via_skill:
name: "none"
author: "nexu-io"
url: https://github.com/nexu-io/html-video
license: Apache-2.0
source_file: "(original — packages/adapter-remotion native template, written for html-video)"
# L3 — what this template does
transformation: >
Original Remotion React-tsx composition. Bars grow with spring() on a staggered
per-bar delay; figures roll 0→value via interpolate tracking the same growth
curve so number and bar settle together. All layout is computed off the live
canvas size (useVideoConfig) so 16:9 / 9:16 / 1:1 all render correctly. System
font stack only — no external fonts or assets, keeping the headless render
deterministic and offline-safe.
author:
name: nexu-io
url: https://github.com/nexu-io
contact: open-design@nexu.io
version: 0.1.0
changelog:
- version: 0.1.0
date: 2026-06-07
notes: First native Remotion template (RFC-08 Phase 2) — data-driven bar growth + number rollup.
preview:
poster: preview.png
performance:
reference_render:
duration_sec: 5
render_wall_clock_sec: 45
machine: "M2 MacBook Air"