docs: move NOTES.md file
This commit is contained in:
parent
0b7597b31f
commit
8ab3aeba7c
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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).
|
||||||
Loading…
Reference in New Issue