Skip to content

Commit

Permalink
edit functions. add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
eternalvision committed Jul 19, 2024
1 parent 285b5c4 commit 3d3206c
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 185 deletions.
24 changes: 13 additions & 11 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
settings: { react: { version: "18.2" } },
plugins: ["react-refresh"],
rules: {
'react-refresh/only-export-components': [
'warn',
"react/prop-types": "off",
"react/jsx-no-target-blank": "off",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
}
};
76 changes: 71 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,74 @@
# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
# Video to ASCII Converter

Currently, two official plugins are available:
## Overview

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
This project is a web application that captures video from the camera and converts it into ASCII art in real-time. The application is built using React and uses context for processing video frames.

## Project Structure

### main.jsx

The main entry point of the application. Here, the application is rendered into the DOM using React's `StrictMode`. This file also includes the styles and the context for video frame processing.

### App.jsx

The `App` component is responsible for connecting the `AsciiArt` and `VideoStream` components. It receives the `ProcessVideoFrameContext` through props and uses it to pass necessary functions and refs to the child components.

### components/index.js

The export file for components. It exports `AsciiArt` and `VideoStream` components for use in other parts of the application.

### AsciiArt/AsciiArt.jsx

The `AsciiArt` component is responsible for displaying the ASCII art. It contains a hidden canvas for processing video frames and a `<pre>` element for rendering the ASCII art.

### VideoStream/VideoStream.jsx

The `VideoStream` component captures video from the camera using the `getUserMedia` API. The video is displayed in a mirrored format, and each frame is passed to the `processVideoFrame` function for ASCII conversion.

### context/ProcessVideoFrameProvider.jsx

The context component that provides the `processVideoFrame` function along with refs for the canvas and ASCII art output. The `processVideoFrame` function processes video frames, converts them to grayscale, and then to ASCII characters.

### styles/index.min.css

The stylesheet for the application, defining the basic styles.

## Usage

1. Ensure that you have Node.js installed.
2. Clone the repository.
3. Install the dependencies using `npm install`.
4. Start the application using `npm start`.

## Components and Contexts

### AsciiArt

Component for displaying ASCII art.

- **props**:
- `canvasRef`: a reference to the hidden canvas element.
- `outputRef`: a reference to the `<pre>` element for rendering ASCII art.

### VideoStream

Component for capturing video from the camera.

- **props**:
- `processVideoFrame`: function for processing each video frame.

### ProcessVideoFrameProvider

Context provider that supplies functions and refs for video frame processing.

- **children**: child components that will be provided with the context.

### Key Parameters

- **originalWidth**: original width of the video.
- **originalHeight**: original height of the video.
- **width**: width of the canvas for video processing (half of the original width).
- **height**: height of the canvas for video processing (half of the original height).
- **asciiCharacters**: string of ASCII characters used for image conversion.
20 changes: 10 additions & 10 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Camera ASCII Transducer</title>
</head>
<body>
<!-- Created by Alexandr Priadchenko: https://github.com/eternalvision -->
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Camera ASCII Transducer</title>
<script async defer type="module" src="./src/main.jsx"></script>
</head>
<body>
<!-- Created by https://github.com/eternalvision -->
<div id="root"></div>
</body>
</html>
20 changes: 10 additions & 10 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { useContext } from "react";
const { AsciiArt, VideoStream } = Components;

export const App = (props) => {
const { ProcessVideoFrameContext } = props;
const { processVideoFrame, canvasRef, outputRef } = useContext(
ProcessVideoFrameContext,
);
const { ProcessVideoFrameContext } = props;
const { processVideoFrame, canvasRef, outputRef } = useContext(
ProcessVideoFrameContext,
);

return (
<>
<AsciiArt canvasRef={canvasRef} outputRef={outputRef} />
<VideoStream processVideoFrame={processVideoFrame} />
</>
);
return (
<>
<AsciiArt canvasRef={canvasRef} outputRef={outputRef} />
<VideoStream processVideoFrame={processVideoFrame} />
</>
);
};
23 changes: 9 additions & 14 deletions src/components/AsciiArt/AsciiArt.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import PropTypes from "prop-types";

export const AsciiArt = (props) => {
const { outputRef, canvasRef } = props;

return (
<>
<canvas ref={canvasRef} style={{ display: "none" }}></canvas>
<pre ref={outputRef}></pre>
</>
);
};
const { outputRef, canvasRef } = props;

AsciiArt.propTypes = {
canvasRef: PropTypes.object.isRequired,
outputRef: PropTypes.object.isRequired,
return (
<>
<canvas ref={canvasRef} style={{ display: "none" }}></canvas>
<div className="PreFather">
<pre ref={outputRef}></pre>
</div>
</>
);
};
62 changes: 27 additions & 35 deletions src/components/VideoStream/VideoStream.jsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
import { useEffect, useRef } from "react";
import PropTypes from "prop-types";

