PS1 · Specific Graphics

特定图形:规格驱动开发

一门规格说明 + 测试并重的作业。从弱规格开始,逐步强化,用 lerp 与 Bézier 曲线绘制复杂静态图形,并设计动画工具箱。

基本信息

PS1规格说明测试先行Bézier动画

官方讲义:web.mit.edu/6.102/ps1 ↗

作业要求

核心练习顺序:lerp → 弱规格 → lerpColorfillGradientmakePalette → 强规格 → interpolate → Bézier 函数 → 学生设计的动画工具箱。

重要:curves.ts 中的占位参数(renameMe: 0renameMe: undefined)是故意的。作业要求你强化规格说明并改善静态检查,所以不要只是绕过它们写代码。

规格说明

类型定义

type Color = [number, number, number]; // RGB 分量,0..255 整数
type Point = { x: number; y: number };

浮点比较容差:0.001

需实现的函数

  1. lerp(v0, v1, t): number — 先创建故意弱化的版本供 lerpColor 使用;后强化以支持 interpolate。为两种规格写不同的合法客户测试。
  2. lerpColor(c0, c1, t): Color — 0 ≤ t ≤ 1,线性插值每个 RGB 分量,使用 half-up 取整。
  3. fillGradient(colors: Map<number,Color>): number — 填充缺失的整数步,线性插值,返回插入的键数量。
  4. makePalette(color, n): Color[] — 将基色转 HSL,均匀选取 n 个色相,保持饱和度/亮度。
  5. interpolate(v0, v1, easing, t): number — 先对 t 应用缓动函数,再做线性插值。拒绝 t 超出 [0,1]。
  6. Bézier 函数bezierInterpolatebezierPath 需要你重命名占位参数、弱化静态前提、同时强化整体规格。
  7. 动画工具箱 — 设计最多三个函数,使讲义中的动画简洁。先规格 → 测试 → 实现。

完成清单

  • 弱 lerp 规格/测试/实现
  • lerpColor
  • fillGradient
  • makePalette
  • 强 lerp 规格/测试/实现
  • interpolate
  • 强化的 Bézier 规格 + 测试 + 实现
  • 动画工具箱 + 示例

源码骨架

lerp.ts — 弱/强规格练习

const todo = (name: string): never => { throw new Error(`TODO: implement ${name}`); };

function lerpWeak(v0: number, v1: number, t: number): number { void v0; void v1; void t; return todo('lerpWeak'); }
function lerpStrong(v0: number, v1: number, t: number): number { void v0; void v1; void t; return todo('lerpStrong'); }

// 在作业过程中,随规格强化切换导出的实现
export const lerp = lerpWeak;
export const forTestingOnly = { lerpWeak, lerpStrong };

colors.ts — 颜色转换工具

export type Color = [number, number, number];
const todo = (name: string): never => { throw new Error(`TODO: implement ${name}`); };

export function lerpColor(c0: Color, c1: Color, t: number): Color { void c0; void c1; void t; return todo('lerpColor'); }
export function fillGradient(colors: Map<number, Color>): number { void colors; return todo('fillGradient'); }
export function makePalette(color: Color, n: number): Color[] { void color; void n; return todo('makePalette'); }
export function interpolate(v0: number, v1: number, easing: (ti:number)=>number, t:number): number { void v0; void v1; void easing; void t; return todo('interpolate'); }

curves.ts — 故意弱化的 Bézier 签名

export type Point = { x: number; y: number };
const todo = (name: string): never => { throw new Error(`TODO: specify + implement ${name}`); };

// These deliberately weak/awkward starter signatures mirror the public TypeDoc exercise.
export function bezierInterpolate(controlPoints: Point[], renameMe: 0): Point { void controlPoints; void renameMe; return todo('bezierInterpolate'); }
export function bezierPath(controlPoints: Point[], easing:(ti:number)=>number, renameMe: undefined): Point[] { void controlPoints; void easing; void renameMe; return todo('bezierPath'); }

utils.ts — 测试工具与颜色转换

import assert from 'node:assert/strict';
import type { Color } from './colors.js';

export const TOLERANCE = 0.001;
export function assertApproxEqual(actual:number, expected:number, tolerance=TOLERANCE): void {
  assert.ok(Math.abs(actual-expected) <= tolerance, `expected ${actual} ≈ ${expected}`);
}

// Clean-room utility replacements for the color-conversion helpers the handout says are provided.
export function rgbToHsl([r0,g0,b0]: Color): [number,number,number] { /* ... HSL conversion ... */ }
export function hslToRgb([h,s,l]: [number,number,number]): Color { /* ... RGB conversion ... */ }

toolbox.ts — 动画工具箱(待设计)

// PS1 intentionally asks YOU to design at most three useful animation-toolbox functions.
// Write specifications first, then tests, then implementations. Do not start by inventing code here.
export {};