Merge pull request #16332 from ShadowLarkens/cummies

Massively improve painting experience
This commit is contained in:
Heroman3003
2024-09-21 21:53:17 +10:00
committed by GitHub
+273 -21
View File
@@ -1,8 +1,9 @@
import { clamp } from 'common/math';
import { BooleanLike } from 'common/react';
import { Component, createRef, RefObject } from 'react';
import { Component, createRef, RefObject, useState } from 'react';
import { useBackend } from '../backend';
import { Box, Button } from '../components';
import { Box, Button, Stack } from '../components';
import { Window } from '../layouts';
const PX_PER_UNIT = 24;
@@ -12,11 +13,16 @@ type PaintCanvasProps = Partial<{
value: string[][];
dotsize: number;
res: number;
tool: Tool;
}>;
class PaintCanvas extends Component<PaintCanvasProps> {
canvasRef: RefObject<HTMLCanvasElement>;
mouseIsDown: boolean;
lastSuccessfulPaint: number;
onCVClick: (x: number, y: number) => void;
lineStart: [number, number] | null;
lastHovered: [number, number] | null;
constructor(props) {
super(props);
@@ -32,10 +38,128 @@ class PaintCanvas extends Component<PaintCanvasProps> {
this.drawCanvas(this.props);
}
getLinePixels() {
if (!this.lineStart || !this.lastHovered) {
return;
}
let [x0, y0] = this.lineStart;
let [x1, y1] = this.lastHovered;
x0 = x0 - 1;
x1 = x1 - 1;
y0 = y0 - 1;
y1 = y1 - 1;
let dx = Math.abs(x1 - x0);
let sx = x0 < x1 ? 1 : -1;
let dy = -Math.abs(y1 - y0);
let sy = y0 < y1 ? 1 : -1;
let error = dx + dy;
let pixels: [number, number][] = [];
while (true) {
pixels.push([x0, y0]);
// grid[x0][y0] = '#000';
if (x0 === x1 && y0 === y1) {
break;
}
let e2 = error * 2;
if (e2 >= dy) {
error = error + dy;
x0 = x0 + sx;
}
if (e2 <= dx) {
error = error + dx;
y0 = y0 + sy;
}
}
return pixels;
}
drawLine(grid: string[][]) {
let pixels = this.getLinePixels();
if (!pixels) {
return;
}
for (let [x, y] of pixels) {
if (x < grid.length && y < grid[0].length) {
grid[x][y] = '#000000';
}
}
}
areaFill(grid: string[][]) {
if (!this.lastHovered) {
return;
}
let [x_start, y_start] = this.lastHovered;
x_start = clamp(x_start - 1, 0, grid.length - 1);
y_start = clamp(y_start - 1, 0, grid[0].length - 1);
const origPixelColor = grid[x_start][y_start];
const inside = (x_c, y_c) => {
if (x_c < 0 || y_c < 0 || x_c >= grid.length || y_c >= grid[0].length) {
return false;
}
return grid[x_c][y_c] === origPixelColor;
};
let s: [number, number, number, number][] = [];
s.push([x_start, x_start, y_start, 1]);
s.push([x_start, x_start, y_start - 1, -1]);
let pixels_touched: [number, number][] = [];
while (s.length) {
// This can't fail because of our while condition
let [x1, x2, y, dy] = s.pop()!;
let x = x1;
if (inside(x, y)) {
while (inside(x - 1, y)) {
grid[x - 1][y] = '#000000';
pixels_touched.push([x - 1, y]);
x = x - 1;
}
if (x < x1) {
s.push([x, x1 - 1, y - dy, -dy]);
}
}
while (x1 <= x2) {
while (inside(x1, y)) {
grid[x1][y] = '#000000';
pixels_touched.push([x1, y]);
x1 = x1 + 1;
}
if (x1 > x) {
s.push([x, x1 - 1, y + dy, dy]);
}
if (x1 - 1 > x2) {
s.push([x2 + 1, x1 - 1, y - dy, -dy]);
}
x1 = x1 + 1;
while (x1 < x2 && !inside(x1, y)) {
x1 = x1 + 1;
}
x = x1;
}
}
return pixels_touched;
}
drawCanvas(propSource: PaintCanvasProps) {
const canvas = this.canvasRef.current!;
const ctx = canvas.getContext('2d')!;
const grid = propSource.value;
const grid = JSON.parse(JSON.stringify(propSource.value));
if (!grid) {
return;
}
@@ -48,6 +172,13 @@ class PaintCanvas extends Component<PaintCanvasProps> {
const y_scale = Math.round(canvas.height / y_size);
ctx.save();
ctx.scale(x_scale, y_scale);
if (this.mouseIsDown && propSource.tool === Tool.Line) {
this.drawLine(grid);
} else if (propSource.tool === Tool.Fill) {
this.areaFill(grid);
}
for (let x = 0; x < grid.length; x++) {
const element = grid[x];
for (let y = 0; y < element.length; y++) {
@@ -59,22 +190,96 @@ class PaintCanvas extends Component<PaintCanvasProps> {
ctx.restore();
}
clickwrapper(event: React.MouseEvent) {
getCoord(
event: React.MouseEvent<HTMLCanvasElement, MouseEvent>,
): [number, number] | null {
const value = this.props.value;
if (!value) {
return;
return null;
}
const x_size = value.length;
if (!x_size) {
return;
return null;
}
const y_size = this.props.value[0].length;
const canvas = this.canvasRef.current!;
const x_scale = canvas.width / x_size;
const y_scale = canvas.height / y_size;
const x = Math.floor(event.nativeEvent.offsetX / x_scale) + 1;
const y = Math.floor(event.nativeEvent.offsetY / y_scale) + 1;
this.onCVClick(x, y);
return [x, y];
}
mouseDown(event: React.MouseEvent<HTMLCanvasElement, MouseEvent>) {
this.mouseIsDown = true;
let coord = this.getCoord(event);
if (!coord) {
return;
}
this.lineStart = coord;
this.lastHovered = coord;
if (this.props.tool === Tool.Paintbrush) {
this.onCVClick(coord[0], coord[1]);
} else if (this.props.tool === Tool.Fill) {
let gridCopy = JSON.parse(JSON.stringify(this.props.value));
let pixels_touched = this.areaFill(gridCopy);
if (pixels_touched) {
for (let [x, y] of pixels_touched) {
this.onCVClick(x + 1, y + 1);
}
}
}
}
mouseUp(event: React.MouseEvent<HTMLCanvasElement, MouseEvent>) {
this.mouseIsDown = false;
let coord = this.getCoord(event);
if (!coord) {
return;
}
this.lastHovered = coord;
if (this.props.tool === Tool.Line) {
let line = this.getLinePixels();
if (line) {
for (let [x, y] of line) {
this.onCVClick(x + 1, y + 1);
}
}
}
}
mouseMove(event: React.MouseEvent<HTMLCanvasElement, MouseEvent>) {
const coord = this.getCoord(event);
if (!coord) {
return;
}
this.lastHovered = coord;
let time = new Date().getTime();
if (this.lastSuccessfulPaint + 50 > time) {
return;
}
if (this.mouseIsDown && this.props.tool === Tool.Paintbrush) {
this.onCVClick(coord[0], coord[1]);
this.lastSuccessfulPaint = time;
} else if (this.mouseIsDown && this.props.tool === Tool.Line) {
// Force redraw
this.drawCanvas(this.props);
this.lastSuccessfulPaint = time;
} else if (this.props.tool === Tool.Fill) {
this.drawCanvas(this.props);
}
}
render() {
@@ -86,7 +291,10 @@ class PaintCanvas extends Component<PaintCanvasProps> {
width={width * dotsize || 300}
height={height * dotsize || 300}
{...rest}
onClick={(e) => this.clickwrapper(e)}
// onClick={(e) => this.clickwrapper(e)}
onMouseDown={(e) => this.mouseDown(e)}
onMouseMove={(e) => this.mouseMove(e)}
onMouseUp={(e) => this.mouseUp(e)}
>
Canvas failed to render.
</canvas>
@@ -106,9 +314,17 @@ type Data = {
finalized: BooleanLike;
};
enum Tool {
Paintbrush,
Line,
Fill,
}
export const Canvas = (props) => {
const { act, data } = useBackend<Data>();
const [tool, setTool] = useState<Tool>(Tool.Paintbrush);
const dotsize = PX_PER_UNIT;
const [width, height] = getImageSize(data.grid);
return (
@@ -117,20 +333,56 @@ export const Canvas = (props) => {
height={Math.min(700, height * dotsize + 72)}
>
<Window.Content>
<Stack>
<Stack.Item basis="10%">
<Stack vertical>
<Stack.Item>
<Button
icon="paintbrush"
fontSize={2}
onClick={() => setTool(Tool.Paintbrush)}
selected={tool === Tool.Paintbrush}
tooltip="Paintbrush"
/>
</Stack.Item>
<Stack.Item>
<Button
icon="grip-lines"
fontSize={2}
iconRotation={-45}
onClick={() => setTool(Tool.Line)}
selected={tool === Tool.Line}
tooltip="Line"
/>
</Stack.Item>
<Stack.Item>
<Button
icon="bucket"
fontSize={2}
onClick={() => setTool(Tool.Fill)}
selected={tool === Tool.Fill}
tooltip="Fill"
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow />
<Stack.Item pr={1}>
<PaintCanvas
tool={tool}
value={data.grid}
dotsize={dotsize}
onCanvasClick={(x, y) => act('paint', { x, y })}
/>
</Stack.Item>
</Stack>
<Box textAlign="center">
<PaintCanvas
value={data.grid}
dotsize={dotsize}
onCanvasClick={(x, y) => act('paint', { x, y })}
/>
<Box>
{!data.finalized && (
<Button.Confirm onClick={() => act('finalize')}>
Finalize
</Button.Confirm>
)}
&nbsp;{data.name}
</Box>
{!data.finalized && (
<Button.Confirm onClick={() => act('finalize')}>
Finalize
</Button.Confirm>
)}
&nbsp;{data.name}
</Box>
</Window.Content>
</Window>