diff --git a/README.md b/README.md
index bd49b34..90582b2 100644
--- a/README.md
+++ b/README.md
@@ -21,15 +21,22 @@
```tsx
// App.jsx
import React from "react";
-import { Group, Rect, Text } from "react-tela";
+import { Group, Rect, Text, useDimensions } from "react-tela";
+
+function Contents() {
+ const dims = useDimensions();
+ return <>
+
+
+ Hello world!
+
+ >;
+}
export function App() {
return (
-
-
- Hello world!
-
+
);
}
diff --git a/biome.json b/biome.json
index 59493f8..ac7221f 100644
--- a/biome.json
+++ b/biome.json
@@ -1,6 +1,6 @@
{
"linter": {
- "enabled": true
+ "enabled": false
},
"javascript": {
"formatter": {
diff --git a/src/canvas.ts b/src/canvas.ts
index 5c79ed7..160b260 100644
--- a/src/canvas.ts
+++ b/src/canvas.ts
@@ -9,10 +9,7 @@ export class Canvas extends Entity {
constructor(opts: CanvasProps, root: Root) {
super(opts);
- this.subcanvas = new root.Canvas(
- this.calculatedWidth,
- this.calculatedHeight,
- );
+ this.subcanvas = new root.Canvas(this.width, this.height);
}
getContext(...args: Parameters) {
@@ -21,12 +18,6 @@ export class Canvas extends Entity {
render(): void {
super.render();
- this.root.ctx.drawImage(
- this.subcanvas,
- 0,
- 0,
- this.calculatedWidth,
- this.calculatedHeight,
- );
+ this.root.ctx.drawImage(this.subcanvas, 0, 0, this.width, this.height);
}
}
diff --git a/src/entity.ts b/src/entity.ts
index f06a392..95bdc2c 100644
--- a/src/entity.ts
+++ b/src/entity.ts
@@ -1,25 +1,24 @@
import { TelaEventTarget } from './event-target.js';
-import { parsePercent } from './util.js';
import type { Root } from './root.js';
-import type { PercentageString, TelaMouseEvent } from './types.js';
+import type { TelaMouseEvent } from './types.js';
export type EntityProps = {
/**
* The x (horizontal) coordinate of the entity from the top-left corner of the context.
*/
- x?: number | PercentageString;
+ x?: number;
/**
* The y (vertical) coordinate of the entity from the top-left corner of the context.
*/
- y?: number | PercentageString;
+ y?: number;
/**
- * The height of the entity in pixels.
+ * The width of the entity in pixels.
*/
- width?: number | PercentageString;
+ width?: number;
/**
* The height of the entity in pixels.
*/
- height?: number | PercentageString;
+ height?: number;
/**
* The alpha transparency value of the entity. The value `0` is fully transparent. The value `1` is fully opaque.
*
@@ -62,10 +61,10 @@ export type EntityProps = {
};
export class Entity extends TelaEventTarget {
- x: number | PercentageString;
- y: number | PercentageString;
- width: number | PercentageString;
- height: number | PercentageString;
+ x: number;
+ y: number;
+ width: number;
+ height: number;
alpha: number;
rotate: number;
scaleX?: number;
@@ -123,41 +122,19 @@ export class Entity extends TelaEventTarget {
}
get calculatedX() {
- let { x } = this;
- if (typeof x !== 'number') {
- x = this.root.ctx.canvas.width * parsePercent(x);
- }
- return x + this.calculatedWidth / 2;
+ return this.x + this.width / 2;
}
get calculatedY() {
- let { y } = this;
- if (typeof y !== 'number') {
- y = this.root.ctx.canvas.height * parsePercent(y);
- }
- return y + this.calculatedHeight / 2;
- }
-
- get calculatedWidth() {
- if (typeof this.width === 'number') {
- return this.width;
- }
- return this.root.ctx.canvas.width * parsePercent(this.width);
- }
-
- get calculatedHeight() {
- if (typeof this.height === 'number') {
- return this.height;
- }
- return this.root.ctx.canvas.height * parsePercent(this.height);
+ return this.y + this.height / 2;
}
get offsetX() {
- return -this.calculatedWidth / 2;
+ return -this.width / 2;
}
get offsetY() {
- return -this.calculatedHeight / 2;
+ return -this.height / 2;
}
get matrix() {
@@ -191,7 +168,7 @@ export class Entity extends TelaEventTarget {
get path() {
const p = new this.root.Path2D();
- p.rect(0, 0, this.calculatedWidth, this.calculatedHeight);
+ p.rect(0, 0, this.width, this.height);
return p;
}
diff --git a/src/group.ts b/src/group.ts
index 303ab35..b9a923f 100644
--- a/src/group.ts
+++ b/src/group.ts
@@ -3,10 +3,7 @@ import { Entity, EntityProps } from './entity.js';
import { proxyEvents } from './events.js';
import type { ICanvasRenderingContext2D } from './types.js';
-export interface GroupProps extends Omit {
- width: number;
- height: number;
-}
+export interface GroupProps extends EntityProps {}
export class Group extends Entity {
subroot: Root;
@@ -24,8 +21,8 @@ export class Group extends Entity {
this.subroot.ctx.canvas,
0,
0,
- this.calculatedWidth,
- this.calculatedHeight,
+ this.width,
+ this.height,
);
}
}
diff --git a/src/hooks/use-layout.ts b/src/hooks/use-layout.ts
new file mode 100644
index 0000000..eb11794
--- /dev/null
+++ b/src/hooks/use-layout.ts
@@ -0,0 +1,19 @@
+import { createContext, useContext } from 'react';
+
+export interface Layout {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
+
+export const LayoutContext = createContext({
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+});
+
+export function useLayout() {
+ return useContext(LayoutContext);
+}
diff --git a/src/image.ts b/src/image.ts
index f17d48f..a77486e 100644
--- a/src/image.ts
+++ b/src/image.ts
@@ -1,15 +1,13 @@
import { Entity, type EntityProps } from './entity.js';
import type { Root } from './root.js';
-import type { IImage, PercentageString } from './types.js';
+import type { IImage } from './types.js';
-export interface ImageProps extends Omit {
+export interface ImageProps extends EntityProps {
src: string;
sx?: number;
sy?: number;
sw?: number;
sh?: number;
- width?: number | PercentageString;
- height?: number | PercentageString;
}
export class Image extends Entity {
@@ -70,8 +68,8 @@ export class Image extends Entity {
this.sh ?? img.naturalHeight,
0,
0,
- this.calculatedWidth,
- this.calculatedHeight,
+ this.width,
+ this.height,
);
}
}
diff --git a/src/index.tsx b/src/index.tsx
index aaa08e1..be24b04 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -18,13 +18,24 @@ import { Path as _Path, type PathProps } from './path.js';
import { Image as _Image, type ImageProps } from './image.js';
import { Text as _Text, type TextProps as _TextProps } from './text.js';
import { ICanvas } from './types.js';
+import { LayoutContext, useLayout } from './hooks/use-layout.js';
+import { EntityProps } from './entity.js';
type MaybeArray = T | T[];
-const factory = [(type: string) => {
- const c = forwardRef][((props, ref) =>
- createElement(type, { ...props, ref }),
- );
+function useAdjustedLayout(props: any) {
+ let { x, y, width, height } = useLayout();
+ x += props.x ?? 0;
+ y += props.y ?? 0;
+ width += props.width ?? 0;
+ height += props.height ?? 0;
+ return { x, y, width, height };
+}
+
+const factory = ][(type: string) => {
+ const c = forwardRef][((props, ref) => {
+ return createElement(type, { ...props, ...useAdjustedLayout(props), ref });
+ });
c.displayName = type;
return c;
};
@@ -42,7 +53,6 @@ export type { _Canvas as CanvasRef };
export const Arc = factory<_Arc, ArcProps>('Arc');
export const Canvas = factory<_Canvas, CanvasProps>('Canvas');
-//export const Group = factory<_Group, GroupProps>('Group');
export const Image = factory<_Image, ImageProps>('Image');
export const Path = factory<_Path, PathProps>('Path');
export const Rect = factory<_Rect, RectProps>('Rect');
@@ -67,35 +77,40 @@ export const Group = forwardRef<_Group, GroupProps>((props, ref) => {
const root = useParent();
const rootRef = useRef();
let canvas: ICanvas;
+ const layout = useAdjustedLayout(props);
if (rootRef.current) {
canvas = rootRef.current.ctx.canvas;
} else {
- canvas = new root.Canvas(props.width || 300, props.height || 150);
+ canvas = new root.Canvas(layout.width || 300, layout.height || 150);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Could not get "2d" canvas context');
}
rootRef.current = new GroupRoot(ctx, root);
}
- if (props.width > 0 && props.width !== canvas.width) {
- canvas.width = props.width;
+ if (layout.width > 0 && layout.width !== canvas.width) {
+ canvas.width = layout.width;
}
- if (props.height > 0 && props.height !== canvas.height) {
- canvas.height = props.height;
+ if (layout.height > 0 && layout.height !== canvas.height) {
+ canvas.height = layout.height;
}
//console.log({ props })
return (
- {createElement('Group', {
- ...props,
- root: rootRef.current,
- ref,
- })}
+
+ {createElement('Group', {
+ ...props,
+ ...layout,
+ root: rootRef.current,
+ ref,
+ })}
+
);
});
Group.displayName = 'Group';
export { useParent } from './hooks/use-parent.js';
+export { useLayout, LayoutContext, type Layout } from './hooks/use-layout.js';
export { useDimensions } from './hooks/use-dimensions.js';
export { useTextMetrics } from './hooks/use-text-metrics.js';
diff --git a/src/round-rect.ts b/src/round-rect.ts
index b6b05a1..c5e7675 100644
--- a/src/round-rect.ts
+++ b/src/round-rect.ts
@@ -14,7 +14,7 @@ export class RoundRect extends Shape {
get path() {
const p = new this.root.Path2D();
- p.roundRect(0, 0, this.calculatedWidth, this.calculatedHeight, this.radii);
+ p.roundRect(0, 0, this.width, this.height, this.radii);
return p;
}
}
diff --git a/src/test.tsx b/src/test.tsx
index 43bb6fc..696c4b4 100644
--- a/src/test.tsx
+++ b/src/test.tsx
@@ -1,3 +1,4 @@
+// @ts-nocheck
import React, {
useEffect,
useRef,
@@ -64,6 +65,7 @@ import {
useTextMetrics,
TextProps,
useDimensions,
+ LayoutContext,
} from './index.js';
const canvas = document.getElementById('c') as HTMLCanvasElement;
@@ -758,12 +760,13 @@ function CenteredText({ children, ...props }: TextProps) {
function Page1() {
// biome-ignore lint/suspicious/noExplicitAny:
const data = useLoaderData() as any;
+ const dims = useDimensions();
return (
<>
-
+
-
+
hello world
@@ -794,6 +797,18 @@ function Page1() {
);
}
+function Page3() {
+ return (
+
+
+
+ );
+}
+
+function RedRect() {
+ return ;
+}
+
function ErrorBoundary() {
// biome-ignore lint/suspicious/noExplicitAny:
const error = useAsyncError() as any;
@@ -820,7 +835,7 @@ function Async() {
const data = useAsyncValue();
console.log(data);
return (
-
+
Loaded!
);
@@ -835,6 +850,7 @@ const routes: RouteObject[] = [
{
path: '/test',
//element: ,
+ //element: ,
//element: ,
element: ,
//element: ,
diff --git a/src/text.ts b/src/text.ts
index b9dc60e..b23a833 100644
--- a/src/text.ts
+++ b/src/text.ts
@@ -49,7 +49,7 @@ export class Text extends Entity {
}
render(): void {
- let {
+ const {
value,
fontFamily = 'sans-serif',
fontWeight = '',
diff --git a/src/types.ts b/src/types.ts
index d71255f..2622515 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,5 +1,3 @@
-export type PercentageString = `${string}%`;
-
export interface ICanvasRenderingContext2D {
globalAlpha: number;
canvas: ICanvas;
diff --git a/src/util.ts b/src/util.ts
index 9e42615..c91affc 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,10 +1,6 @@
import { Root } from './root.js';
import type { Entity } from './entity.js';
-import type { PercentageString, Point } from './types.js';
-
-export function parsePercent(str: PercentageString) {
- return parseFloat(str.slice(0, -1)) / 100;
-}
+import type { Point } from './types.js';
const MouseEvent = globalThis.MouseEvent || class MouseEvent extends Event {};
diff --git a/test/__image_snapshots__/group-test-tsx-test-group-test-tsx-should-render-group-1-snap.png b/test/__image_snapshots__/group-test-tsx-test-group-test-tsx-should-render-group-1-snap.png
new file mode 100644
index 0000000..2aee8c8
Binary files /dev/null and b/test/__image_snapshots__/group-test-tsx-test-group-test-tsx-should-render-group-1-snap.png differ
diff --git a/test/__image_snapshots__/group-test-tsx-test-group-test-tsx-should-render-group-with-parent-layout-context-1-snap.png b/test/__image_snapshots__/group-test-tsx-test-group-test-tsx-should-render-group-with-parent-layout-context-1-snap.png
new file mode 100644
index 0000000..1d2f32a
Binary files /dev/null and b/test/__image_snapshots__/group-test-tsx-test-group-test-tsx-should-render-group-with-parent-layout-context-1-snap.png differ
diff --git a/test/__image_snapshots__/path-test-tsx-test-path-test-tsx-should-render-path-1-snap.png b/test/__image_snapshots__/path-test-tsx-test-path-test-tsx-should-render-path-1-snap.png
new file mode 100644
index 0000000..1bc97e7
Binary files /dev/null and b/test/__image_snapshots__/path-test-tsx-test-path-test-tsx-should-render-path-1-snap.png differ
diff --git a/test/__image_snapshots__/rect-test-tsx-test-rect-test-tsx-should-render-rect-with-layout-context-1-snap.png b/test/__image_snapshots__/rect-test-tsx-test-rect-test-tsx-should-render-rect-with-layout-context-1-snap.png
new file mode 100644
index 0000000..c33a1c3
Binary files /dev/null and b/test/__image_snapshots__/rect-test-tsx-test-rect-test-tsx-should-render-rect-with-layout-context-1-snap.png differ
diff --git a/test/group.test.tsx b/test/group.test.tsx
new file mode 100644
index 0000000..5373f27
--- /dev/null
+++ b/test/group.test.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { join } from 'path';
+import { test, expect } from 'vitest';
+import config, { Canvas, GlobalFonts } from '@napi-rs/canvas';
+import { Group, Rect, Text, useDimensions, LayoutContext } from '../src';
+import { render } from '../src/render';
+
+GlobalFonts.registerFromPath(
+ join(__dirname, 'Geist-Regular.otf'),
+ 'Geist Sans',
+);
+
+test('should render ', async () => {
+ const canvas = new Canvas(300, 100);
+ let dims: { width: number; height: number };
+ function Inner() {
+ dims = useDimensions();
+ return (
+ <>
+
+
+ Hello world!
+
+ >
+ );
+ }
+ await render(
+
+
+ ,
+ canvas,
+ config,
+ );
+ expect(dims!).toEqual({
+ width: 200,
+ height: 50,
+ });
+ expect(canvas.toBuffer('image/png')).toMatchImageSnapshot();
+});
+
+test('should render with parent layout context', async () => {
+ const canvas = new Canvas(300, 100);
+ let dims: { width: number; height: number };
+ function Inner() {
+ dims = useDimensions();
+ return (
+ <>
+
+
+ Hello world!
+
+ >
+ );
+ }
+ await render(
+
+
+
+
+ ,
+ canvas,
+ config,
+ );
+ expect(dims!).toEqual({
+ width: 200,
+ height: 50,
+ });
+ expect(canvas.toBuffer('image/png')).toMatchImageSnapshot();
+});
diff --git a/test/path.test.tsx b/test/path.test.tsx
new file mode 100644
index 0000000..20ea2d1
--- /dev/null
+++ b/test/path.test.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { test, expect } from 'vitest';
+import config, { Canvas } from '@napi-rs/canvas';
+import { Path } from '../src';
+import { render } from '../src/render';
+
+test('should render ', async () => {
+ const canvas = new Canvas(800, 800);
+ await render(
+ {
+ // setStroke3('black');
+ //}}
+ //onMouseLeave={() => {
+ // setStroke3(undefined);
+ //}}
+ />,
+ canvas,
+ config,
+ );
+ expect(canvas.toBuffer('image/png')).toMatchImageSnapshot();
+});
diff --git a/test/rect.test.tsx b/test/rect.test.tsx
index 67efe09..dd18100 100644
--- a/test/rect.test.tsx
+++ b/test/rect.test.tsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { test, expect } from 'vitest';
import config, { Canvas } from '@napi-rs/canvas';
-import { Rect } from '../src';
+import { LayoutContext, Rect } from '../src';
import { render } from '../src/render';
import { enableEvents, dispatchEvent } from './helpers/event';
@@ -15,6 +15,21 @@ test('should render ', async () => {
expect(canvas.toBuffer('image/png')).toMatchImageSnapshot();
});
+test('should render with layout context', async () => {
+ const canvas = new Canvas(150, 100);
+ function BlueRect() {
+ return ;
+ }
+ await render(
+
+
+ ,
+ canvas,
+ config,
+ );
+ expect(canvas.toBuffer('image/png')).toMatchImageSnapshot();
+});
+
test('should receive "click" event', async () => {
const canvas = new Canvas(150, 100);
enableEvents(canvas);
]