Skip to content

Commit

Permalink
Fix merge Merge branch 'master' into bresenham-circle
Browse files Browse the repository at this point in the history
  • Loading branch information
FredrikMeyer committed Nov 8, 2023
2 parents a751c39 + 41f067a commit deedc33
Show file tree
Hide file tree
Showing 9 changed files with 648 additions and 608 deletions.
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,26 @@
"react-icons": "^4.11.0"
},
"devDependencies": {
"@types/react": "^18.2.33",
"@types/react-dom": "^18.2.8",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-react": "^4.1.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.1.1",
"autoprefixer": "^10.4.16",
"eslint": "^8.52.0",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss-modules-values": "^4.0.0",
"postcss-nesting": "^12.0.1",
"prettier": "3.0.3",
"sass": "^1.68.0",
"stylelint": "^15.10.1",
"stylelint-config-css-modules": "^4.2.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-config-standard-scss": "^11.0.0",
"sass": "^1.69.5",
"stylelint": "^15.11.0",
"stylelint-config-css-modules": "^4.3.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-config-standard-scss": "^11.1.0",
"typescript": "^5.2.2",
"typescript-plugin-css-modules": "^5.0.2",
"vite": "^4.4.9"
"vite": "^4.5.0"
},
"browserslist": [
"defaults"
Expand Down
18 changes: 18 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ function useCanvasDimension(
const [canvasDimensions, setCanvasDimensions] =
React.useState<Optional<[number, number]>>();

React.useEffect(() => {
const observer = new ResizeObserver((e) => {
if (e.length > 0) {
const observerEntry = e[0];
setCanvasDimensions([
observerEntry.contentRect.height,
observerEntry.contentRect.width,
]);
}
});
const current = canvasContainerRef.current;
if (current) {
observer.observe(current);
}

return () => (current ? observer.unobserve(current) : undefined);
});

React.useEffect(() => {
const current = canvasContainerRef.current;
if (current) {
Expand Down
316 changes: 2 additions & 314 deletions src/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,320 +2,8 @@ import React from "react";
import { DrawingTool } from "./Toolbar";
import "./Canvas.scss";
import { Color } from "./colors";
import FloodFiller from "./floodFill";

function useCtx(reference: React.RefObject<HTMLCanvasElement>) {
const [ctx, setCtx] = React.useState<CanvasRenderingContext2D | undefined>(
undefined,
);

React.useEffect(() => {
const canvasElement = reference.current;
if (canvasElement !== null) {
const context = canvasElement.getContext("2d");
if (context) {
setCtx(context);
}
}
}, [reference]);

if (ctx) {
ctx.imageSmoothingEnabled = false;
}

return { ctx: ctx };
}

function getNewPixel(contxt: CanvasRenderingContext2D, size: number) {
const p = contxt.createImageData(size, size);
const d = p.data;

for (let i = 0; i < d.length / 4; i++) {
d[4 * i] = 0;
d[4 * i + 1] = 0;
d[4 * i + 2] = 0;
d[4 * i + 3] = 255;
}

return p;
}

function mouseEventToCoords(
ev: React.MouseEvent<HTMLCanvasElement>,
top: number,
left: number,
) {
const x = ev.clientX - left;
const y = ev.clientY - top;

const scale = window.devicePixelRatio;

return [x * scale, y * scale];
}

function DrawingCanvas({
activeTool,
toolSize,
currentColor,
width: originalWidth,
height: originalHeight,
canvasRef,
backgroundCanvasRef,
leftTop,
onCommit,
}: {
activeTool: DrawingTool;
toolSize: number;
currentColor: Color;
width: number;
height: number;
canvasRef: React.RefObject<HTMLCanvasElement>;
backgroundCanvasRef: React.RefObject<HTMLCanvasElement>;
leftTop: { left: number; top: number };
onCommit: () => void;
}) {
const [width, height] = useScaleByDevicePixelRatio(
canvasRef,
originalWidth,
originalHeight,
);

const [drawingState, setDrawingState] = React.useState<
| { tool: "LINE"; startPoint: [number, number] }
| { tool: "SQUARE"; startPoint: [number, number] }
| { tool: "CIRCLE"; startPoint: [number, number] }
| undefined
>(undefined);
const { ctx } = useCtx(canvasRef);

const onDrawStart = React.useCallback(
(x: number, y: number) => {
if (activeTool === "LINE") {
ctx?.moveTo(x, y);
} else {
ctx?.beginPath();
}
},
[ctx, activeTool],
);

const onMove = React.useCallback(
(ctx: CanvasRenderingContext2D, x: number, y: number) => {
ctx.lineWidth = toolSize;
ctx.strokeStyle = Color.toRGBString(currentColor);
if (activeTool === "LINE" && drawingState?.tool === "LINE") {
const [startX, startY] = drawingState.startPoint;
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(x, y);
ctx.stroke();
return;
}
if (activeTool === "SQUARE" && drawingState?.tool === "SQUARE") {
const [startX, startY] = drawingState.startPoint;
ctx.clearRect(0, 0, width, height);
const rectWidth = Math.floor(x - startX);
const rectHeight = Math.floor(y - startY);
ctx.strokeRect(
Math.floor(startX) + 0.5,
Math.floor(startY) + 0.5,
rectWidth,
rectHeight,
);
ctx.stroke();
return;
}
if (activeTool === "ROUNDED_RECT" && drawingState?.tool === "SQUARE") {
const [startX, startY] = drawingState.startPoint;
ctx.clearRect(0, 0, width, height);
const rectWidth = Math.floor(x - startX);
const rectHeight = Math.floor(y - startY);
ctx.beginPath();
ctx.roundRect(
Math.floor(startX) + 0.5,
Math.floor(startY) + 0.5,
rectWidth,
rectHeight,
20,
);
ctx.stroke();
return;
}
if (activeTool === "CIRCLE" && drawingState?.tool === "CIRCLE") {
const [startX, startY] = drawingState.startPoint;
ctx.clearRect(0, 0, width, height);
const rectWidth = x - startX;
const rectHeight = y - startY;

const rx = Math.abs(rectWidth);
const ry = Math.abs(rectHeight);

const cx = Math.round(startX + 0.5 * rectWidth);
const cy = Math.round(startY + 0.5 * rectHeight);
ctx.beginPath();
ctx.fillStyle = Color.toRGBString(currentColor);
ctx.fillRect(cx, cy, 1, 1);

const pxl = getNewPixel(ctx, toolSize);
const circleSymmetricPts = (x: number, y: number) => {
ctx.putImageData(pxl, x, y);
ctx.putImageData(pxl, x, -y + 2 * cy); // x, -y
ctx.putImageData(pxl, -x + 2 * cx, y); // -x, y
ctx.putImageData(pxl, -x + 2 * cx, -y + 2 * cy); // -x, -y

ctx.putImageData(pxl, y - cy + cx, x - cx + cy); // y, x
ctx.putImageData(pxl, y - cy + cx, -x + cx + cy); // y, -x : y -> y - cy -> x - cx -> cx - x -> cx - x + cy
ctx.putImageData(pxl, -y + cy + cx, x - cx + cy); // - y, x : x -> x - cx -> y - cy -> cy - y -> cy -y + cx
ctx.putImageData(pxl, -y + cy + cx, -x + cx + cy); // -y, -x :
};

const drawCircle = (radius: number) => {
let xPosition = 0;
let yPosition = radius;
let d = 1 - radius;
circleSymmetricPts(cx + xPosition, cy + yPosition);

while (yPosition > xPosition) {
if (d < 0) {
d = d + 2 * xPosition + 3;
} else {
d = d + 2 * (xPosition - yPosition) + 5;
yPosition = yPosition - 1;
}
xPosition = xPosition + 1;
circleSymmetricPts(cx + xPosition, cy + yPosition);
}
};

drawCircle(rx);

ctx.stroke();
return;
}
if (activeTool == "DRAW") {
ctx.lineTo(x, y);
ctx.stroke();
} else if (activeTool == "ERASE") {
ctx.strokeStyle = "white";
ctx.lineTo(x, y);
ctx.stroke();
}
},

[currentColor, activeTool, toolSize, height, width, drawingState],
);

const floodfiller = React.useMemo(
() =>
backgroundCanvasRef.current
? new FloodFiller(backgroundCanvasRef.current, width, height)
: undefined,
[backgroundCanvasRef, width, height],
);

const onPointerDown = React.useCallback(
(e: React.PointerEvent<HTMLCanvasElement>) => {
const [x, y] = mouseEventToCoords(e, leftTop.top, leftTop.left);
if (activeTool === "LINE") {
setDrawingState({ tool: "LINE", startPoint: [x, y] });
}
if (activeTool === "SQUARE") {
setDrawingState({ tool: "SQUARE", startPoint: [x, y] });
}
if (activeTool === "ROUNDED_RECT") {
setDrawingState({ tool: "SQUARE", startPoint: [x, y] });
}
if (activeTool === "CIRCLE") {
setDrawingState({ tool: "CIRCLE", startPoint: [x, y] });
}
if (activeTool == "FILL") {
if (floodfiller) {
floodfiller.floodFill(x, y, currentColor);
}
}
onDrawStart(x, y);
},
[
activeTool,
leftTop.left,
leftTop.top,
onDrawStart,
floodfiller,
currentColor,
],
);

const onPointerMove = React.useCallback(
(e: React.PointerEvent<HTMLCanvasElement>) => {
if (e.buttons !== 1) return;

const [x, y] = mouseEventToCoords(e, leftTop.top, leftTop.left);

if (ctx) {
onMove(ctx, x, y);
}
},
[ctx, leftTop.left, leftTop.top, onMove],
);

const onPointerUp = React.useCallback(() => {
if (ctx) {
setDrawingState(undefined);
onCommit();
}
}, [ctx, onCommit]);

return (
<canvas
id="drawing-canvas"
ref={canvasRef}
width={width}
height={height}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
></canvas>
);
}

function useScaleByDevicePixelRatio(
canvasRef: React.RefObject<HTMLCanvasElement>,
width: number,
height: number,
) {
const [scaledWidthHeight, setScaledWidthHeight] = React.useState<
null | [number, number]
>(null);

React.useLayoutEffect(() => {
const current = canvasRef.current;
const context = current?.getContext("2d");

const scale = window.devicePixelRatio; // 2 on retina
if (current) {
if (!context) {
console.warn("Context not loaded before scaling.");
return;
}

setScaledWidthHeight([
Math.floor(width * scale),
Math.floor(height * scale),
]);

context.scale(scale, scale);
current.style.width = `${width}px`;
current.style.height = `${height}px`;
} else {
console.warn("Canvas element not loaded before scaling.");
}

return () => context?.scale(1 / scale, 1 / scale);
}, [canvasRef, height, width]);

return scaledWidthHeight || [width, height];
}
import { useScaleByDevicePixelRatio } from "./canvasUtils";
import DrawingCanvas from "./DrawingCanvas";

function BackgroundCanvas({
width: originalWidth,
Expand Down
Loading

0 comments on commit deedc33

Please sign in to comment.