Files
hyperframes/packages/studio/src/components/editor/propertyPanelFlatEffectSpecs.ts
T

306 lines
8.7 KiB
TypeScript

import {
getHfColorGradingCapabilities,
normalizeHfColorGrading,
type HfColorGradingActiveEffectKey,
type HfColorGradingEffectKey,
type HfColorGradingPresetId,
} from "@hyperframes/core/color-grading";
type SliderControl = {
kind: "slider";
key: HfColorGradingEffectKey;
label: string;
min?: number;
max?: number;
step?: number;
scale?: number;
unit?: string;
format?: (value: number) => string;
};
export type EffectControl =
| SliderControl
| { kind: "toggle"; key: HfColorGradingEffectKey; label: string }
| {
kind: "select";
key: HfColorGradingEffectKey;
label: string;
options: Array<{ value: string; label: string }>;
};
export type EffectSpec = {
key: HfColorGradingActiveEffectKey;
label: string;
showMaster?: false;
masterLabel?: string;
masterFormat?: (value: number) => string;
max?: number;
settings?: readonly EffectControl[];
palette?: "mono" | "art";
};
type EffectGroup = {
label: string;
effects: readonly EffectSpec[];
presets?: readonly HfColorGradingPresetId[];
};
const EFFECT_CONTROL_LIMITS = new Map(
getHfColorGradingCapabilities().effects.flatMap((effect) =>
effect.controls.map((control) => [control.key, control] as const),
),
);
function controlRange(key: HfColorGradingEffectKey, scale: number) {
const control = EFFECT_CONTROL_LIMITS.get(key);
return control ? { min: control.min * scale, max: control.max * scale, scale } : { scale };
}
const percent = (key: HfColorGradingEffectKey, label: string): SliderControl => ({
kind: "slider",
key,
label,
...controlRange(key, 100),
});
const degrees = (key: HfColorGradingEffectKey, label: string, max: number): SliderControl => ({
kind: "slider",
key,
label,
...controlRange(key, max),
unit: "deg",
});
function enumOptions(key: HfColorGradingEffectKey, labels: readonly string[]) {
const control = EFFECT_CONTROL_LIMITS.get(key);
const first = control?.min ?? 0;
const count = (control?.max ?? labels.length - 1) - first + 1;
if (labels.length !== count) throw new Error(`${key} labels do not match Core capabilities`);
return labels.map((label, index) => ({ value: String(first + index), label }));
}
const ASCII_STYLES = enumOptions("asciiStyle", [
"Standard",
"Dense",
"Minimal",
"Blocks",
"Braille",
"Technical",
"Matrix",
"Hatching",
]);
const SCREEN_SHAPES = enumOptions("monoScreenShape", [
"Circle",
"Square",
"Diamond",
"Triangle",
"Line",
]);
export const EFFECT_GROUPS: readonly EffectGroup[] = [
{
label: "Essentials",
effects: [
{
key: "blur",
label: "Blur",
masterLabel: "Radius",
masterFormat: (value) => `${(0.75 + Math.pow(value, 1.35) * 32).toFixed(1)}px`,
},
{
key: "pixelate",
label: "Pixelate",
masterLabel: "Cell Size",
masterFormat: (value) => `${Math.round(1 + value * 47)}px`,
},
{
key: "bloom",
label: "Bloom",
masterLabel: "Intensity",
max: controlRange("bloom", 100).max,
settings: [
{
kind: "slider",
key: "bloomRadius",
label: "Radius",
...controlRange("bloomRadius", 1),
unit: "px",
},
],
},
],
},
{
label: "Retro & Glitch",
presets: ["creator-camcorder", "vhs-playback", "home-movie-8mm"],
effects: [
{ key: "chromaBleed", label: "Chroma Softening", masterLabel: "Smear" },
{
key: "tapeDamage",
label: "Tape Damage",
showMaster: false,
settings: [
percent("tapeTracking", "Tracking"),
percent("tapeNoise", "Noise"),
percent("tapeSpeed", "Speed"),
],
},
{ key: "filmArtifacts", label: "Film Artifacts", masterLabel: "Density" },
{
key: "scanlines",
label: "Scanlines",
masterLabel: "Opacity",
settings: [
{
...percent("scanlineCount", "Line Count"),
format: (value) => `${Math.round(50 + value * 450)}`,
},
percent("scanlineSoftness", "Softness"),
],
},
{ key: "crtCurvature", label: "CRT Curvature", masterLabel: "Curvature" },
{
key: "chromaticAberration",
label: "Channel Separation",
masterLabel: "Separation",
settings: [degrees("chromaticAngle", "Angle", 360)],
},
{
key: "digitalGlitch",
label: "Digital Glitch",
showMaster: false,
settings: [
percent("digitalGlitchColorSplit", "Color Split"),
percent("digitalGlitchLineTear", "Line Tear"),
percent("digitalGlitchPixelate", "Pixelation"),
percent("digitalGlitchBlockAmount", "Block Amount"),
percent("digitalGlitchBlockDisplacement", "Displacement"),
percent("digitalGlitchBlockOpacity", "Block Opacity"),
percent("digitalGlitchSpeed", "Speed"),
],
},
],
},
{
label: "Print",
presets: ["editorial-halftone", "two-ink-print"],
effects: [
{
key: "halftone",
label: "Halftone",
showMaster: false,
settings: [percent("halftoneSize", "Dot Size")],
},
{
key: "twoInkPrint",
label: "Two-Ink Print",
showMaster: false,
settings: [percent("twoInkPrintSize", "Dot Size")],
},
{
key: "dither",
label: "Ordered Dither",
showMaster: false,
palette: "mono",
settings: [
{
...percent("ditherSize", "Point Size"),
format: (value) => `${(1 + value * 4).toFixed(1)}px`,
},
],
},
{
key: "monoScreen",
label: "Mono Screen",
showMaster: false,
palette: "mono",
settings: [
{
...percent("monoScreenSize", "Cell Size"),
format: (value) => `${Math.round(4 + value * 14)}px`,
},
degrees("monoScreenAngle", "Angle", 90),
percent("monoScreenSpread", "Spread"),
{ kind: "select", key: "monoScreenShape", label: "Shape", options: SCREEN_SHAPES },
{ kind: "toggle", key: "monoScreenInvert", label: "Invert" },
],
},
],
},
{
label: "Art",
effects: [
{
key: "ascii",
label: "ASCII",
showMaster: false,
palette: "mono",
settings: [
{
...percent("asciiSize", "Character Size"),
format: (value) => `${Math.round(4 + value * 76)}px`,
},
{ kind: "select", key: "asciiStyle", label: "Style", options: ASCII_STYLES },
{ kind: "toggle", key: "asciiInvert", label: "Invert" },
{ kind: "toggle", key: "asciiColor", label: "Use Source Color" },
percent("asciiRotation", "Edge Rotation"),
],
},
{
key: "engraving",
label: "Engraving",
showMaster: false,
palette: "art",
settings: [
percent("engravingSpacing", "Spacing"),
percent("engravingMinThickness", "Min Thickness"),
percent("engravingMaxThickness", "Max Thickness"),
degrees("engravingAngle", "Angle", 180),
percent("engravingContrast", "Contrast"),
percent("engravingSharpness", "Sharpness"),
percent("engravingWave", "Wave"),
percent("engravingWaveFrequency", "Wave Frequency"),
],
},
{
key: "crosshatch",
label: "Crosshatch",
showMaster: false,
palette: "art",
settings: [
percent("crosshatchSpacing", "Spacing"),
percent("crosshatchThickness", "Thickness"),
degrees("crosshatchAngle", "Angle", 180),
percent("crosshatchContrast", "Contrast"),
percent("crosshatchEdges", "Edge Detail"),
percent("crosshatchLineWeight", "Line Variation"),
percent("crosshatchWave", "Wave"),
percent("crosshatchWaveFrequency", "Wave Frequency"),
],
},
{
key: "kuwahara",
label: "Kuwahara Paint",
showMaster: false,
settings: [
{
...percent("kuwaharaRadius", "Radius"),
format: (value) => `${Math.round(2 + value * 14)}px`,
},
percent("kuwaharaSharpness", "Sharpness"),
{
...percent("kuwaharaSaturation", "Saturation"),
format: (value) => `${Math.round(value * 200)}%`,
},
],
},
],
},
];
export const EFFECT_SPECS = EFFECT_GROUPS.flatMap((group) => group.effects);
const DEFAULT_GRADING = normalizeHfColorGrading("neutral");
if (!DEFAULT_GRADING) throw new Error("Missing neutral color grading preset");
export const DEFAULT_EFFECTS = DEFAULT_GRADING.effects;