Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Each timeline section `effect` maps to one of the entries below. Include any of
| `finale` | `trail`, `starSpeed`, `starWarp`, `starTurn`, `particleCount`, `particleForce`, `bars`, `barHeight` | |
| `proper3d` | `speed` | |
| `fake3d` | `speed` | |
| `shadowVolumes` | `count`, `seed`, `speed`, `contrast`, `lightYawAmp`, `lightHeight`, `shadowLength`, `perspective`, `groundGrid`, `rotateOccluders`, `accent`, `beatPunch`, `cameraDrift`, `mobilePadding` | Canvas faux-3D hard-shadow scene with deterministic silhouette projections. |
| `textured_cube` | `scale`, `camDist`, `focalMul`, `rotXSpeed`, `rotYSpeed`, `rotZSpeed`, `backfaceCull`, `perspectiveCorrect`, `edge`, `edgeAlpha`, `shadeStrength`, `audioReact`, `beatKick`, `textureAnim` | Software-textured cube with optional affine/perspective mapping. |
| `portrait` | `zoom`, `drift` | |
| `sphere3d` | `speed` | |
Expand Down
48 changes: 44 additions & 4 deletions docs/effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Generated from `src/renderer/effects/manifest/index.ts`.

Total effects: **81**.
Total effects: **82**.

## Table of contents

