keep undo buffer
This commit is contained in:
parent
bb05395563
commit
b7e597c68f
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue