PS1 · Specific Graphics
特定图形:规格驱动开发
一门规格说明 + 测试并重的作业。从弱规格开始,逐步强化,用 lerp 与 Bézier 曲线绘制复杂静态图形,并设计动画工具箱。
作业要求
核心练习顺序:lerp → 弱规格 → lerpColor → fillGradient → makePalette → 强规格 → interpolate → Bézier 函数 → 学生设计的动画工具箱。
重要:curves.ts中的占位参数(renameMe: 0和renameMe: undefined)是故意的。作业要求你强化规格说明并改善静态检查,所以不要只是绕过它们写代码。
规格说明
类型定义
type Color = [number, number, number]; // RGB 分量,0..255 整数
type Point = { x: number; y: number };
浮点比较容差:0.001
需实现的函数
lerp(v0, v1, t): number— 先创建故意弱化的版本供lerpColor使用;后强化以支持interpolate。为两种规格写不同的合法客户测试。lerpColor(c0, c1, t): Color— 0 ≤ t ≤ 1,线性插值每个 RGB 分量,使用 half-up 取整。fillGradient(colors: Map<number,Color>): number— 填充缺失的整数步,线性插值,返回插入的键数量。makePalette(color, n): Color[]— 将基色转 HSL,均匀选取 n 个色相,保持饱和度/亮度。interpolate(v0, v1, easing, t): number— 先对 t 应用缓动函数,再做线性插值。拒绝 t 超出 [0,1]。- Bézier 函数 —
bezierInterpolate和bezierPath需要你重命名占位参数、弱化静态前提、同时强化整体规格。 - 动画工具箱 — 设计最多三个函数,使讲义中的动画简洁。先规格 → 测试 → 实现。
完成清单
- 弱 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 {};