Initial commit

This commit is contained in:
2026-04-08 10:01:19 -07:00
commit 6657125a1e
68 changed files with 15886 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@goodgrief/cue-engine",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@goodgrief/shared-types": "file:../shared-types"
}
}
+62
View File
@@ -0,0 +1,62 @@
import type { Cue } from "@goodgrief/shared-types";
export interface CueRuntimeState {
cueStack: Cue[];
currentCueId: string | null;
armedCueId: string | null;
previewCueId: string | null;
blackout: boolean;
safeSceneActive: boolean;
}
export const createCueRuntimeState = (cueStack: Cue[]): CueRuntimeState => ({
cueStack: [...cueStack].sort((left, right) => left.orderIndex - right.orderIndex),
currentCueId: null,
armedCueId: cueStack[0]?.id ?? null,
previewCueId: cueStack[0]?.id ?? null,
blackout: false,
safeSceneActive: false
});
export const armCue = (state: CueRuntimeState, cueId: string): CueRuntimeState => ({
...state,
armedCueId: cueId,
previewCueId: cueId
});
export const takeCue = (state: CueRuntimeState): CueRuntimeState => {
if (!state.armedCueId) {
return state;
}
const currentIndex = state.cueStack.findIndex((cue) => cue.id === state.armedCueId);
const nextCue = state.cueStack[currentIndex + 1] ?? null;
return {
...state,
currentCueId: state.armedCueId,
armedCueId: nextCue?.id ?? null,
previewCueId: nextCue?.id ?? null,
blackout: false,
safeSceneActive: false
};
};
export const triggerBlackout = (state: CueRuntimeState): CueRuntimeState => ({
...state,
blackout: true
});
export const triggerSafeScene = (state: CueRuntimeState, cueId: string): CueRuntimeState => ({
...state,
currentCueId: cueId,
previewCueId: cueId,
safeSceneActive: true,
blackout: false
});
export const skipToCue = (state: CueRuntimeState, cueId: string): CueRuntimeState => ({
...state,
armedCueId: cueId,
previewCueId: cueId
});
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"include": [
"src"
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@goodgrief/effects",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@goodgrief/shared-types": "file:../shared-types"
}
}
+98
View File
@@ -0,0 +1,98 @@
import type { EffectPreset } from "@goodgrief/shared-types";
import { defaultEffectPresets } from "@goodgrief/shared-types";
export interface EffectCategorySpec {
id:
| "compositing"
| "temporal"
| "spatial"
| "color"
| "depth"
| "particles"
| "reveal"
| "audio"
| "performer"
| "projection";
title: string;
artisticPurpose: string;
recommendedImplementation: "css" | "canvas" | "webgl" | "custom_shader";
performanceNote: string;
}
export const effectCategories: EffectCategorySpec[] = [
{
id: "compositing",
title: "Compositing",
artisticPurpose: "Layer images as memories held in relation rather than simply stacked.",
recommendedImplementation: "webgl",
performanceNote: "Keep pass count low and prefer pre-sized textures."
},
{
id: "temporal",
title: "Temporal",
artisticPurpose: "Create residue, afterimage, freeze, and unstable recall.",
recommendedImplementation: "custom_shader",
performanceNote: "Requires explicit history buffer management."
},
{
id: "spatial",
title: "Spatial",
artisticPurpose: "Turn still photos into playable, live-composed space.",
recommendedImplementation: "webgl",
performanceNote: "Cheap until object counts and shadow complexity grow."
},
{
id: "color",
title: "Color and Tone",
artisticPurpose: "Shift between warmth, institutional coldness, camp, and grief hush.",
recommendedImplementation: "custom_shader",
performanceNote: "Bound operator ranges to prevent unreadable projections."
},
{
id: "depth",
title: "Depth and Parallax",
artisticPurpose: "Give still imagery weight, procession, and breath.",
recommendedImplementation: "webgl",
performanceNote: "Batch materials and cap simultaneous textures."
},
{
id: "particles",
title: "Particles",
artisticPurpose: "Dust, ash, petals, paper, stars, and bodily debris.",
recommendedImplementation: "custom_shader",
performanceNote: "Use one reusable GPU-instanced system."
},
{
id: "reveal",
title: "Reveal and Conceal",
artisticPurpose: "Let imagery emerge through masks, curtains, water, and tears.",
recommendedImplementation: "custom_shader",
performanceNote: "Ordering and alpha complexity need strict discipline."
},
{
id: "audio",
title: "Audio Reactive",
artisticPurpose: "Subtle breathing and swell tied to score rather than spectacle.",
recommendedImplementation: "webgl",
performanceNote: "Should always be operator-disableable."
},
{
id: "performer",
title: "Performer Reactive",
artisticPurpose: "Rare moments where the body reveals or occludes memory.",
recommendedImplementation: "custom_shader",
performanceNote: "Calibration-heavy, high risk, not for core playback."
},
{
id: "projection",
title: "Projection Safety",
artisticPurpose: "Keep output readable in dark rooms and across mismatched surfaces.",
recommendedImplementation: "webgl",
performanceNote: "Apply as the final output calibration layer."
}
];
export const effectPresetLibrary: EffectPreset[] = defaultEffectPresets;
export const findEffectPreset = (id: string) =>
effectPresetLibrary.find((preset) => preset.id === id) ?? null;
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"include": [
"src"
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "@goodgrief/render-engine",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@goodgrief/shared-types": "file:../shared-types",
"three": "^0.176.0"
}
}
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"include": [
"src"
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@goodgrief/shared-types",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"zod": "^3.24.4"
}
}
+374
View File
@@ -0,0 +1,374 @@
export type SubmissionSource = "live" | "pre_show" | "invite" | "admin_upload" | "library_import";
export type SubmissionStatus =
| "uploaded"
| "processing"
| "pending_moderation"
| "approved_partial"
| "approved_all"
| "rejected"
| "archived";
export type ProcessingStatus = "queued" | "ready" | "failed";
export type ModerationStatus = "pending" | "approved" | "hold" | "rejected" | "archived";
export type ModerationDecisionType = "approved" | "hold" | "rejected" | "archive_only";
export type CollectionKind = "bank" | "playlist" | "moment" | "favorites" | "archive_set";
export type SceneRenderMode = "2d" | "3d" | "hybrid" | "shader_overlay";
export type CueTriggerMode = "manual" | "follow" | "hold" | "armed";
export type OperatorSessionMode = "rehearsal" | "tech" | "show" | "archive_review";
export type OutputSurfaceRole = "program" | "preview" | "aux";
export type SceneTier = "mvp" | "v1" | "stretch";
export type SceneFamily = "hero" | "chorus" | "floor_paint" | "arrival" | "rupture" | "safe";
export type TextTreatmentMode = "off" | "edge_whispers" | "relay_ticker" | "anchor_caption";
export type SceneCategory =
| "memory_elegy"
| "humor_rupture"
| "choir_swell"
| "abstract_grief"
| "photo_collage"
| "immersive_3d"
| "transition"
| "audience_reactive";
export interface ContributorConsent {
id: string;
submissionId: string;
hasRights: boolean;
allowProjection: boolean;
acknowledgePublicPerformance: boolean;
agreedAt: string;
allowArchive?: boolean;
contactEmail?: string;
guardianConfirmed?: boolean;
}
export interface Submission {
id: string;
source: SubmissionSource;
submittedAt: string;
status: SubmissionStatus;
consentId: string;
displayName?: string;
caption?: string;
promptAnswer?: string;
sessionToken?: string;
notes?: string;
}
export interface QualityFlags {
tooSmall?: boolean;
blurry?: boolean;
lowContrast?: boolean;
unusualAspectRatio?: boolean;
}
export interface PhotoAsset {
id: string;
submissionId: string;
originalKey: string;
thumbKey?: string;
previewKey?: string;
renderKey?: string;
mimeType: string;
width?: number;
height?: number;
orientation?: "portrait" | "landscape" | "square";
sha256?: string;
pHash?: string;
dominantColor?: string;
processingStatus: ProcessingStatus;
moderationStatus: ModerationStatus;
createdAt: string;
qualityFlags?: QualityFlags;
approvedAt?: string;
rejectionReason?: string;
}
export interface Tag {
id: string;
label: string;
category: "tone" | "era" | "subject" | "palette" | "scene_fit" | "quality" | "show_moment";
color?: string;
description?: string;
}
export interface Collection {
id: string;
name: string;
kind: CollectionKind;
createdAt: string;
description?: string;
coverAssetId?: string;
locked?: boolean;
assetIds: string[];
tagIds: string[];
}
export interface PhotoTreatmentParams {
contrast: number;
saturation: number;
blackPoint: number;
whitePoint: number;
paletteMix: number;
clarity: number;
edgeLight: number;
}
export interface ScenicTreatmentParams {
washIntensity: number;
spill: number;
floorMix: number;
paletteBias: number;
vignette: number;
fillHue: number;
fillSaturation: number;
fillLightness: number;
}
export interface CompositionParams {
motion: number;
density: number;
depth: number;
focus: number;
crop: number;
emphasis: number;
bands?: number;
columns?: number;
shutters?: number;
tiles?: number;
lanes?: number;
edge?: "left" | "right";
}
export interface TextTreatmentParams {
mode: TextTreatmentMode;
opacity: number;
density: number;
scale: number;
}
export interface SceneParamGroups {
photoTreatment: PhotoTreatmentParams;
scenicTreatment: ScenicTreatmentParams;
composition: CompositionParams;
textTreatment: TextTreatmentParams;
}
export interface SceneParamPatch {
photoTreatment?: Partial<PhotoTreatmentParams>;
scenicTreatment?: Partial<ScenicTreatmentParams>;
composition?: Partial<CompositionParams>;
textTreatment?: Partial<TextTreatmentParams>;
}
export interface PhotoTreatmentPreset {
id: string;
name: string;
params: Partial<PhotoTreatmentParams>;
}
export interface ScenicTreatmentPreset {
id: string;
name: string;
params: Partial<ScenicTreatmentParams>;
}
export interface SceneDefinition {
id: string;
sceneKey: string;
sceneFamily: SceneFamily;
name: string;
category: SceneCategory;
tier: SceneTier;
visualDescription: string;
emotionalUseCase: string;
renderMode: SceneRenderMode;
complexity: "low" | "medium" | "high";
performanceRisk: "low" | "medium" | "high";
inputRules: {
minAssets: number;
maxAssets?: number;
recommendedTags: string[];
};
defaultParams: SceneParamGroups;
defaultPresetId: string;
supportedPresetIds: string[];
operatorControls: string[];
metadataHints: string[];
}
export interface CueTransition {
style: "cut" | "dissolve" | "veil_wipe" | "luma_hold" | "rupture_offset";
durationMs: number;
}
export interface Cue {
id: string;
showConfigId: string;
orderIndex: number;
sceneDefinitionId: string;
triggerMode: CueTriggerMode;
transitionIn: CueTransition;
transitionOut: CueTransition;
collectionId?: string;
assetIds?: string[];
durationMs?: number;
effectPresetId?: string;
parameterOverrides?: SceneParamPatch;
notes?: string;
nextCueId?: string;
}
export interface CueUpsertPayload {
id?: string;
showConfigId?: string;
orderIndex?: number;
sceneDefinitionId: string;
triggerMode: CueTriggerMode;
transitionIn: CueTransition;
transitionOut: CueTransition;
collectionId?: string;
assetIds?: string[];
durationMs?: number;
effectPresetId?: string;
parameterOverrides?: SceneParamPatch;
notes?: string;
nextCueId?: string;
}
export interface CueMovePayload {
direction: "up" | "down";
}
export interface CueGeneratePayload {
sceneDefinitionId?: string;
preferredAssetIds?: string[];
includeRupture?: boolean;
}
export interface EffectPreset {
id: string;
modeKey: string;
compatibleSceneFamilies: SceneFamily[];
name: string;
category:
| "compositing"
| "temporal"
| "spatial"
| "color"
| "depth"
| "particles"
| "reveal"
| "audio"
| "performer"
| "projection";
artisticPurpose: string;
operatorControls: string[];
implementationLevel: "css" | "canvas" | "webgl" | "custom_shader";
performanceNotes: string;
paramDefaults: SceneParamPatch;
safeRanges: Record<string, { min: number; max: number }>;
}
export interface OperatorSession {
id: string;
startedAt: string;
mode: OperatorSessionMode;
operatorName: string;
showConfigId: string;
endedAt?: string;
venueName?: string;
incidentNotes?: string;
}
export interface ModerationDecision {
id: string;
assetId: string;
operatorSessionId: string;
decision: ModerationDecisionType;
decidedAt: string;
reasonCode?: string;
note?: string;
}
export interface OutputSurface {
id: string;
name: string;
role: OutputSurfaceRole;
width: number;
height: number;
aspectRatio: string;
screenIndex?: number;
maskShape?: "full_frame" | "letterbox" | "pillarbox" | "custom";
safeMargin?: number;
colorProfile?: string;
fullscreenBounds?: {
x: number;
y: number;
width: number;
height: number;
};
}
export interface ShowConfig {
id: string;
showName: string;
venueName: string;
defaultOutputSurfaceId: string;
safeSceneCueId: string;
retentionDays: number;
ingestPolicy: "fully_live" | "pre_show_plus_live" | "pre_show_only";
theme?: string;
projectionNotes?: string;
operatorShortcuts?: Record<string, string>;
}
export interface SessionEvent {
id: string;
sessionId: string;
timestamp: string;
type:
| "cue_fired"
| "cue_skipped"
| "blackout"
| "safe_scene"
| "submission_received"
| "asset_approved"
| "asset_rejected";
payload: Record<string, string | number | boolean | null>;
}
export interface SubmissionPayload {
displayName?: string;
caption?: string;
promptAnswer?: string;
allowArchive?: boolean;
hasRights: boolean;
allowProjection: boolean;
acknowledgePublicPerformance: boolean;
source?: SubmissionSource;
}
export interface ModerationActionPayload {
decision: ModerationDecisionType;
reasonCode?: string;
note?: string;
tagIds?: string[];
collectionIds?: string[];
}
export interface RepositoryState {
submissions: Submission[];
consents: ContributorConsent[];
photoAssets: PhotoAsset[];
tags: Tag[];
collections: Collection[];
scenes: SceneDefinition[];
cues: Cue[];
effectPresets: EffectPreset[];
operatorSessions: OperatorSession[];
moderationDecisions: ModerationDecision[];
outputSurfaces: OutputSurface[];
showConfig: ShowConfig;
sessionEvents: SessionEvent[];
}
+21
View File
@@ -0,0 +1,21 @@
import type { ModerationDecisionType } from "./entities";
export type ApiEvent =
| {
type: "submission.received";
submissionId: string;
assetId: string;
}
| {
type: "asset.moderated";
assetId: string;
decision: ModerationDecisionType;
}
| {
type: "cue.fired";
cueId: string;
}
| {
type: "cue.safe";
cueId: string;
};
+5
View File
@@ -0,0 +1,5 @@
export * from "./entities";
export * from "./events";
export * from "./mock";
export * from "./scene-params";
export * from "./scenes";
+113
View File
@@ -0,0 +1,113 @@
import type {
Collection,
OperatorSession,
OutputSurface,
RepositoryState,
ShowConfig,
Tag
} from "./entities";
import { defaultCueStack, defaultEffectPresets, defaultSceneDefinitions } from "./scenes";
export const defaultTags: Tag[] = [
{ id: "tag-quiet", label: "quiet", category: "tone", color: "#9ea29f" },
{ id: "tag-family", label: "family", category: "subject", color: "#d09d74" },
{ id: "tag-portrait", label: "portrait", category: "subject", color: "#6f8579" },
{ id: "tag-live", label: "live", category: "show_moment", color: "#e27f66" },
{ id: "tag-choir", label: "choir", category: "show_moment", color: "#7d7098" },
{ id: "tag-archive", label: "archive", category: "scene_fit", color: "#c8c0b3" }
];
export const defaultCollections: Collection[] = [
{
id: "collection-curated-library",
name: "Curated Library",
kind: "bank",
createdAt: new Date().toISOString(),
description: "Imported operator-managed seed and rehearsal media.",
locked: true,
assetIds: [],
tagIds: ["tag-archive", "tag-portrait"]
},
{
id: "collection-favorites",
name: "Favorites",
kind: "favorites",
createdAt: new Date().toISOString(),
description: "Operator-trusted images for flexible live use.",
locked: false,
assetIds: [],
tagIds: ["tag-portrait", "tag-quiet"]
},
{
id: "collection-choir-swell",
name: "Choir Swell",
kind: "moment",
createdAt: new Date().toISOString(),
description: "Assets suitable for collective or musical lift.",
locked: false,
assetIds: [],
tagIds: ["tag-choir", "tag-family"]
}
];
export const defaultOutputSurfaces: OutputSurface[] = [
{
id: "surface-program",
name: "Program",
role: "program",
width: 1920,
height: 1080,
aspectRatio: "16:9",
safeMargin: 0.06
},
{
id: "surface-preview",
name: "Preview",
role: "preview",
width: 1280,
height: 720,
aspectRatio: "16:9"
}
];
export const defaultShowConfig: ShowConfig = {
id: "show-good-grief",
showName: "Good Grief",
venueName: "Studio Black Box",
defaultOutputSurfaceId: "surface-program",
safeSceneCueId: "cue-safe-hold",
retentionDays: 21,
ingestPolicy: "fully_live",
theme: "Tender collage for live projection.",
projectionNotes: "Maintain center-safe composition and low white clip.",
operatorShortcuts: {
Space: "take cue",
KeyB: "blackout",
KeyS: "safe scene",
ArrowDown: "next cue"
}
};
export const defaultOperatorSession: OperatorSession = {
id: "session-default",
startedAt: new Date().toISOString(),
mode: "rehearsal",
operatorName: "Operator",
showConfigId: "show-good-grief"
};
export const createEmptyRepositoryState = (): RepositoryState => ({
submissions: [],
consents: [],
photoAssets: [],
tags: defaultTags,
collections: defaultCollections,
scenes: defaultSceneDefinitions,
cues: defaultCueStack,
effectPresets: defaultEffectPresets,
operatorSessions: [defaultOperatorSession],
moderationDecisions: [],
outputSurfaces: defaultOutputSurfaces,
showConfig: defaultShowConfig,
sessionEvents: []
});
+108
View File
@@ -0,0 +1,108 @@
import type {
CompositionParams,
PhotoTreatmentParams,
ScenicTreatmentParams,
SceneParamGroups,
SceneParamPatch,
TextTreatmentParams
} from "./entities";
const defaultTextTreatment = (): TextTreatmentParams => ({
mode: "off",
opacity: 0.2,
density: 0.35,
scale: 0.8
});
export type SceneParamScalar = number | string | boolean;
export const createSceneParams = (input: {
photoTreatment: PhotoTreatmentParams;
scenicTreatment: ScenicTreatmentParams;
composition: CompositionParams;
textTreatment?: TextTreatmentParams;
}): SceneParamGroups => ({
photoTreatment: { ...input.photoTreatment },
scenicTreatment: { ...input.scenicTreatment },
composition: { ...input.composition },
textTreatment: {
...defaultTextTreatment(),
...input.textTreatment
}
});
export const mergeSceneParams = (base: SceneParamGroups, ...patches: Array<SceneParamPatch | undefined>): SceneParamGroups => {
const merged = createSceneParams(base);
for (const patch of patches) {
if (!patch) {
continue;
}
if (patch.photoTreatment) {
merged.photoTreatment = {
...merged.photoTreatment,
...patch.photoTreatment
};
}
if (patch.scenicTreatment) {
merged.scenicTreatment = {
...merged.scenicTreatment,
...patch.scenicTreatment
};
}
if (patch.composition) {
merged.composition = {
...merged.composition,
...patch.composition
};
}
if (patch.textTreatment) {
merged.textTreatment = {
...merged.textTreatment,
...patch.textTreatment
};
}
}
return merged;
};
export const getSceneParamValue = (params: SceneParamGroups | SceneParamPatch, path: string): SceneParamScalar | undefined => {
const [group, key] = path.split(".");
if (!group || !key) {
return undefined;
}
const container = params[group as keyof SceneParamGroups] as Record<string, SceneParamScalar> | undefined;
return container?.[key];
};
export const setSceneParamValue = (params: SceneParamGroups, path: string, value: SceneParamScalar): SceneParamGroups => {
const [group, key] = path.split(".");
if (!group || !key) {
return params;
}
if (!(group in params)) {
return params;
}
return {
...params,
[group]: {
...((params[group as keyof SceneParamGroups] as unknown as Record<string, SceneParamScalar>) ?? {}),
[key]: value
}
} as SceneParamGroups;
};
export const flattenSceneParams = (params: SceneParamGroups | SceneParamPatch) =>
Object.fromEntries(
Object.entries(params).flatMap(([group, values]) =>
Object.entries(values ?? {}).map(([key, value]) => [`${group}.${key}`, value] as const)
)
) as Record<string, SceneParamScalar>;
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"include": [
"src"
]
}