export const VideoStream = ({ processVideoFrame }) => {
const videoRef = useRef(null);
const videoRef = useRef(null);

useEffect(() => {
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({ video: true })
.then((stream) => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.style.transform = "scaleX(-1)";
videoRef.current
.play()
.then(() => {
requestAnimationFrame(() =>
processVideoFrame(videoRef.current),
);
})
.catch((err) => {
console.error(
"Ошибка воспроизведения видео: ",
err,
);
});
}
})
.catch((err) => {
console.error("Ошибка доступа к камере: ", err);
});
}
}, [processVideoFrame]);
useEffect(() => {
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({ video: true })
.then((stream) => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.style.transform = "scaleX(-1)";
videoRef.current
.play()
.then(() => {
requestAnimationFrame(() =>
processVideoFrame(videoRef.current),
);
})
.catch((err) => {
console.error(err);
});
}
})
.catch((err) => {
console.error(err);
});
}
}, [processVideoFrame]);

return <video ref={videoRef}></video>;
};

VideoStream.propTypes = {
processVideoFrame: PropTypes.func.isRequired,
return <video ref={videoRef}></video>;
};
116 changes: 53 additions & 63 deletions src/context/ProcessVideoFrameContext.jsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,63 @@
import { createContext, useCallback, useRef } from "react";
import PropTypes from "prop-types";

export const ProcessVideoFrameContext = createContext();

const originalWidth = 1160;
const originalHeight = 400;
const originalWidth = 1650;
const originalHeight = 450;
const width = originalWidth / 2;
const height = originalHeight / 2;
const asciiCharacters =
"@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,\"^`'. ";
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

export const ProcessVideoFrameProvider = ({ children }) => {
const canvasRef = useRef(null);
const outputRef = useRef(null);

const processVideoFrame = useCallback(
(videoElement) => {
const canvas = canvasRef.current;
const output = outputRef.current;
if (canvas && output && videoElement) {
canvas.width = width;
canvas.height = height;

const ctx = canvas.getContext("2d", {
willReadFrequently: true,
});
ctx.translate(width, 0);
ctx.scale(-1, 1);
ctx.drawImage(videoElement, 0, 0, width, height);

const imageData = ctx.getImageData(0, 0, width, height);
let asciiImage = "";
const charLength = asciiCharacters.length;

for (let i = 0; i < imageData.data.length; i += 4) {
const gray =
(imageData.data[i] +
imageData.data[i + 1] +
imageData.data[i + 2]) /
3;
const index = Math.floor((gray * charLength) / 255);
asciiImage += asciiCharacters.charAt(index);

if (i % (width * 4) === 0) {
asciiImage += "\n";
}
}

output.textContent = asciiImage;
}
setTimeout(
() =>
requestAnimationFrame(() =>
processVideoFrame(videoElement),
),
60,
);
},
[canvasRef, outputRef],
);

return (
<ProcessVideoFrameContext.Provider
value={{ processVideoFrame, canvasRef, outputRef }}>
{children}
</ProcessVideoFrameContext.Provider>
);
};

ProcessVideoFrameProvider.propTypes = {
children: PropTypes.any.isRequired,
const canvasRef = useRef(null);
const outputRef = useRef(null);

const processVideoFrame = useCallback(
(videoElement) => {
const canvas = canvasRef.current;
const output = outputRef.current;
if (canvas && output && videoElement) {
canvas.width = width;
canvas.height = height;

const ctx = canvas.getContext("2d", {
willReadFrequently: true,
});
ctx.translate(width, 0);
ctx.scale(-1, 1);
ctx.drawImage(videoElement, 0, 0, width, height);

const imageData = ctx.getImageData(0, 0, width, height);
let asciiImage = "";
const charLength = asciiCharacters.length;

for (let i = 0; i < imageData.data.length; i += 4) {
const gray =
(imageData.data[i] +
imageData.data[i + 1] +
imageData.data[i + 2]) /
3;
const index = Math.floor((gray * charLength) / 255);
asciiImage += asciiCharacters.charAt(index);

if (i % (width * 4) === 0) asciiImage += "\n";
}

output.textContent = asciiImage;
}
setTimeout(
() => requestAnimationFrame(() => processVideoFrame(videoElement)),
60,
);
},
[canvasRef, outputRef],
);

return (
<ProcessVideoFrameContext.Provider
value={{ processVideoFrame, canvasRef, outputRef }}>
{children}
</ProcessVideoFrameContext.Provider>
);
};
1 change: 0 additions & 1 deletion src/css/index.min.css

This file was deleted.

Loading

0 comments on commit 3d3206c

Please sign in to comment.