Skip to content

Commit 0140b45

Browse files
authored
feat: add initial rendering implementation (#11)
* chore: begin work on Toyer contextx * chore: add ToyerVideo * feat: make the rendering sort of work * chore: lint and break files apart * chore: more correct render cycle * feat: support framer-motion motionValue as input * Create quick-masks-clean.md * chore: update changeset to minor * chore: switch to const
1 parent 76da033 commit 0140b45

14 files changed

+519
-25
lines changed

Diff for: .changeset/quick-masks-clean.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"react-toyer": minor
3+
---
4+
5+
#### Api
6+
7+
feat: create initial api for rendering several vidos on one canvas
8+
9+
feat: FramerMotion's motionValue support for canvas internals (not canvas itself)
10+
11+
#### Components
12+
13+
* Toyer (new)
14+
* ToyerVideo (new)

Diff for: docs/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
react-toyer / [Exports](modules.md)
2+
3+
# React Toyer

Diff for: docs/modules.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[react-toyer](README.md) / Exports
2+
3+
# react-toyer
4+
5+
## Table of contents
6+
7+
### Namespaces
8+
9+
- [Toyer](modules/toyer.md)
10+
- [ToyerVideo](modules/toyervideo.md)
11+
12+
### Variables
13+
14+
- [Toyer](modules.md#toyer)
15+
- [ToyerVideo](modules.md#toyervideo)
16+
17+
## Variables
18+
19+
### Toyer
20+
21+
`Const` **Toyer**: `FC`<`IToyerProps`\>
22+
23+
___
24+
25+
### ToyerVideo
26+
27+
`Const` **ToyerVideo**: `VFC`<`IToyerVideoProps`\>

Diff for: docs/modules/toyer.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[react-toyer](../README.md) / [Exports](../modules.md) / Toyer
2+
3+
# Namespace: Toyer
4+
5+
## Table of contents
6+
7+
### Variables
8+
9+
- [displayName](toyer.md#displayname)
10+
11+
## Variables
12+
13+
### displayName
14+
15+
**displayName**: `undefined` \| `string`

Diff for: docs/modules/toyervideo.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[react-toyer](../README.md) / [Exports](../modules.md) / ToyerVideo
2+
3+
# Namespace: ToyerVideo
4+
5+
## Table of contents
6+
7+
### Variables
8+
9+
- [displayName](toyervideo.md#displayname)
10+
11+
## Variables
12+
13+
### displayName
14+
15+
**displayName**: `undefined` \| `string`

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@testing-library/react": "12.0.0",
4242
"@types/jest": "26.0.24",
4343
"@types/react": "17.0.14",
44+
"canvas": "2.8.0",
4445
"eslint-config-altnext": "1.1.6",
4546
"husky": "7.0.1",
4647
"jest": "27.0.6",

Diff for: src/__tests__/toyer.spec.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { render } from '@testing-library/react';
22

33
import { Toyer } from '../toyer';
4+
import { ToyerVideo } from '../toyer-video';
45

56
describe('init', () => {
67
it('should work', () => {
7-
const { container } = render(<Toyer />);
8+
const { container } = render(
9+
<Toyer height={120} width={300}>
10+
<ToyerVideo src={'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'} playing />
11+
</Toyer>,
12+
);
813

914
expect(container).toBeDefined();
1015
});

Diff for: src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { Toyer } from './toyer';
2+
export { ToyerVideo } from './toyer-video';

Diff for: src/interfaces.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,32 @@
1-
export interface IToyerProps {}
1+
export type ValueType<T> = T | { get(): T };
2+
3+
interface ISized {
4+
width: ValueType<number>;
5+
height: ValueType<number>;
6+
}
7+
8+
interface IMovable {
9+
top?: ValueType<number>;
10+
left?: ValueType<number>;
11+
}
12+
13+
export interface IVideoItem extends IMovable, ISized {
14+
index: number;
15+
playing: boolean;
16+
element: HTMLVideoElement;
17+
}
18+
19+
export interface IToyerContext {
20+
latestIndex: number;
21+
canvas: ISized;
22+
videos: IVideoItem[];
23+
}
24+
25+
export interface IToyerProps {
26+
width: number;
27+
height: number;
28+
}
29+
30+
export interface IToyerVideoProps extends Partial<ISized & Omit<IVideoItem, 'element'>> {
31+
src: string;
32+
}

Diff for: src/toyer-video.tsx

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { VFC } from 'react';
2+
import { useContext, useEffect, useRef } from 'react';
3+
4+
import { ToyerContext } from './toyer.context';
5+
import { getIndex, registerVideo } from './toyer.helpers';
6+
import type { IToyerVideoProps } from './interfaces';
7+
8+
export const ToyerVideo: VFC<IToyerVideoProps> = ({ index, src, playing, width, height, top, left }) => {
9+
const context = useContext(ToyerContext);
10+
const videoRef = useRef<HTMLVideoElement>();
11+
const currentIndex = getIndex(context, index);
12+
13+
useEffect(() => {
14+
const video = document.createElement('video');
15+
16+
video.muted = true;
17+
video.src = src;
18+
19+
videoRef.current = video;
20+
21+
return () => {
22+
video.remove();
23+
};
24+
}, [src]);
25+
26+
useEffect(
27+
() =>
28+
registerVideo(context, {
29+
index: currentIndex,
30+
playing: !!playing,
31+
element: videoRef.current!,
32+
top,
33+
left,
34+
width: width ?? context.canvas.width,
35+
height: height ?? context.canvas.height,
36+
}),
37+
/* Its intentinal to keep out playing from deps here */
38+
/* eslint-disable-next-line react-hooks/exhaustive-deps */
39+
[context, currentIndex, width, height, top, left],
40+
);
41+
42+
return null;
43+
};
44+
45+
ToyerVideo.displayName = 'ToyerVideo';

Diff for: src/toyer.context.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createContext } from 'react';
2+
3+
import type { IToyerContext } from './interfaces';
4+
5+
export const ToyerContext = createContext<IToyerContext>({
6+
latestIndex: 0,
7+
videos: [],
8+
canvas: { height: 0, width: 0 },
9+
});

Diff for: src/toyer.helpers.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { IToyerContext, IVideoItem, ValueType } from './interfaces';
2+
3+
export const registerVideo = (context: IToyerContext, video: IVideoItem): (() => void) => {
4+
if (video.element.readyState >= video.element.HAVE_ENOUGH_DATA) {
5+
context.videos[video.index] = video;
6+
} else {
7+
video.element.load();
8+
video.element.addEventListener('loadedmetadata', () => {
9+
context.videos[video.index] = video;
10+
});
11+
}
12+
13+
return () => {
14+
if (context.videos[video.index]) {
15+
delete context.videos[video.index];
16+
}
17+
};
18+
};
19+
20+
export const getIndex = (context: IToyerContext, index?: number): number => index ?? (context.latestIndex += 1);
21+
22+
export const getValue = <T extends unknown>(value: ValueType<T>): T => {
23+
if (typeof value === 'object' && value !== null && 'get' in (value as object)) {
24+
return (value as { get(): T }).get();
25+
}
26+
27+
return value as T;
28+
};

Diff for: src/toyer.tsx

+72-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,74 @@
1-
import type { VFC } from 'react';
1+
import type { FC } from 'react';
2+
import { useEffect, useRef } from 'react';
23

3-
import type { IToyerProps } from './interfaces';
4+
import { ToyerContext } from './toyer.context';
5+
import { getValue } from './toyer.helpers';
6+
import type { IToyerContext, IToyerProps } from './interfaces';
47

5-
export const Toyer: VFC<IToyerProps> = () => <div>Toyer</div>;
8+
export const Toyer: FC<IToyerProps> = ({ children, height, width }) => {
9+
const canvasRef = useRef<HTMLCanvasElement>(null);
10+
const contextRef = useRef<IToyerContext>({ canvas: { height, width }, latestIndex: 0, videos: [] });
11+
12+
useEffect(() => {
13+
let running = true;
14+
let ctx = canvasRef.current?.getContext('2d');
15+
16+
const tick = (): void => {
17+
if (!running) {
18+
return;
19+
}
20+
21+
if (!ctx) {
22+
ctx = canvasRef.current?.getContext('2d');
23+
24+
requestAnimationFrame(tick);
25+
26+
return;
27+
}
28+
29+
ctx.clearRect(0, 0, getValue(contextRef.current.canvas.width), getValue(contextRef.current.canvas.height));
30+
31+
for (const video of Object.values(contextRef.current.videos)) {
32+
if (video.playing && video.element.paused) {
33+
video.element.play().catch((error) => {
34+
console.error('Error trying to initiate playback', error);
35+
});
36+
}
37+
38+
if (!video.playing && !video.element.paused) {
39+
video.element.pause();
40+
video.element.currentTime = 0;
41+
}
42+
43+
if (video.playing) {
44+
ctx.drawImage(
45+
video.element,
46+
getValue(video.left ?? 0),
47+
getValue(video.top ?? 0),
48+
getValue(video.width),
49+
getValue(video.height),
50+
);
51+
}
52+
}
53+
54+
requestAnimationFrame(tick);
55+
};
56+
57+
requestAnimationFrame(tick);
58+
59+
return () => {
60+
running = false;
61+
};
62+
}, []);
63+
64+
contextRef.current.latestIndex = 0;
65+
66+
return (
67+
<div style={{ height, width }}>
68+
<canvas ref={canvasRef} height={height} width={width} />
69+
<ToyerContext.Provider value={contextRef.current}>{children}</ToyerContext.Provider>
70+
</div>
71+
);
72+
};
73+
74+
Toyer.displayName = 'Toyer';

0 commit comments

Comments
 (0)