Skip to content

Commit

Permalink
refactor the Canvas component
Browse files Browse the repository at this point in the history
  • Loading branch information
aradzie committed Dec 8, 2024
1 parent d4dfbb7 commit 2ebcb52
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 64 deletions.
28 changes: 28 additions & 0 deletions packages/keybr-widget/lib/components/canvas/Canvas.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { test } from "node:test";
import { render } from "@testing-library/react";
import { useEffect, useRef } from "react";
import { type Size } from "../../utils/size.ts";
import { Canvas } from "./Canvas.tsx";
import { type CanvasRef } from "./Canvas.types.ts";
import { type Graphics } from "./graphics.ts";

test("render", () => {
const paint =
({ width, height }: Size) =>
(g: Graphics) => {
g.clear(0, 0, width, height);
};

function UnderTest() {
const ref = useRef<CanvasRef>(null);
useEffect(() => {
ref.current!.paint(paint);
ref.current!.getContext("2d")!.rect(0, 0, 100, 100);
}, []);
return <Canvas ref={ref} paint={paint} />;
}

const r = render(<UnderTest />);

r.unmount();
});
133 changes: 71 additions & 62 deletions packages/keybr-widget/lib/components/canvas/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,75 @@
import { memo, type ReactNode, type RefObject, useEffect, useRef } from "react";
import {
type ForwardedRef,
forwardRef,
memo,
useEffect,
useImperativeHandle,
useRef,
} from "react";
import { useElementSize } from "../../hooks/use-element-size.ts";
import { type Size } from "../../utils/size.ts";
import { type MouseProps, type WheelProps } from "../types.ts";
import { Graphics, type ShapeList } from "./graphics.ts";
import { type CanvasProps, type CanvasRef } from "./Canvas.types.ts";
import { Graphics } from "./graphics.ts";

export type PaintCallback = (size: Size) => ShapeList;

export const useCanvas = (
paint: PaintCallback,
): RefObject<HTMLCanvasElement> => {
const ref = useRef<HTMLCanvasElement>(null);
const size = useElementSize(ref);
useEffect(() => {
const canvas = ref.current;
if (canvas == null) {
return;
}
const context = canvas.getContext("2d");
if (context == null) {
return;
}
if (size != null && size.width > 0 && size.height > 0) {
const ratio = devicePixelRatio;
const width = Math.max(1, size.width * ratio);
const height = Math.max(1, size.height * ratio);
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
export const Canvas = memo(
forwardRef(function Canvas(
{ className, id, paint, style, title, onResize, ...props }: CanvasProps,
ref: ForwardedRef<CanvasRef>,
) {
const element = useRef<HTMLCanvasElement>(null);
const size = useElementSize(element);
useImperativeHandle(ref, () => ({
getSize: () => size,
getContext: (...args) => {
const canvas = element.current!;
return canvas.getContext.call(canvas, ...args) as any;
},
toBlob: (...args) => {
const canvas = element.current!;
canvas.toBlob.call(canvas, ...args);
},
toDataURL: (...args) => {
const canvas = element.current!;
return canvas.toDataURL.call(canvas, ...args);
},
paint: (paint) => {
if (size != null && size.width > 0 && size.height > 0) {
const canvas = element.current!;
const context = canvas.getContext("2d")!;
new Graphics(context).paint(paint(size));
}
},
}));
useEffect(() => {
if (size != null && size.width > 0 && size.height > 0) {
const canvas = element.current!;
const context = canvas.getContext("2d")!;
const ratio = devicePixelRatio;
canvas.width = Math.max(1, size.width * ratio);
canvas.height = Math.max(1, size.height * ratio);
context.scale(ratio, ratio);
}
new Graphics(context).paint(paint(size));
}
}, [paint, size]);
return ref;
};

export const Canvas = memo(function Canvas({
paint,
title,
...props
}: {
readonly paint: PaintCallback;
readonly title?: string;
} & MouseProps &
WheelProps): ReactNode {
const ref = useCanvas(paint);
return (
<canvas
{...props}
ref={ref}
title={title}
style={{
display: "block",
position: "absolute",
insetInlineStart: "0px",
insetBlockStart: "0px",
inlineSize: "100%",
blockSize: "100%",
margin: "0px",
padding: "0px",
borderStyle: "none",
}}
/>
);
});
}, [size]);
useEffect(() => {
if (size != null && size.width > 0 && size.height > 0) {
const canvas = element.current!;
const context = canvas.getContext("2d")!;
new Graphics(context).paint(paint(size));
}
}, [size, paint]);
return (
<canvas
{...props}
ref={element}
id={id}
className={className}
style={{
display: "block",
inlineSize: "100%",
blockSize: "100%",
...style,
}}
title={title}
/>
);
}),
);
24 changes: 24 additions & 0 deletions packages/keybr-widget/lib/components/canvas/Canvas.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type CSSProperties } from "react";
import { type Size } from "../../utils/size.ts";
import { type ClassName, type MouseProps, type WheelProps } from "../types.ts";
import { type ShapeList } from "./graphics.ts";

export type PaintCallback = (size: Size) => ShapeList;

export type CanvasRef = {
readonly getSize: () => Size | null;
readonly getContext: typeof HTMLCanvasElement.prototype.getContext;
readonly toBlob: typeof HTMLCanvasElement.prototype.toBlob;
readonly toDataURL: typeof HTMLCanvasElement.prototype.toDataURL;
readonly paint: (callback: PaintCallback) => void;
};

export type CanvasProps = {
readonly className?: ClassName;
readonly id?: string;
readonly paint: PaintCallback;
readonly style?: CSSProperties;
readonly title?: string;
readonly onResize?: (size: Size) => void;
} & MouseProps &
WheelProps;
2 changes: 1 addition & 1 deletion packages/keybr-widget/lib/components/canvas/graphics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type StrokeStyle,
type TextStyle,
toCanvasColor,
} from "./style.ts";
} from "./graphics-style.ts";

export type Shape = (g: Graphics) => void;

Expand Down
3 changes: 2 additions & 1 deletion packages/keybr-widget/lib/components/canvas/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./Canvas.tsx";
export * from "./Canvas.types.ts";
export * from "./graphics.ts";
export * from "./style.ts";
export * from "./graphics-style.ts";

0 comments on commit 2ebcb52

Please sign in to comment.