diff --git a/gui/src/components/MaskCanvas.tsx b/gui/src/components/MaskCanvas.tsx index 797acd2f..6f9056d6 100644 --- a/gui/src/components/MaskCanvas.tsx +++ b/gui/src/components/MaskCanvas.tsx @@ -2,7 +2,7 @@ import { doesExist, Maybe, mustExist } from '@apextoaster/js-utils'; import { FormatColorFill, Gradient } from '@mui/icons-material'; import { Button, Stack } from '@mui/material'; import { throttle } from 'lodash'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; import { ConfigParams, DEFAULT_BRUSH, SAVE_TIME } from '../config.js'; import { NumericField } from './NumericField'; @@ -28,30 +28,6 @@ export const MASK_STATE = { dirty: 'dirty', }; -export function floodBelow(n: number): number { - if (n < THRESHOLDS.upper) { - return COLORS.black; - } else { - return COLORS.white; - } -} - -export function floodAbove(n: number): number { - if (n > THRESHOLDS.lower) { - return COLORS.white; - } else { - return COLORS.black; - } -} - -export function floodGray(n: number): number { - return n; -} - -export function grayToRGB(n: number): string { - return `rgb(${n.toFixed(0)},${n.toFixed(0)},${n.toFixed(0)})`; -} - export type FloodFn = (n: number) => number; export interface Point { @@ -71,28 +47,6 @@ export interface MaskCanvasProps { export function MaskCanvas(props: MaskCanvasProps) { const { base, config, source } = props; - function floodMask(flood: FloodFn) { - const buffer = mustExist(bufferRef.current); - const ctx = mustExist(buffer.getContext('2d')); - const image = ctx.getImageData(0, 0, buffer.width, buffer.height); - const pixels = image.data; - - for (let x = 0; x < buffer.width; ++x) { - for (let y = 0; y < buffer.height; ++y) { - const i = (y * buffer.width * PIXEL_SIZE) + (x * PIXEL_SIZE); - const hue = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / PIXEL_WEIGHT; - const final = flood(hue); - - pixels[i] = final; - pixels[i + 1] = final; - pixels[i + 2] = final; - } - } - - ctx.putImageData(image, 0, 0); - save(); - } - function saveMask(): void { if (doesExist(bufferRef.current)) { if (maskState.current === MASK_STATE.clean) { @@ -107,27 +61,21 @@ export function MaskCanvas(props: MaskCanvasProps) { } function drawBuffer() { - if (doesExist(bufferRef.current) && doesExist(canvasRef.current)) { - const dest = mustExist(canvasRef.current); - const ctx = mustExist(dest.getContext('2d')); - - ctx.clearRect(0, 0, dest.width, dest.height); + if (doesExist(brushRef.current) && doesExist(bufferRef.current) && doesExist(canvasRef.current)) { + const { ctx } = getClearContext(canvasRef); ctx.globalAlpha = MASK_OPACITY; ctx.drawImage(bufferRef.current, 0, 0); - } - } - function drawCircle(ctx: CanvasRenderingContext2D, point: Point): void { - ctx.beginPath(); - ctx.arc(point.x, point.y, brushSize, 0, FULL_CIRCLE); - ctx.fill(); + if (maskState.current !== MASK_STATE.painting) { + ctx.drawImage(brushRef.current, 0, 0); + } + } } function drawSource(file: Blob): void { const image = new Image(); image.onload = () => { - const buffer = mustExist(bufferRef.current); - const ctx = mustExist(buffer.getContext('2d')); + const { ctx } = getContext(bufferRef); ctx.drawImage(image, 0, 0); URL.revokeObjectURL(src); @@ -139,6 +87,11 @@ export function MaskCanvas(props: MaskCanvasProps) { } function finishPainting() { + if (doesExist(brushRef.current)) { + getClearContext(brushRef); + drawBuffer(); + } + if (maskState.current === MASK_STATE.painting) { maskState.current = MASK_STATE.dirty; save(); @@ -147,6 +100,8 @@ export function MaskCanvas(props: MaskCanvasProps) { const save = useMemo(() => throttle(saveMask, SAVE_TIME), []); + // eslint-disable-next-line no-null/no-null + const brushRef = useRef(null); // eslint-disable-next-line no-null/no-null const bufferRef = useRef(null); // eslint-disable-next-line no-null/no-null @@ -162,11 +117,11 @@ export function MaskCanvas(props: MaskCanvasProps) { useEffect(() => { // including clicks.length prevents the initial render from saving a blank canvas if (doesExist(bufferRef.current) && maskState.current === MASK_STATE.painting && clicks.length > 0) { - const ctx = mustExist(bufferRef.current.getContext('2d')); + const { ctx } = getContext(bufferRef); ctx.fillStyle = grayToRGB(brushColor); for (const click of clicks) { - drawCircle(ctx, click); + drawCircle(ctx, click, brushSize); } clicks.length = 0; @@ -206,6 +161,14 @@ export function MaskCanvas(props: MaskCanvasProps) { } return + { - if (maskState.current === MASK_STATE.painting) { - const canvas = mustExist(canvasRef.current); - const bounds = canvas.getBoundingClientRect(); + const canvas = mustExist(canvasRef.current); + const bounds = canvas.getBoundingClientRect(); + if (maskState.current === MASK_STATE.painting) { setClicks([...clicks, { x: event.clientX - bounds.left, y: event.clientY - bounds.top, }]); + } else { + const { ctx } = getClearContext(brushRef); + ctx.fillStyle = grayToRGB(brushColor); + + drawCircle(ctx, { + x: event.clientX - bounds.left, + y: event.clientY - bounds.top, + }, brushSize); + + drawBuffer(); } }} /> @@ -279,20 +251,94 @@ export function MaskCanvas(props: MaskCanvasProps) { - ; + + ; +} + +function getContext(ref: RefObject) { + const canvas = mustExist(ref.current); + const ctx = mustExist(canvas.getContext('2d')); + + return { canvas, ctx }; +} + +function getClearContext(ref: RefObject) { + const ret = getContext(ref); + ret.ctx.clearRect(0, 0, ret.canvas.width, ret.canvas.height); + + return ret; +} + +function drawCircle(ctx: CanvasRenderingContext2D, point: Point, size: number): void { + ctx.beginPath(); + ctx.arc(point.x, point.y, size, 0, FULL_CIRCLE); + ctx.fill(); +} + +export function floodBelow(n: number): number { + if (n < THRESHOLDS.upper) { + return COLORS.black; + } else { + return COLORS.white; + } +} + +export function floodAbove(n: number): number { + if (n > THRESHOLDS.lower) { + return COLORS.white; + } else { + return COLORS.black; + } +} + +export function floodGray(n: number): number { + return n; +} + +export function grayToRGB(n: number): string { + return `rgb(${n.toFixed(0)},${n.toFixed(0)},${n.toFixed(0)})`; +} + +function floodCanvas(ref: RefObject, flood: FloodFn) { + const { canvas, ctx } = getContext(ref); + const image = ctx.getImageData(0, 0, canvas.width, canvas.height); + const pixels = image.data; + + for (let x = 0; x < canvas.width; ++x) { + for (let y = 0; y < canvas.height; ++y) { + const i = (y * canvas.width * PIXEL_SIZE) + (x * PIXEL_SIZE); + const hue = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / PIXEL_WEIGHT; + const final = flood(hue); + + pixels[i] = final; + pixels[i + 1] = final; + pixels[i + 2] = final; + } + } + + ctx.putImageData(image, 0, 0); }