diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-05-02 16:38:06 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-05-02 16:38:06 +0200 |
| commit | 8ab3aeba7c99cb634254d5c1f52fab3f5f4811b1 (patch) | |
| tree | e7913d52cf35223ec95fa7c10e86b1c6b2a44cf4 | |
| parent | 0b7597b31fa956c7f3d4838923b545ba5e341422 (diff) | |
| download | opencut-8ab3aeba7c99cb634254d5c1f52fab3f5f4811b1.tar.gz | |
docs: move NOTES.md file
| -rw-r--r-- | apps/web/src/graphics/index.ts | 298 | ||||
| -rw-r--r-- | apps/web/src/rendering/NOTES.md | 27 | ||||
| -rw-r--r-- | notes/primitives-vs-domains.md | 57 |
3 files changed, 206 insertions, 176 deletions
diff --git a/apps/web/src/graphics/index.ts b/apps/web/src/graphics/index.ts index 3f277d40..7f5ac08b 100644 --- a/apps/web/src/graphics/index.ts +++ b/apps/web/src/graphics/index.ts @@ -1,149 +1,149 @@ -import { resolveGraphicParamsAtTime } from "@/animation";
-import type { ElementAnimations } from "@/animation/types";
-import { buildDefaultParamValues } from "@/params/registry";
-import type { ParamValues } from "@/params";
-import { graphicsRegistry } from "./registry";
-import {
- registerDefaultGraphics,
- ellipseGraphicDefinition,
- polygonGraphicDefinition,
- rectangleGraphicDefinition,
- starGraphicDefinition,
-} from "./definitions";
-import {
- DEFAULT_GRAPHIC_SOURCE_SIZE,
- type GraphicInstance,
- type GraphicDefinition,
-} from "./types";
-
-const graphicPreviewUrlCache = new Map<string, string>();
-
-const FALLBACK_CORNER_RADIUS_RATIO = 0.2;
-const FALLBACK_FILL_OPACITY = 0.08;
-const FALLBACK_MIN_FONT_SIZE = 12;
-const FALLBACK_FONT_SIZE_RATIO = 0.15;
-
-function buildFallbackPreviewUrl({
- name,
- size,
-}: {
- name: string;
- size: number;
-}): string {
- const svg = `
- <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}" />
- <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>
- `;
- return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
-}
-
-export function getGraphicDefinition({
- definitionId,
-}: {
- definitionId: string;
-}): GraphicDefinition {
- registerDefaultGraphics();
- return graphicsRegistry.get(definitionId);
-}
-
-export function buildDefaultGraphicInstance({
- definitionId,
-}: {
- definitionId: string;
-}): GraphicInstance {
- const definition = getGraphicDefinition({ definitionId });
- return {
- definitionId,
- params: buildDefaultParamValues(definition.params),
- };
-}
-
-export function resolveGraphicParams(
- definition: GraphicDefinition,
- params?: ParamValues,
-): ParamValues {
- return {
- ...buildDefaultParamValues(definition.params),
- ...(params ?? {}),
- };
-}
-
-export function resolveGraphicElementParamsAtTime({
- element,
- localTime,
-}: {
- element: {
- definitionId: string;
- params: ParamValues;
- animations?: ElementAnimations;
- };
- localTime: number;
-}): ParamValues {
- const definition = getGraphicDefinition({
- definitionId: element.definitionId,
- });
- return resolveGraphicParamsAtTime({
- params: resolveGraphicParams(definition, element.params),
- definitions: definition.params,
- animations: element.animations,
- localTime,
- });
-}
-
-export function buildGraphicPreviewUrl({
- definitionId,
- params,
- size = DEFAULT_GRAPHIC_SOURCE_SIZE,
-}: {
- definitionId: string;
- params?: ParamValues;
- size?: number;
-}): string {
- const definition = getGraphicDefinition({ definitionId });
- const resolvedParams = resolveGraphicParams(definition, params);
- const cacheKey = JSON.stringify({ definitionId, resolvedParams, size });
- const cachedUrl = graphicPreviewUrlCache.get(cacheKey);
- if (cachedUrl) {
- return cachedUrl;
- }
-
- if (typeof document === "undefined") {
- return buildFallbackPreviewUrl({ name: definition.name, size });
- }
-
- const canvas = document.createElement("canvas");
- canvas.width = size;
- canvas.height = size;
- const ctx = canvas.getContext("2d");
- if (!ctx) {
- return buildFallbackPreviewUrl({ name: definition.name, size });
- }
-
- definition.render({
- ctx,
- params: resolvedParams,
- width: size,
- height: size,
- });
-
- const previewUrl = canvas.toDataURL("image/png");
- graphicPreviewUrlCache.set(cacheKey, previewUrl);
- return previewUrl;
-}
-
-export {
- DEFAULT_GRAPHIC_SOURCE_SIZE,
- ellipseGraphicDefinition,
- graphicsRegistry,
- polygonGraphicDefinition,
- rectangleGraphicDefinition,
- registerDefaultGraphics,
- starGraphicDefinition,
-};
-export type {
- GraphicDefinition,
- GraphicInstance,
- GraphicRenderContext,
-} from "./types";
+import { resolveGraphicParamsAtTime } from "@/animation"; +import type { ElementAnimations } from "@/animation/types"; +import { buildDefaultParamValues } from "@/params/registry"; +import type { ParamValues } from "@/params"; +import { graphicsRegistry } from "./registry"; +import { + registerDefaultGraphics, + ellipseGraphicDefinition, + polygonGraphicDefinition, + rectangleGraphicDefinition, + starGraphicDefinition, +} from "./definitions"; +import { + DEFAULT_GRAPHIC_SOURCE_SIZE, + type GraphicInstance, + type GraphicDefinition, +} from "./types"; + +const graphicPreviewUrlCache = new Map<string, string>(); + +const FALLBACK_CORNER_RADIUS_RATIO = 0.2; +const FALLBACK_FILL_OPACITY = 0.08; +const FALLBACK_MIN_FONT_SIZE = 12; +const FALLBACK_FONT_SIZE_RATIO = 0.15; + +function buildFallbackPreviewUrl({ + name, + size, +}: { + name: string; + size: number; +}): string { + const svg = ` + <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}" /> + <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> + `; + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; +} + +export function getGraphicDefinition({ + definitionId, +}: { + definitionId: string; +}): GraphicDefinition { + registerDefaultGraphics(); + return graphicsRegistry.get(definitionId); +} + +export function buildDefaultGraphicInstance({ + definitionId, +}: { + definitionId: string; +}): GraphicInstance { + const definition = getGraphicDefinition({ definitionId }); + return { + definitionId, + params: buildDefaultParamValues(definition.params), + }; +} + +export function resolveGraphicParams( + definition: GraphicDefinition, + params?: ParamValues, +): ParamValues { + return { + ...buildDefaultParamValues(definition.params), + ...(params ?? {}), + }; +} + +export function resolveGraphicElementParamsAtTime({ + element, + localTime, +}: { + element: { + definitionId: string; + params: ParamValues; + animations?: ElementAnimations; + }; + localTime: number; +}): ParamValues { + const definition = getGraphicDefinition({ + definitionId: element.definitionId, + }); + return resolveGraphicParamsAtTime({ + params: resolveGraphicParams(definition, element.params), + definitions: definition.params, + animations: element.animations, + localTime, + }); +} + +export function buildGraphicPreviewUrl({ + definitionId, + params, + size = DEFAULT_GRAPHIC_SOURCE_SIZE, +}: { + definitionId: string; + params?: ParamValues; + size?: number; +}): string { + const definition = getGraphicDefinition({ definitionId }); + const resolvedParams = resolveGraphicParams(definition, params); + const cacheKey = JSON.stringify({ definitionId, resolvedParams, size }); + const cachedUrl = graphicPreviewUrlCache.get(cacheKey); + if (cachedUrl) { + return cachedUrl; + } + + if (typeof document === "undefined") { + return buildFallbackPreviewUrl({ name: definition.name, size }); + } + + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + if (!ctx) { + return buildFallbackPreviewUrl({ name: definition.name, size }); + } + + definition.render({ + ctx, + params: resolvedParams, + width: size, + height: size, + }); + + const previewUrl = canvas.toDataURL("image/png"); + graphicPreviewUrlCache.set(cacheKey, previewUrl); + return previewUrl; +} + +export { + DEFAULT_GRAPHIC_SOURCE_SIZE, + ellipseGraphicDefinition, + graphicsRegistry, + polygonGraphicDefinition, + rectangleGraphicDefinition, + registerDefaultGraphics, + starGraphicDefinition, +}; +export type { + GraphicDefinition, + GraphicInstance, + GraphicRenderContext, +} from "./types"; diff --git a/apps/web/src/rendering/NOTES.md b/apps/web/src/rendering/NOTES.md deleted file mode 100644 index 38055c14..00000000 --- a/apps/web/src/rendering/NOTES.md +++ /dev/null @@ -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.
\ No newline at end of file diff --git a/notes/primitives-vs-domains.md b/notes/primitives-vs-domains.md new file mode 100644 index 00000000..6708246c --- /dev/null +++ b/notes/primitives-vs-domains.md @@ -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).
\ No newline at end of file |
