docs: move NOTES.md file

This commit is contained in:
Maze Winther 2026-05-02 16:38:06 +02:00
parent 0b7597b31f
commit 8ab3aeba7c
3 changed files with 206 additions and 176 deletions

View File

@ -1,149 +1,149 @@
import { resolveGraphicParamsAtTime } from "@/animation"; import { resolveGraphicParamsAtTime } from "@/animation";
import type { ElementAnimations } from "@/animation/types"; import type { ElementAnimations } from "@/animation/types";
import { buildDefaultParamValues } from "@/params/registry"; import { buildDefaultParamValues } from "@/params/registry";
import type { ParamValues } from "@/params"; import type { ParamValues } from "@/params";
import { graphicsRegistry } from "./registry"; import { graphicsRegistry } from "./registry";
import { import {
registerDefaultGraphics, registerDefaultGraphics,
ellipseGraphicDefinition, ellipseGraphicDefinition,
polygonGraphicDefinition, polygonGraphicDefinition,
rectangleGraphicDefinition, rectangleGraphicDefinition,
starGraphicDefinition, starGraphicDefinition,
} from "./definitions"; } from "./definitions";
import { import {
DEFAULT_GRAPHIC_SOURCE_SIZE, DEFAULT_GRAPHIC_SOURCE_SIZE,
type GraphicInstance, type GraphicInstance,
type GraphicDefinition, type GraphicDefinition,
} from "./types"; } from "./types";
const graphicPreviewUrlCache = new Map<string, string>(); const graphicPreviewUrlCache = new Map<string, string>();
const FALLBACK_CORNER_RADIUS_RATIO = 0.2; const FALLBACK_CORNER_RADIUS_RATIO = 0.2;
const FALLBACK_FILL_OPACITY = 0.08; const FALLBACK_FILL_OPACITY = 0.08;
const FALLBACK_MIN_FONT_SIZE = 12; const FALLBACK_MIN_FONT_SIZE = 12;
const FALLBACK_FONT_SIZE_RATIO = 0.15; const FALLBACK_FONT_SIZE_RATIO = 0.15;
function buildFallbackPreviewUrl({ function buildFallbackPreviewUrl({
name, name,
size, size,
}: { }: {
name: string; name: string;
size: number; size: number;
}): string { }): string {
const svg = ` const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"> <svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
<rect width="${size}" height="${size}" rx="${size * FALLBACK_CORNER_RADIUS_RATIO}" fill="white" fill-opacity="${FALLBACK_FILL_OPACITY}" /> <rect width="${size}" height="${size}" rx="${size * FALLBACK_CORNER_RADIUS_RATIO}" fill="white" fill-opacity="${FALLBACK_FILL_OPACITY}" />
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="white" font-size="${Math.max(FALLBACK_MIN_FONT_SIZE, size * FALLBACK_FONT_SIZE_RATIO)}" font-family="sans-serif">${name}</text> <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="white" font-size="${Math.max(FALLBACK_MIN_FONT_SIZE, size * FALLBACK_FONT_SIZE_RATIO)}" font-family="sans-serif">${name}</text>
</svg> </svg>
`; `;
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
} }
export function getGraphicDefinition({ export function getGraphicDefinition({
definitionId, definitionId,
}: { }: {
definitionId: string; definitionId: string;
}): GraphicDefinition { }): GraphicDefinition {
registerDefaultGraphics(); registerDefaultGraphics();
return graphicsRegistry.get(definitionId); return graphicsRegistry.get(definitionId);
} }
export function buildDefaultGraphicInstance({ export function buildDefaultGraphicInstance({
definitionId, definitionId,
}: { }: {
definitionId: string; definitionId: string;
}): GraphicInstance { }): GraphicInstance {
const definition = getGraphicDefinition({ definitionId }); const definition = getGraphicDefinition({ definitionId });
return { return {
definitionId, definitionId,
params: buildDefaultParamValues(definition.params), params: buildDefaultParamValues(definition.params),
}; };
} }
export function resolveGraphicParams( export function resolveGraphicParams(
definition: GraphicDefinition, definition: GraphicDefinition,
params?: ParamValues, params?: ParamValues,
): ParamValues { ): ParamValues {
return { return {
...buildDefaultParamValues(definition.params), ...buildDefaultParamValues(definition.params),
...(params ?? {}), ...(params ?? {}),
}; };
} }
export function resolveGraphicElementParamsAtTime({ export function resolveGraphicElementParamsAtTime({
element, element,
localTime, localTime,
}: { }: {
element: { element: {
definitionId: string; definitionId: string;
params: ParamValues; params: ParamValues;
animations?: ElementAnimations; animations?: ElementAnimations;
}; };
localTime: number; localTime: number;
}): ParamValues { }): ParamValues {
const definition = getGraphicDefinition({ const definition = getGraphicDefinition({
definitionId: element.definitionId, definitionId: element.definitionId,
}); });
return resolveGraphicParamsAtTime({ return resolveGraphicParamsAtTime({
params: resolveGraphicParams(definition, element.params), params: resolveGraphicParams(definition, element.params),
definitions: definition.params, definitions: definition.params,
animations: element.animations, animations: element.animations,
localTime, localTime,
}); });
} }
export function buildGraphicPreviewUrl({ export function buildGraphicPreviewUrl({
definitionId, definitionId,
params, params,
size = DEFAULT_GRAPHIC_SOURCE_SIZE, size = DEFAULT_GRAPHIC_SOURCE_SIZE,
}: { }: {
definitionId: string; definitionId: string;
params?: ParamValues; params?: ParamValues;
size?: number; size?: number;
}): string { }): string {
const definition = getGraphicDefinition({ definitionId }); const definition = getGraphicDefinition({ definitionId });
const resolvedParams = resolveGraphicParams(definition, params); const resolvedParams = resolveGraphicParams(definition, params);
const cacheKey = JSON.stringify({ definitionId, resolvedParams, size }); const cacheKey = JSON.stringify({ definitionId, resolvedParams, size });
const cachedUrl = graphicPreviewUrlCache.get(cacheKey); const cachedUrl = graphicPreviewUrlCache.get(cacheKey);
if (cachedUrl) { if (cachedUrl) {
return cachedUrl; return cachedUrl;
} }
if (typeof document === "undefined") { if (typeof document === "undefined") {
return buildFallbackPreviewUrl({ name: definition.name, size }); return buildFallbackPreviewUrl({ name: definition.name, size });
} }
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = size; canvas.width = size;
canvas.height = size; canvas.height = size;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (!ctx) { if (!ctx) {
return buildFallbackPreviewUrl({ name: definition.name, size }); return buildFallbackPreviewUrl({ name: definition.name, size });
} }
definition.render({ definition.render({
ctx, ctx,
params: resolvedParams, params: resolvedParams,
width: size, width: size,
height: size, height: size,
}); });
const previewUrl = canvas.toDataURL("image/png"); const previewUrl = canvas.toDataURL("image/png");
graphicPreviewUrlCache.set(cacheKey, previewUrl); graphicPreviewUrlCache.set(cacheKey, previewUrl);
return previewUrl; return previewUrl;
} }
export { export {
DEFAULT_GRAPHIC_SOURCE_SIZE, DEFAULT_GRAPHIC_SOURCE_SIZE,
ellipseGraphicDefinition, ellipseGraphicDefinition,
graphicsRegistry, graphicsRegistry,
polygonGraphicDefinition, polygonGraphicDefinition,
rectangleGraphicDefinition, rectangleGraphicDefinition,
registerDefaultGraphics, registerDefaultGraphics,
starGraphicDefinition, starGraphicDefinition,
}; };
export type { export type {
GraphicDefinition, GraphicDefinition,
GraphicInstance, GraphicInstance,
GraphicRenderContext, GraphicRenderContext,
} from "./types"; } from "./types";

View File

@ -1,27 +0,0 @@
# Notes
## `Transform` is misplaced
`Transform` is currently defined in `apps/web/src/rendering/index.ts`. It's
exported from the rendering domain because rendering happens to be one of its
consumers — but every other domain that touches scene geometry consumes it too
(`@/timeline`, `@/preview`, `@/animation/values`, `@/text`, etc.). It's not
"of" rendering any more than `Vector2` would be "of" math.
`Transform` is closer to a primitive than a domain concept. It's infrastructure:
a small value type with no behavior, no dependencies, no domain-specific
invariants. Anything that wants to position something on a 2D canvas needs it.
The codebase should be refactored so it's defined lower-level, rather than
sitting in a domain. Candidate homes:
- `apps/web/src/primitives/transform.ts` (new dir for these value types)
- `apps/web/src/geometry/transform.ts` if `Vector2` and friends end up there
- `apps/web/src/rendering/transform.ts` is acceptable only if `@/rendering`
becomes a primitives folder rather than a domain (it's borderline today —
it also exports `BlendMode`, which has the same problem).
Side effect of fixing this: `@/rendering/animation-values.ts` (which exists
only because `Transform` lives next to it) can move into `@/animation/values.ts`
alongside the other resolve-at-time helpers, since `Transform` would no longer
pull in a domain dependency.

View File

@ -0,0 +1,57 @@
# Primitives vs domains
The codebase has a recurring smell: **primitive value types defined inside
domain folders**. The clearest current example is `Transform`, which lives in
`apps/web/src/rendering/index.ts`. Rendering happens to consume it — but so do
`@/timeline`, `@/preview`, `@/animation`, `@/text`, and anything else that
positions things on a 2D canvas. It's not "of" rendering; rendering just owns
the file.
## The test
If a type can be described without mentioning clips, tracks, effects, layers,
keyframes, or any other product concept — and it has no behavior beyond shape
— it's a **primitive**. The moment a type needs to know what a clip is, it
has crossed into domain territory.
Primitives have:
- No domain-specific invariants (a 2D position doesn't care that it's a clip's
position; it's just `{ x, y }`).
- No dependencies on other parts of the app — they're leaves.
- Multiple unrelated consumers across domains.
- A name that would make sense in any 2D editor / video tool / graphics lib.
Domains, in contrast, can name things that only make sense given the rest of
the product (`TimelineElement`, `Effect`, `GraphicDefinition`, `MediaAsset`).
## Why it matters
When a primitive lives in a domain folder, every other domain that consumes it
takes a misleading dependency — `@/timeline` ends up importing from
`@/rendering` not because timeline needs rendering, but because that's where
`Transform` happens to sit. The dependency graph lies, and pieces that should
move freely become anchored to the wrong layer.
## The refactor
Move primitives out of domain folders into a primitives location (somewhere
like `apps/web/src/primitives/`, or split by concern — `geometry/`, `time/`,
`color/`, etc.). Whatever the bucket, the rule is "no product concepts, no
behavior, no upward dependencies".
Don't bulk-move. Each move is deliberate — the right destination depends on
what other primitives already exist and what naming convention has emerged.
## Side effects to watch for
Files often end up parked next to misplaced primitives because they had nowhere
better to live. Example: `apps/web/src/rendering/animation-values.ts` exists
only because `Transform` lives next door. Once `Transform` moves to a primitive
location, that file collapses back into `apps/web/src/animation/values.ts`
alongside the other resolve-at-time helpers — there's no remaining reason to
split them.
When moving a primitive, look at what *else* in its current folder only exists
because of that primitive. Those usually want to move too (or merge somewhere
else once the anchor is gone).