ea718a26df
Переименовать блок сюжетной линии в «История» и сделать его сворачиваемым. Добавить выбор круглой/квадратной формы для кистей открытия и закрытия с сохранением в штрихах и сбросом при смене инструмента или сцены без затемнения. Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
3.8 KiB
TypeScript
152 lines
3.8 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
|
|
import type {
|
|
SceneDarknessBrushShape,
|
|
SceneDarknessRevealStroke,
|
|
SceneDarknessState,
|
|
SceneDarknessStrokeMode,
|
|
} from '../../../shared/types';
|
|
import {
|
|
normalizeSceneDarknessBrushShape,
|
|
normalizeSceneDarknessStrokeMode,
|
|
} from '../../../shared/types/sceneDarkness';
|
|
|
|
export type SceneDarknessOverlayProps = {
|
|
state: SceneDarknessState | null;
|
|
viewport?: { x: number; y: number; w: number; h: number };
|
|
/** 1.0 — полностью чёрный (Presentation); 0.5 — полупрозрачный (Control). */
|
|
overlayAlpha: number;
|
|
style?: React.CSSProperties;
|
|
};
|
|
|
|
type DarknessStrokeLike = SceneDarknessRevealStroke | {
|
|
points: { x: number; y: number }[];
|
|
radiusN: number;
|
|
mode?: SceneDarknessStrokeMode;
|
|
shape?: SceneDarknessBrushShape;
|
|
};
|
|
|
|
function stampSquare(
|
|
ctx: CanvasRenderingContext2D,
|
|
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 r = stroke.radiusN * Math.min(w, h);
|
|
if (pts.length === 1) {
|
|
const p = pts[0];
|
|
if (!p) return;
|
|
ctx.beginPath();
|
|
ctx.arc(p.x * w, p.y * h, r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
return;
|
|
}
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
ctx.lineWidth = r * 2;
|
|
ctx.beginPath();
|
|
const first = pts[0];
|
|
if (!first) return;
|
|
ctx.moveTo(first.x * w, first.y * h);
|
|
for (let i = 1; i < pts.length; i++) {
|
|
const p = pts[i];
|
|
if (!p) continue;
|
|
ctx.lineTo(p.x * w, p.y * h);
|
|
}
|
|
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,
|
|
overlayAlpha,
|
|
style,
|
|
}: SceneDarknessOverlayProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas || !state?.enabled || !viewport) return;
|
|
const w = Math.max(1, Math.round(viewport.w));
|
|
const h = Math.max(1, Math.round(viewport.h));
|
|
if (canvas.width !== w) canvas.width = w;
|
|
if (canvas.height !== h) canvas.height = h;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.fillStyle = '#000000';
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
for (const stroke of state.strokes) {
|
|
drawDarknessStroke(ctx, stroke, w, h);
|
|
}
|
|
if (state.draft && state.draft.points.length > 0) {
|
|
drawDarknessStroke(ctx, state.draft, w, h);
|
|
}
|
|
}, [state, viewport]);
|
|
|
|
if (!state?.enabled || !viewport) return null;
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
aria-hidden
|
|
style={{
|
|
position: 'absolute',
|
|
left: viewport.x,
|
|
top: viewport.y,
|
|
width: viewport.w,
|
|
height: viewport.h,
|
|
opacity: overlayAlpha,
|
|
pointerEvents: 'none',
|
|
zIndex: 3,
|
|
...style,
|
|
}}
|
|
/>
|
|
);
|
|
}
|