一整屏光点
一开始就是一整屏光点,配上一行行浮现的字。好看,但太抽象,看完记不住讲了什么。
没有拍摄,也没有剪辑软件。每一帧都是让 AI 写代码画出来的:星空、深海、算法、一辆倒挂着开的跑车。
游戏宣传片 · 1:00 · 站内播放
我用 AI 做的 3D 外滩游戏,在游戏里录的实机画面。
配乐:Epic trailer action music 2 — humanoide9000(freesound.org,CC BY 4.0),有截取和淡入淡出。
排片表 · PROGRAMME
14 期科普短片分成 5 个系列,加上两支游戏宣传片、一支三维短片和一支 MV。看过的会盖章,记进小城护照。
一个直觉上不可能的结论,用两分钟把它推出来。
算法里藏着的处世方法。
城市、交通和系统里反常识的事。
历史、关系和我们自己。
离家最远的那些东西。
在自己做的游戏里录的实机画面。
照着文字里的意象建出来的地方。
自己写的歌,配一支动画。

新一期先发在抖音、小红书、视频号,都搜「小丁AI折腾」。想看下一期拆什么,在评论区点名。
在手机上直接打开抖音主页 ↗
后台 · BACKSTAGE
左边这段代码来自《携你入星空》的制作工程,右边是它画出来的那一帧。每一期都是一个网页:AI 写好画面和时间线,浏览器按帧渲染,再把几千张图拼成视频。
// 心形:标准心形曲线,转一个小角度,落在冥王星左下那片平原上
const HEART_PTS = (() => {
const pts = [];
for (let k = 0; k < 360; k++) {
const th = k / 360 * 6.283,
xr = 16 * Math.pow(Math.sin(th), 3) / 17,
yr = (13 * Math.cos(th) - 5 * Math.cos(2 * th)
- 2 * Math.cos(3 * th) - Math.cos(4 * th)) / 17;
const u = xr * .96 + yr * .26, v = -xr * .26 + yr * .96; // 反旋转
pts.push([u / 2.05 + .14, -(v - .15) / 2.05 + .12]);
}
return pts;
})();
// 冥王星:预先画进一张离屏图(带那颗"心"),之后可以整体缩放,
// 也可以先缩小再放大做出"像素化"的效果
const PL = document.createElement('canvas'); PL.width = PL.height = 800;
(() => {
const g = PL.getContext('2d'), R = 400, img = g.createImageData(800, 800);
for (let py = 0; py < 800; py++) for (let px = 0; px < 800; px++) {
const x = (px - R) / R, y = (py - R) / R, r2 = x * x + y * y, i = (py * 800 + px) * 4;
if (r2 > 1) { img.data[i + 3] = 0; continue; }
const z = Math.sqrt(1 - r2);
let c = [196, 160, 124];
const n = .55 * Math.sin(x * 5.3 + 1.7 * Math.sin(y * 3.1))
+ .3 * Math.sin(y * 7.7 + 2.3 * Math.sin(x * 4.2))
+ .15 * (hash(Math.floor(px / 6), Math.floor(py / 6)) - .5);
c = mix(c, [150, 110, 86], clamp(n * .3 + .25));
// 赤道附近一条暗红色的带(克苏鲁区一带),心形区域里是很亮的冰
const dark = clamp(1 - Math.abs(y - .22 - .15 * Math.sin(x * 3)) / .16) * clamp((-.05 - x) * 4);
c = mix(c, [92, 48, 38], dark * .85);
if (inPoly(HEART_PTS, x, y)) c = mix(c, [236, 220, 200], .85);
const shade = .25 + .75 * Math.pow(z, .6); // 边缘变暗
img.data[i] = c[0] * shade; img.data[i + 1] = c[1] * shade;
img.data[i + 2] = c[2] * shade; img.data[i + 3] = 255;
}
g.putImageData(img, 0, 0);
})();
function drawPluto(cx, cy, R, a, pixel = 0) {
if (a <= 0) return;
ctx.save(); ctx.globalAlpha = a;
if (pixel > 1) {
// 先缩到 (2R/pixel) 见方,再关掉平滑放大回来:越早,格子越粗
const n = Math.max(1, Math.round(2 * R / pixel));
PIX.width = PIX.height = n;
const pg = PIX.getContext('2d'); pg.clearRect(0, 0, n, n); pg.drawImage(PL, 0, 0, n, n);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(PIX, cx - R, cy - R, 2 * R, 2 * R);
ctx.imageSmoothingEnabled = true;
} else ctx.drawImage(PL, cx - R, cy - R, 2 * R, 2 * R);
ctx.restore();
// 大气层:一圈很薄的蓝色雾(新视野号拍到过冥王星的蓝色霾层)
lx.strokeStyle = rgba([150, 190, 255], .3 * a); lx.lineWidth = 4;
lx.beginPath(); lx.arc(cx, cy, R + 3, 0, 6.283); lx.stroke();
}
function heartLine(cx, cy, R, a, glow = 1) {
if (a <= 0) return;
lx.strokeStyle = rgba(COL.heart, .5 * a); lx.lineWidth = 2;
lx.beginPath();
HEART_PTS.forEach(([x, y], i) => {
const X = cx + x * R, Y = cy + y * R;
i ? lx.lineTo(X, Y) : lx.moveTo(X, Y);
});
lx.closePath(); lx.stroke();
if (glow) {
const [x, y] = [cx + .2 * R, cy + .1 * R];
const g = lx.createRadialGradient(x, y, 0, x, y, R * .6);
g.addColorStop(0, rgba(COL.heart, .07 * a * glow));
g.addColorStop(1, rgba(COL.heart, 0));
lx.fillStyle = g; lx.fillRect(x - R, y - R, 2 * R, 2 * R);
}
}
FRAME · 《携你入星空》02:00 · 成片截帧一开始就是一整屏光点,配上一行行浮现的字。好看,但太抽象,看完记不住讲了什么。
前两秒就有一半人划走;字放在屏幕底部,被抖音的标题挡住了;两分钟的片子,后半段留不住人。每一期都在改这些。
每个场景要有一个占半屏的具体东西,不能只有光点;第 0 帧就要有字和主体;每个数字必须能查到出处,算出来的数字要由代码现场算;画面要跟着音乐的拍子走。
票根 · 小城护照
每看一部记 10 经验,和开车、读文章记在同一本护照里。