1
0
Fork 0

keep undo buffer

This commit is contained in:
Sean Sube 2023-02-12 23:32:06 -06:00
parent bb05395563
commit b7e597c68f
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
1 changed files with 79 additions and 91 deletions

View File

@ -1,10 +1,10 @@
import { doesExist, Maybe, mustExist } from '@apextoaster/js-utils'; import { doesExist, Maybe, mustExist } from '@apextoaster/js-utils';
import { FormatColorFill, Gradient, InvertColors, Undo } from '@mui/icons-material'; import { FormatColorFill, Gradient, InvertColors, Undo } from '@mui/icons-material';
import { Button, Stack, Typography } from '@mui/material'; import { Button, Stack, Typography } from '@mui/material';
import { createLogger } from 'browser-bunyan';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import React, { RefObject, useContext, useEffect, useMemo, useRef, useState } from 'react'; import React, { RefObject, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { createLogger } from 'browser-bunyan';
import { SAVE_TIME } from '../../config.js'; import { SAVE_TIME } from '../../config.js';
import { ConfigContext, StateContext } from '../../state.js'; import { ConfigContext, StateContext } from '../../state.js';
@ -55,20 +55,15 @@ export function MaskCanvas(props: MaskCanvasProps) {
const { params } = mustExist(useContext(ConfigContext)); const { params } = mustExist(useContext(ConfigContext));
function composite() { function composite() {
if (doesExist(visibleRef.current)) { if (doesExist(maskRef.current)) {
const { ctx } = getClearContext(visibleRef); const { ctx } = getClearContext(maskRef);
if (doesExist(maskRef.current)) {
ctx.globalAlpha = MASK_OPACITY;
ctx.drawImage(maskRef.current, 0, 0);
}
if (doesExist(bufferRef.current)) { if (doesExist(bufferRef.current)) {
ctx.globalAlpha = MASK_OPACITY; ctx.globalAlpha = MASK_OPACITY;
ctx.drawImage(bufferRef.current, 0, 0); ctx.drawImage(bufferRef.current, 0, 0);
} }
if (doesExist(brushRef.current) && maskState.current !== MASK_STATE.painting) { if (doesExist(brushRef.current) && painting.current === false) {
ctx.drawImage(brushRef.current, 0, 0); ctx.drawImage(brushRef.current, 0, 0);
} }
} }
@ -86,82 +81,82 @@ export function MaskCanvas(props: MaskCanvasProps) {
composite(); composite();
} }
function drawClicks(c2: Array<Point>, set: (value: React.SetStateAction<Array<Point>>) => void): boolean { function drawClicks(clicks: Array<Point>): void {
if (c2.length > 0) { if (clicks.length > 0) {
logger.debug('drawing clicks', { count: c2.length }); logger.debug('drawing clicks', { count: clicks.length });
const { ctx } = getContext(bufferRef); const { ctx } = getContext(bufferRef);
ctx.fillStyle = grayToRGB(brush.color, brush.strength); ctx.fillStyle = grayToRGB(brush.color, brush.strength);
for (const click of c2) { for (const click of clicks) {
drawCircle(ctx, click, brush.size); drawCircle(ctx, click, brush.size);
} }
dirty.current = true;
composite(); composite();
set([]);
return true;
} }
return false;
} }
async function drawMask(file: Blob): Promise<void> { async function drawMask(file: Blob): Promise<void> {
const image = await imageFromBlob(file); const image = await imageFromBlob(file);
logger.debug('draw mask'); if (doesExist(bufferRef.current)) {
logger.debug('draw mask');
const { canvas, ctx } = getClearContext(maskRef); const { canvas, ctx } = getClearContext(maskRef);
ctx.globalAlpha = FULL_OPACITY; ctx.globalAlpha = FULL_OPACITY;
ctx.drawImage(image, 0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
// getClearContext(bufferRef); composite();
composite(); }
}
function drawUndo(): void {
if (doesExist(bufferRef.current) && doesExist(undoRef.current)) {
logger.debug('draw undo');
const { ctx } = getClearContext(bufferRef);
ctx.drawImage(undoRef.current, 0, 0);
composite();
}
} }
function finishPainting() { function finishPainting() {
logger.debug('finish painting'); logger.debug('finish painting');
painting.current = false;
if (doesExist(brushRef.current)) { if (doesExist(brushRef.current)) {
getClearContext(brushRef); getClearContext(brushRef);
} }
if (drawClicks(clicks, setClicks) === false) { if (dirty.current) {
logger.debug('force compositing'); save();
composite();
}
if (maskState.current === MASK_STATE.painting) {
maskState.current = MASK_STATE.dirty;
} }
} }
function flushBuffer(): void { function saveUndo(): void {
if (doesExist(maskRef.current) && doesExist(bufferRef.current)) { if (doesExist(bufferRef.current) && doesExist(undoRef.current)) {
logger.debug('flush buffer'); logger.debug('save undo');
const { ctx } = getContext(maskRef); const { ctx } = getClearContext(undoRef);
ctx.drawImage(bufferRef.current, 0, 0); ctx.drawImage(bufferRef.current, 0, 0);
getClearContext(bufferRef);
composite();
} }
} }
function saveMask(): void { function saveMask(): void {
if (doesExist(maskRef.current)) { if (doesExist(bufferRef.current)) {
logger.debug('save mask'); logger.debug('save mask');
if (maskState.current === MASK_STATE.clean) { if (dirty.current === false) {
return; return;
} }
maskRef.current.toBlob((blob) => { bufferRef.current.toBlob((blob) => {
maskState.current = MASK_STATE.clean; dirty.current = false;
props.onSave(mustExist(blob)); props.onSave(mustExist(blob));
}); });
} }
} }
const draw = useMemo(() => throttle(drawClicks, DRAW_TIME), []); const save = useMemo(() => throttle(saveMask, SAVE_TIME), []);
const save = useMemo(() => throttle(saveMask, SAVE_TIME, {
trailing: true,
}), []);
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const brushRef = useRef<HTMLCanvasElement>(null); const brushRef = useRef<HTMLCanvasElement>(null);
@ -170,12 +165,12 @@ export function MaskCanvas(props: MaskCanvasProps) {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const maskRef = useRef<HTMLCanvasElement>(null); const maskRef = useRef<HTMLCanvasElement>(null);
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const visibleRef = useRef<HTMLCanvasElement>(null); const undoRef = useRef<HTMLCanvasElement>(null);
// painting state // painting state
const maskState = useRef(MASK_STATE.clean); const painting = useRef(false);
const [background, setBackground] = useState<string>(); const dirty = useRef(false);
const [clicks, setClicks] = useState<Array<Point>>([]); const background = useRef<string>();
const state = mustExist(useContext(StateContext)); const state = mustExist(useContext(StateContext));
const brush = useStore(state, (s) => s.brush); const brush = useStore(state, (s) => s.brush);
@ -183,14 +178,10 @@ export function MaskCanvas(props: MaskCanvasProps) {
const setBrush = useStore(state, (s) => s.setBrush); const setBrush = useStore(state, (s) => s.setBrush);
useEffect(() => { useEffect(() => {
if (maskState.current === MASK_STATE.dirty) { if (dirty.current) {
save(); save();
} }
}, [dirty.current]);
return () => {
logger.debug('save cleanup');
};
}, [maskState.current]);
useEffect(() => { useEffect(() => {
if (doesExist(bufferRef.current) && doesExist(mask)) { if (doesExist(bufferRef.current) && doesExist(mask)) {
@ -202,24 +193,24 @@ export function MaskCanvas(props: MaskCanvasProps) {
useEffect(() => { useEffect(() => {
if (doesExist(source)) { if (doesExist(source)) {
if (doesExist(background)) { if (doesExist(background.current)) {
URL.revokeObjectURL(background); URL.revokeObjectURL(background.current);
} }
setBackground(URL.createObjectURL(source)); background.current = URL.createObjectURL(source);
// initialize the mask if it does not exist // initialize the mask if it does not exist
if (doesExist(mask) === false) { if (doesExist(mask) === false) {
getClearContext(bufferRef); getClearContext(bufferRef);
maskState.current = MASK_STATE.dirty; dirty.current = true;
} }
} }
}, [source]); }, [source]);
// last resort to draw lost clicks // last resort to draw lost clicks
// const lostClicks = drawClicks(); // const lostClicks = drawClicks();
logger.debug('rendered', { clicks: clicks.length }); logger.debug('rendered');
draw(clicks, setClicks); // draw(clicks, setClicks);
const styles: React.CSSProperties = { const styles: React.CSSProperties = {
backgroundPosition: 'top left', backgroundPosition: 'top left',
@ -230,8 +221,8 @@ export function MaskCanvas(props: MaskCanvasProps) {
maxWidth: params.width.default, maxWidth: params.width.default,
}; };
if (doesExist(background)) { if (doesExist(background.current)) {
styles.backgroundImage = `url(${background})`; styles.backgroundImage = `url(${background.current})`;
} }
return <Stack spacing={2}> return <Stack spacing={2}>
@ -240,6 +231,7 @@ export function MaskCanvas(props: MaskCanvasProps) {
height={params.height.default} height={params.height.default}
width={params.width.default} width={params.width.default}
style={{ style={{
...styles,
display: 'none', display: 'none',
}} }}
/> />
@ -248,6 +240,16 @@ export function MaskCanvas(props: MaskCanvasProps) {
height={params.height.default} height={params.height.default}
width={params.width.default} width={params.width.default}
style={{ style={{
...styles,
display: 'none',
}}
/>
<canvas
ref={undoRef}
height={params.height.default}
width={params.width.default}
style={{
...styles,
display: 'none', display: 'none',
}} }}
/> />
@ -255,43 +257,32 @@ export function MaskCanvas(props: MaskCanvasProps) {
ref={maskRef} ref={maskRef}
height={params.height.default} height={params.height.default}
width={params.width.default} width={params.width.default}
style={{
display: 'none',
}}
/>
<canvas
ref={visibleRef}
height={params.height.default}
width={params.width.default}
style={styles} style={styles}
onClick={(event) => { onClick={(event) => {
logger.debug('mouse click', { state: maskState.current, clicks: clicks.length }); logger.debug('mouse click', { state: painting.current });
const canvas = mustExist(visibleRef.current); const canvas = mustExist(maskRef.current);
const bounds = canvas.getBoundingClientRect(); const bounds = canvas.getBoundingClientRect();
setClicks([...clicks, { drawClicks([{
x: event.clientX - bounds.left, x: event.clientX - bounds.left,
y: event.clientY - bounds.top, y: event.clientY - bounds.top,
}]); }]);
drawClicks(clicks, setClicks);
maskState.current = MASK_STATE.dirty;
}} }}
onMouseDown={() => { onMouseDown={() => {
logger.debug('mouse down', { state: maskState.current, clicks: clicks.length }); logger.debug('mouse down', { state: painting.current });
maskState.current = MASK_STATE.painting; painting.current = true;
flushBuffer(); saveUndo();
}} }}
onMouseLeave={finishPainting} onMouseLeave={finishPainting}
onMouseOut={finishPainting} onMouseOut={finishPainting}
onMouseUp={finishPainting} onMouseUp={finishPainting}
onMouseMove={(event) => { onMouseMove={(event) => {
const canvas = mustExist(visibleRef.current); const canvas = mustExist(maskRef.current);
const bounds = canvas.getBoundingClientRect(); const bounds = canvas.getBoundingClientRect();
if (maskState.current === MASK_STATE.painting) { if (painting.current) {
setClicks([...clicks, { drawClicks([{
x: event.clientX - bounds.left, x: event.clientX - bounds.left,
y: event.clientY - bounds.top, y: event.clientY - bounds.top,
}]); }]);
@ -345,10 +336,7 @@ export function MaskCanvas(props: MaskCanvasProps) {
<Button <Button
variant='outlined' variant='outlined'
startIcon={<Undo />} startIcon={<Undo />}
onClick={() => { onClick={() => drawUndo()}
getClearContext(bufferRef);
composite();
}}
/> />
<Button <Button
variant='outlined' variant='outlined'
@ -356,7 +344,7 @@ export function MaskCanvas(props: MaskCanvasProps) {
onClick={() => { onClick={() => {
floodCanvas(maskRef, floodBlack); floodCanvas(maskRef, floodBlack);
composite(); composite();
maskState.current = MASK_STATE.dirty; dirty.current = true;
}}> }}>
Fill with black Fill with black
</Button> </Button>
@ -366,7 +354,7 @@ export function MaskCanvas(props: MaskCanvasProps) {
onClick={() => { onClick={() => {
floodCanvas(maskRef, floodWhite); floodCanvas(maskRef, floodWhite);
composite(); composite();
maskState.current = MASK_STATE.dirty; dirty.current = true;
}}> }}>
Fill with white Fill with white
</Button> </Button>
@ -376,7 +364,7 @@ export function MaskCanvas(props: MaskCanvasProps) {
onClick={() => { onClick={() => {
floodCanvas(maskRef, floodInvert); floodCanvas(maskRef, floodInvert);
composite(); composite();
maskState.current = MASK_STATE.dirty; dirty.current = true;
}}> }}>
Invert Invert
</Button> </Button>
@ -386,7 +374,7 @@ export function MaskCanvas(props: MaskCanvasProps) {
onClick={() => { onClick={() => {
floodCanvas(maskRef, floodBelow); floodCanvas(maskRef, floodBelow);
composite(); composite();
maskState.current = MASK_STATE.dirty; dirty.current = true;
}}> }}>
Gray to black Gray to black
</Button> </Button>
@ -396,7 +384,7 @@ export function MaskCanvas(props: MaskCanvasProps) {
onClick={() => { onClick={() => {
floodCanvas(maskRef, floodAbove); floodCanvas(maskRef, floodAbove);
composite(); composite();
maskState.current = MASK_STATE.dirty; dirty.current = true;
}}> }}>
Gray to white Gray to white
</Button> </Button>