Expand Down Expand Up @@ -63,6 +63,7 @@ Total effects: **81**.
- [Effect: roadDrive](#effect-roadDrive)
- [Effect: rotozoom](#effect-rotozoom)
- [Effect: shadebobs_bobs](#effect-shadebobs-bobs)
- [Effect: shadowVolumes](#effect-shadowVolumes)
- [Effect: sine_distorter](#effect-sine-distorter)
- [Effect: sine_scroller_logo](#effect-sine-scroller-logo)
- [Effect: space_hangar](#effect-space-hangar)
Expand Down Expand Up @@ -106,16 +107,16 @@ Total effects: **81**.

### Common parameter patterns

- `speed` (used in 44 effects)
- `seed` (used in 36 effects)
- `speed` (used in 45 effects)
- `seed` (used in 37 effects)
- `audioReact` (used in 34 effects)
- `beatKick` (used in 14 effects)
- `glow` (used in 13 effects)
- `trail` (used in 10 effects)
- `palette` (used in 9 effects)
- `count` (used in 7 effects)
- `hueShift` (used in 7 effects)
- `scanlines` (used in 7 effects)
- `count` (used in 6 effects)
- `bufH` (used in 6 effects)
- `bufW` (used in 6 effects)

Expand Down Expand Up @@ -2080,6 +2081,45 @@ Total effects: **81**.
}
```

## Effect: shadowVolumes

- **Registry key:** `shadowVolumes`
- **Implementation:** `src/renderer/effects/shadowVolumes.ts` (class `ShadowVolumesEffect`)
- **Renderer:** Canvas2D
- **Description:** Canvas faux-3D hard-shadow scene with deterministic silhouette projections.
- **Audio features:** bass, beat, beatStrength, mid, treble
- **Performance notes:** None noted.

### Parameters

| JSON path | Type | Default | Range/constraints | Behaviour notes | Automatable |
| --- | --- | --- | --- | --- | --- |
| `params.accent` | number | 0.32 | min 0, max 1 | Accent | yes |
| `params.beatPunch` | number | 0.5 | min 0, max 1 | Beat Punch | yes |
| `params.cameraDrift` | number | 0.3 | min 0, max 1 | Camera Drift | yes |
| `params.contrast` | number | 0.86 | min 0.2, max 1.5 | Contrast | yes |
| `params.count` | number | 6 | min 2, max 14 | Count | yes |
| `params.groundGrid` | number | 0.45 | min 0, max 1 | Ground Grid | yes |
| `params.lightHeight` | number | 1 | min 0.2, max 2 | Light Height | yes |
| `params.lightYawAmp` | number | 1 | min 0.1, max 2.5 | Light Yaw Amp | yes |
| `params.mobilePadding` | number | 0.05 | min 0, max 0.24 | Mobile Padding | yes |
| `params.perspective` | number | 1 | min 0.45, max 1.8 | Perspective | yes |
| `params.rotateOccluders` | boolean | 1 | unspecified | Rotate Occluders | unknown |
| `params.seed` | number | 11 | min 0, max 9999 | Seed | yes |
| `params.shadowLength` | number | 1 | min 0.4, max 3 | Shadow Length | yes |
| `params.speed` | number | 1 | min 0.15, max 3 | Speed | yes |

### Minimal layer usage

```json
{
"effect": "shadowVolumes",
"opacity": 1,
"blend": "source-over",
"params": {}
}
```

## Effect: sine_distorter

- **Registry key:** `sine_distorter`
Expand Down
10 changes: 10 additions & 0 deletions public/timeline.release.json
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,16 @@
"params": {
"audioReact": 0.2
}
},
{
"effect": "shadowVolumes",
"blend": "multiply",
"opacity": 0.16,
"params": {
"count": 5,
"contrast": 0.82,
"accent": 0.14
}
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/effects/manifest/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { ribbonsManifest } from "./ribbons.manifest";
import { roadDriveManifest } from "./roadDrive.manifest";
import { rotozoomManifest } from "./rotozoom.manifest";
import { shadebobs_bobsManifest } from "./shadebobs_bobs.manifest";
import { shadow_volumesManifest } from "./shadow_volumes.manifest";
import { sine_distorterManifest } from "./sine_distorter.manifest";
import { sine_scroller_logoManifest } from "./sine_scroller_logo.manifest";
import { space_hangarManifest } from "./space_hangar.manifest";
Expand Down Expand Up @@ -138,6 +139,7 @@ export const generatedEffectManifests = [
roadDriveManifest,
rotozoomManifest,
shadebobs_bobsManifest,
shadow_volumesManifest,
sine_distorterManifest,
sine_scroller_logoManifest,
space_hangarManifest,
Expand Down
36 changes: 36 additions & 0 deletions src/renderer/effects/manifest/shadow_volumes.manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { SHADOW_VOLUMES_DEFAULTS, ShadowVolumesEffect } from "../shadowVolumes";
import { defineEffectManifest, numberControl, toggleControl } from "./shared";

export const shadow_volumesManifest = defineEffectManifest({
key: "shadowVolumes",
className: "ShadowVolumesEffect",
sourcePath: "src/renderer/effects/shadowVolumes.ts",
createEffect: () => new ShadowVolumesEffect(),
debug: {
title: "Shadow Volumes Controls",
controls: [
numberControl("count", "Count", SHADOW_VOLUMES_DEFAULTS.count, { min: 2, max: 14, step: 1 }),
numberControl("seed", "Seed", SHADOW_VOLUMES_DEFAULTS.seed, { min: 0, max: 9999, step: 1 }),
numberControl("speed", "Speed", SHADOW_VOLUMES_DEFAULTS.speed, { min: 0.15, max: 3, step: 0.01 }),
numberControl("contrast", "Contrast", SHADOW_VOLUMES_DEFAULTS.contrast, { min: 0.2, max: 1.5, step: 0.01 }),
numberControl("lightYawAmp", "Light Yaw Amp", SHADOW_VOLUMES_DEFAULTS.lightYawAmp, { min: 0.1, max: 2.5, step: 0.01 }),
numberControl("lightHeight", "Light Height", SHADOW_VOLUMES_DEFAULTS.lightHeight, { min: 0.2, max: 2, step: 0.01 }),
numberControl("shadowLength", "Shadow Length", SHADOW_VOLUMES_DEFAULTS.shadowLength, { min: 0.4, max: 3, step: 0.01 }),
numberControl("perspective", "Perspective", SHADOW_VOLUMES_DEFAULTS.perspective, { min: 0.45, max: 1.8, step: 0.01 }),
numberControl("groundGrid", "Ground Grid", SHADOW_VOLUMES_DEFAULTS.groundGrid, { min: 0, max: 1, step: 0.01 }),
toggleControl("rotateOccluders", "Rotate Occluders", SHADOW_VOLUMES_DEFAULTS.rotateOccluders === 1),
numberControl("accent", "Accent", SHADOW_VOLUMES_DEFAULTS.accent, { min: 0, max: 1, step: 0.01 }),
numberControl("beatPunch", "Beat Punch", SHADOW_VOLUMES_DEFAULTS.beatPunch, { min: 0, max: 1, step: 0.01 }),
numberControl("cameraDrift", "Camera Drift", SHADOW_VOLUMES_DEFAULTS.cameraDrift, { min: 0, max: 1, step: 0.01 }),
numberControl("mobilePadding", "Mobile Padding", SHADOW_VOLUMES_DEFAULTS.mobilePadding, { min: 0, max: 0.24, step: 0.01 })
]
},
docs: {
parameters:
"`count`, `seed`, `speed`, `contrast`, `lightYawAmp`, `lightHeight`, `shadowLength`, `perspective`, `groundGrid`, `rotateOccluders`, `accent`, `beatPunch`, `cameraDrift`, `mobilePadding`",
catalogNote: "Canvas faux-3D hard-shadow scene with deterministic silhouette projections.",
description: "Canvas faux-3D hard-shadow scene with deterministic silhouette projections."
}
});

export default shadow_volumesManifest;
82 changes: 82 additions & 0 deletions src/renderer/effects/shadowVolumes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";

import {
buildOccluderShadowPolygon,
buildShadowVolumeOccluders,
projectShadowVolumesPoint,
resolveShadowVolumesParams,
SHADOW_VOLUMES_DEFAULTS
} from "./shadowVolumes";

describe("shadowVolumes helpers", () => {
it("resolves defaults and clamps params", () => {
expect(resolveShadowVolumesParams({})).toEqual({
...SHADOW_VOLUMES_DEFAULTS,
rotateOccluders: true
});

expect(
resolveShadowVolumesParams({
count: 99,
speed: -1,
contrast: 9,
lightYawAmp: 0,
lightHeight: -5,
shadowLength: 99,
perspective: 0,
groundGrid: 9,
rotateOccluders: 0,
accent: -1,
beatPunch: 9,
cameraDrift: -4,
mobilePadding: 2
})
).toMatchObject({
count: 14,
speed: 0.15,
contrast: 1.5,
lightYawAmp: 0.1,
lightHeight: 0.2,
shadowLength: 3,
perspective: 0.45,
groundGrid: 1,
rotateOccluders: false,
accent: 0,
beatPunch: 1,
cameraDrift: 0,
mobilePadding: 0.24
});
});

it("builds deterministic occluders for the same seed", () => {
const first = buildShadowVolumeOccluders(6, 12);
const second = buildShadowVolumeOccluders(6, 12);
const different = buildShadowVolumeOccluders(6, 13);

expect(first).toEqual(second);
expect(first).not.toEqual(different);
expect(first).toHaveLength(6);
});

it("projects points consistently with depth", () => {
const camera = { x: 200, y: 0, horizonY: 60, scale: 140, perspective: 1 };
const near = projectShadowVolumesPoint({ x: 1, y: 1, z: 0 }, camera);
const far = projectShadowVolumesPoint({ x: 1, y: 8, z: 0 }, camera);

expect(Number.isFinite(near.x)).toBe(true);
expect(Number.isFinite(far.y)).toBe(true);
expect(Math.abs(near.x - camera.x)).toBeGreaterThan(Math.abs(far.x - camera.x));
});

it("builds finite shadow polygons", () => {
const occluder = buildShadowVolumeOccluders(1, 8)[0];
const polygon = buildOccluderShadowPolygon(occluder, { x: -2, y: -1, z: 5 }, 2.4);

expect(polygon).toHaveLength(8);
polygon.forEach((point) => {
expect(Number.isFinite(point.x)).toBe(true);
expect(Number.isFinite(point.y)).toBe(true);
expect(point.z).toBe(0);
});
});
});
Loading