feat(control): история-аккордеон и форма кисти затемнения

Переименовать блок сюжетной линии в «История» и сделать его сворачиваемым. Добавить выбор круглой/квадратной формы для кистей открытия и закрытия с сохранением в штрихах и сбросом при смене инструмента или сцены без затемнения.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-09-02 10:00:10 +08:00
parent 0a9168b320
commit ea718a26df
8 changed files with 288 additions and 30 deletions
@@ -1,11 +1,15 @@
import React, { useEffect, useRef } from 'react';
import type {
SceneDarknessBrushShape,
SceneDarknessRevealStroke,
SceneDarknessState,
SceneDarknessStrokeMode,
} from '../../../shared/types';
import { normalizeSceneDarknessStrokeMode } from '../../../shared/types/sceneDarkness';
import {
normalizeSceneDarknessBrushShape,
normalizeSceneDarknessStrokeMode,
} from '../../../shared/types/sceneDarkness';
export type SceneDarknessOverlayProps = {
state: SceneDarknessState | null;
@@ -15,18 +19,44 @@ export type SceneDarknessOverlayProps = {
style?: React.CSSProperties;
};
function drawDarknessStroke(
type DarknessStrokeLike = SceneDarknessRevealStroke | {
points: { x: number; y: number }[];
radiusN: number;
mode?: SceneDarknessStrokeMode;
shape?: SceneDarknessBrushShape;
};
function stampSquare(
ctx: CanvasRenderingContext2D,
stroke: SceneDarknessRevealStroke | { points: { x: number; y: number }[]; radiusN: number; mode?: SceneDarknessStrokeMode },
x: number,
y: number,
side: number,
): void {
ctx.fillRect(x - side / 2, y - side / 2, side, side);
}
function drawSquareDarknessStroke(
ctx: CanvasRenderingContext2D,
stroke: DarknessStrokeLike,
w: number,
h: number,
): void {
const pts = stroke.points;
if (pts.length === 0) return;
const side = stroke.radiusN * Math.min(w, h) * 2;
for (const p of pts) {
stampSquare(ctx, p.x * w, p.y * h, side);
}
}
function drawCircleDarknessStroke(
ctx: CanvasRenderingContext2D,
stroke: DarknessStrokeLike,
w: number,
h: number,
): void {
const pts = stroke.points;
if (pts.length === 0) return;
const mode = normalizeSceneDarknessStrokeMode(stroke.mode);
ctx.globalCompositeOperation = mode === 'cover' ? 'source-over' : 'destination-out';
ctx.fillStyle = '#000000';
ctx.strokeStyle = '#000000';
const r = stroke.radiusN * Math.min(w, h);
if (pts.length === 1) {
const p = pts[0];
@@ -51,6 +81,24 @@ function drawDarknessStroke(
ctx.stroke();
}
function drawDarknessStroke(
ctx: CanvasRenderingContext2D,
stroke: DarknessStrokeLike,
w: number,
h: number,
): void {
const mode = normalizeSceneDarknessStrokeMode(stroke.mode);
const shape = normalizeSceneDarknessBrushShape(stroke.shape);
ctx.globalCompositeOperation = mode === 'cover' ? 'source-over' : 'destination-out';
ctx.fillStyle = '#000000';
ctx.strokeStyle = '#000000';
if (shape === 'square') {
drawSquareDarknessStroke(ctx, stroke, w, h);
return;
}
drawCircleDarknessStroke(ctx, stroke, w, h);
}
export function SceneDarknessOverlay({
state,
viewport,