feat(effects): add Darkness action effect like freeze
Adds a fullscreen darkness vignette on click and a permanent shadow spot with a black radial gradient (40% edge, 100% center). Includes control panel UI, i18n, eraser support, and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -69,7 +69,7 @@ export function ControlApp() {
|
||||
const previewHostRef = useRef<HTMLDivElement | null>(null);
|
||||
const previewVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const brushRef = useRef<{
|
||||
tool: 'fog' | 'fire' | 'rain' | 'water' | 'lightning' | 'sunbeam' | 'poisonCloud' | 'freeze' | 'eraser';
|
||||
tool: 'fog' | 'fire' | 'rain' | 'water' | 'darkness' | 'lightning' | 'sunbeam' | 'poisonCloud' | 'freeze' | 'eraser';
|
||||
startN?: { x: number; y: number };
|
||||
points?: { x: number; y: number; tMs: number }[];
|
||||
} | null>(null);
|
||||
@@ -133,6 +133,11 @@ export function ControlApp() {
|
||||
void getFreezeEffectLifeMs().then(setFreezeDraftLifeMs);
|
||||
}, []);
|
||||
|
||||
const [darknessDraftLifeMs, setDarknessDraftLifeMs] = useState(820);
|
||||
useEffect(() => {
|
||||
void getFreezeEffectLifeMs().then(setDarknessDraftLifeMs);
|
||||
}, []);
|
||||
|
||||
const [sunbeamDraftLifeMs, setSunbeamDraftLifeMs] = useState(600);
|
||||
useEffect(() => {
|
||||
void getSunbeamEffectLifeMs().then(setSunbeamDraftLifeMs);
|
||||
@@ -714,6 +719,37 @@ export function ControlApp() {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (b.tool === 'darkness' && b.points && b.points.length > 0) {
|
||||
const last = b.points[b.points.length - 1];
|
||||
if (last === undefined) return;
|
||||
const at = { x: last.x, y: last.y };
|
||||
const darknessLifeMs = await getFreezeEffectLifeMs();
|
||||
await fx.dispatch({
|
||||
kind: 'instance.add',
|
||||
instance: {
|
||||
id: `dk_${String(createdAtMs)}_${String(seed)}`,
|
||||
type: 'darkness',
|
||||
seed,
|
||||
createdAtMs,
|
||||
at,
|
||||
intensity: Math.max(0.8, Math.min(1.25, tool.intensity * 1.15)),
|
||||
lifetimeMs: darknessLifeMs,
|
||||
},
|
||||
});
|
||||
await fx.dispatch({
|
||||
kind: 'instance.add',
|
||||
instance: {
|
||||
id: `sh_${String(createdAtMs)}_${String(seed)}`,
|
||||
type: 'shadow',
|
||||
seed: seed ^ 0x0d4a0001,
|
||||
createdAtMs,
|
||||
at,
|
||||
radiusN: Math.max(0.03, tool.radiusN * 0.9),
|
||||
opacity: 1,
|
||||
lifetimeMs: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (b.tool === 'lightning' && b.startN && b.points && b.points.length > 0) {
|
||||
const last = b.points[b.points.length - 1];
|
||||
if (last === undefined) return;
|
||||
@@ -879,6 +915,19 @@ export function ControlApp() {
|
||||
lifetimeMs: null,
|
||||
};
|
||||
}
|
||||
if (b.tool === 'darkness' && b.points && b.points.length > 0) {
|
||||
const last = b.points[b.points.length - 1];
|
||||
if (last === undefined) return null;
|
||||
return {
|
||||
id: '__draft__',
|
||||
type: 'darkness' as const,
|
||||
seed,
|
||||
createdAtMs,
|
||||
at: { x: last.x, y: last.y },
|
||||
intensity: Math.max(0.8, Math.min(1.25, tool.intensity * 1.15)),
|
||||
lifetimeMs: darknessDraftLifeMs,
|
||||
};
|
||||
}
|
||||
if (b.tool === 'lightning' && b.startN && b.points && b.points.length > 0) {
|
||||
const last = b.points[b.points.length - 1];
|
||||
if (last === undefined) return null;
|
||||
@@ -940,6 +989,7 @@ export function ControlApp() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
draftFxTick,
|
||||
darknessDraftLifeMs,
|
||||
freezeDraftLifeMs,
|
||||
poisonDraftLifeMs,
|
||||
sunbeamDraftLifeMs,
|
||||
@@ -1073,6 +1123,15 @@ export function ControlApp() {
|
||||
>
|
||||
<span className={styles.iconGlyph}>❄️</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={tool.tool === 'darkness' ? 'primary' : 'ghost'}
|
||||
iconOnly
|
||||
title={t('control.darkness')}
|
||||
ariaLabel={t('control.darkness')}
|
||||
onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'darkness' } })}
|
||||
>
|
||||
<span className={styles.iconGlyph}>🌑</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={tool.tool === 'poisonCloud' ? 'primary' : 'ghost'}
|
||||
iconOnly
|
||||
|
||||
Reference in New Issue
Block a user