diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 4d86dac..50f6b60 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,19 +4,25 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
@@ -53,43 +59,43 @@
- {
+ "keyToString": {
+ "ASKED_ADD_EXTERNAL_FILES": "true",
+ "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
+ "Playwright.JoinText Component.executor": "Run",
+ "Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "Vitest.compute function (1).executor": "Run",
+ "Vitest.compute function.executor": "Run",
+ "Vitest.mergeText.executor": "Run",
+ "Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
+ "Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
+ "git-widget-placeholder": "main",
+ "ignore.virus.scanning.warn.message": "true",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/Users/HP/IdeaProjects/omni-tools/src/assets",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "npm.dev.executor": "Run",
+ "npm.lint.executor": "Run",
+ "npm.prebuild.executor": "Run",
+ "npm.script:create:tool.executor": "Run",
+ "npm.test.executor": "Run",
+ "npm.test:e2e.executor": "Run",
+ "prettierjs.PrettierConfiguration.Package": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\prettier",
+ "project.structure.last.edited": "Problems",
+ "project.structure.proportion": "0.0",
+ "project.structure.side.proportion": "0.2",
+ "settings.editor.selected.configurable": "settings.typescriptcompiler",
+ "ts.external.directory.path": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
-
+
@@ -150,11 +156,11 @@
-
+
-
+
@@ -172,11 +178,11 @@
+
+
-
-
@@ -207,23 +213,9 @@
-
-
-
-
- 1719007195103
-
-
-
- 1719007195103
-
-
-
- 1719023377131
-
-
-
- 1719023377131
+
+
+
@@ -601,7 +593,23 @@
1719384439535
-
+
+
+ 1719388760134
+
+
+
+ 1719388760134
+
+
+
+ 1719388927238
+
+
+
+ 1719388927238
+
+
@@ -622,8 +630,6 @@
-
-
@@ -647,7 +653,9 @@
-
+
+
+
diff --git a/package-lock.json b/package-lock.json
index c5b9264..e412e18 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,13 +13,20 @@
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@playwright/test": "^1.45.0",
+ "@types/gif-encoder": "^0.7.4",
"@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2",
+ "@types/omggif": "^1.0.5",
"color": "^4.2.3",
"formik": "^2.4.6",
+ "gif-encoder": "^0.7.2",
+ "gif-encoder-2": "^1.0.5",
+ "gif-encoder-2-browser": "^1.0.5",
+ "gifuct-js": "^2.1.2",
"lodash": "^4.17.21",
"morsee": "^1.0.9",
"notistack": "^3.0.1",
+ "omggif": "^1.0.10",
"playwright": "^1.45.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -2511,6 +2518,14 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
+ "node_modules/@types/gif-encoder": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@types/gif-encoder/-/gif-encoder-0.7.4.tgz",
+ "integrity": "sha512-tke9j2sFpRpX2C1JLAxZpTMAzVILlWkkhuSGCbxWuyBvXYtA+og7KpxL5ag+4dbDYJxZ2zVmBH0taxwihgz0ZQ==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
@@ -2540,11 +2555,15 @@
"version": "20.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz",
"integrity": "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==",
- "dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
+ "node_modules/@types/omggif": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/omggif/-/omggif-1.0.5.tgz",
+ "integrity": "sha512-gDQJflz1rOgEcUXkMAl80bDGN46f5mp8GbcM5dyvq+zsFV6YRBRtmNxlJJ5mjY77T7BRkRFzdIBVmK90QYhCxA=="
+ },
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@@ -3779,6 +3798,11 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+ },
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -5116,6 +5140,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gif-encoder": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/gif-encoder/-/gif-encoder-0.7.2.tgz",
+ "integrity": "sha512-rEe2DJCb8quqOElV5orqRjyk2KNjz+Hdy+eWYVNWn7s1/33QQ6boIJHkgOto5qU2NrGxI2coPDRqcEaGFZkQ1w==",
+ "dependencies": {
+ "readable-stream": "~1.1.9"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/gif-encoder-2": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/gif-encoder-2/-/gif-encoder-2-1.0.5.tgz",
+ "integrity": "sha512-fsRAKbZuUoZ7FYGjpFElmflTkKwsn/CzAmL/xDl4558aTAgysIDCUF6AXWO8dmai/ApfZACbPVAM+vPezJXlFg=="
+ },
+ "node_modules/gif-encoder-2-browser": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/gif-encoder-2-browser/-/gif-encoder-2-browser-1.0.5.tgz",
+ "integrity": "sha512-YFIFc3yjgoBFnClWAYFq0FmSSdrk1Q4AFVp+2/CN7mRzh+fDHC6q/jUBzWY9SpGUP8fICzgW1Q2iZtHbdONabA=="
+ },
+ "node_modules/gifuct-js": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz",
+ "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==",
+ "dependencies": {
+ "js-binary-schema-parser": "^2.0.3"
+ }
+ },
"node_modules/git-raw-commits": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz",
@@ -5495,8 +5548,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "4.1.1",
@@ -6006,6 +6058,11 @@
"@sideway/pinpoint": "^2.0.0"
}
},
+ "node_modules/js-binary-schema-parser": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz",
+ "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg=="
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6686,6 +6743,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/omggif": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
+ "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -7380,6 +7442,22 @@
"pify": "^2.3.0"
}
},
+ "node_modules/readable-stream": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "node_modules/readable-stream/node_modules/isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -7973,6 +8051,11 @@
"duplexer": "~0.1.1"
}
},
+ "node_modules/string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
+ },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -8649,8 +8732,7 @@
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
- "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
- "dev": true
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/unicorn-magic": {
"version": "0.1.0",
diff --git a/package.json b/package.json
index 4d77d2d..6f46194 100644
--- a/package.json
+++ b/package.json
@@ -31,13 +31,20 @@
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@playwright/test": "^1.45.0",
+ "@types/gif-encoder": "^0.7.4",
"@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2",
+ "@types/omggif": "^1.0.5",
"color": "^4.2.3",
"formik": "^2.4.6",
+ "gif-encoder": "^0.7.2",
+ "gif-encoder-2": "^1.0.5",
+ "gif-encoder-2-browser": "^1.0.5",
+ "gifuct-js": "^2.1.2",
"lodash": "^4.17.21",
"morsee": "^1.0.9",
"notistack": "^3.0.1",
+ "omggif": "^1.0.10",
"playwright": "^1.45.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index 4901e0a..4fe6dc1 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -14,7 +14,7 @@ const exampleTools: { label: string; url: string }[] = [
url: '/png/create-transparent'
},
{ label: 'Convert text to morse code', url: '/string/to-morse' },
- { label: 'Change GIF speed', url: '' },
+ { label: 'Change GIF speed', url: '/gif/change-speed' },
{ label: 'Pick a random item', url: '' },
{ label: 'Find and replace text', url: '' },
{ label: 'Convert emoji to image', url: '' },
diff --git a/src/tools/Separator.tsx b/src/components/Separator.tsx
similarity index 100%
rename from src/tools/Separator.tsx
rename to src/components/Separator.tsx
diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx
index 5dd49a3..4737ea1 100644
--- a/src/components/ToolLayout.tsx
+++ b/src/components/ToolLayout.tsx
@@ -2,7 +2,7 @@ import { Box } from '@mui/material';
import React, { ReactNode } from 'react';
import { Helmet } from 'react-helmet';
import ToolHeader from './ToolHeader';
-import Separator from '@tools/Separator';
+import Separator from './Separator';
import AllTools from './allTools/AllTools';
import { getToolsByCategory } from '@tools/index';
import { capitalizeFirstLetter } from '../utils/string';
diff --git a/src/components/options/RadioWithTextField.tsx b/src/components/options/RadioWithTextField.tsx
index 4f08ecc..12e3603 100644
--- a/src/components/options/RadioWithTextField.tsx
+++ b/src/components/options/RadioWithTextField.tsx
@@ -34,7 +34,10 @@ const RadioWithTextField = ({
/>
{
+ if (typeof val === 'string') onTextChange(val);
+ else onTextChange(val.target.value);
+ }}
description={description}
/>
diff --git a/src/components/options/TextFieldWithDesc.tsx b/src/components/options/TextFieldWithDesc.tsx
index 7d184ea..dad5ba9 100644
--- a/src/components/options/TextFieldWithDesc.tsx
+++ b/src/components/options/TextFieldWithDesc.tsx
@@ -1,18 +1,20 @@
-import { Box, TextField } from '@mui/material';
+import { Box, TextField, TextFieldProps } from '@mui/material';
import Typography from '@mui/material/Typography';
import React from 'react';
+type OwnProps = {
+ description: string;
+ value: string | number;
+ onChange: (value: string) => void;
+ placeholder?: string;
+};
const TextFieldWithDesc = ({
description,
value,
onChange,
- placeholder
-}: {
- description: string;
- value: string;
- onChange: (value: string) => void;
- placeholder?: string;
-}) => {
+ placeholder,
+ ...props
+}: TextFieldProps & OwnProps) => {
return (
onChange(event.target.value)}
+ {...props}
/>
{description}
diff --git a/src/components/options/ToolOptions.tsx b/src/components/options/ToolOptions.tsx
index 92b0260..351ccec 100644
--- a/src/components/options/ToolOptions.tsx
+++ b/src/components/options/ToolOptions.tsx
@@ -6,6 +6,28 @@ import { Formik, FormikProps, FormikValues, useFormikContext } from 'formik';
import ToolOptionGroups, { ToolOptionGroup } from './ToolOptionGroups';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
+const FormikListenerComponent = ({
+ initialValues,
+ input,
+ compute
+}: {
+ initialValues: T;
+ input: any;
+ compute: (optionsValues: T, input: any) => void;
+}) => {
+ const { values } = useFormikContext();
+ const { showSnackBar } = useContext(CustomSnackBarContext);
+
+ useEffect(() => {
+ try {
+ if (values && input) compute(values, input);
+ } catch (exception: unknown) {
+ if (exception instanceof Error) showSnackBar(exception.message, 'error');
+ }
+ }, [values, input]);
+
+ return null; // This component doesn't render anything
+};
export default function ToolOptions({
children,
initialValues,
@@ -24,21 +46,7 @@ export default function ToolOptions({
formRef?: RefObject>;
}) {
const theme = useTheme();
- const FormikListenerComponent = () => {
- const { values } = useFormikContext();
- const { showSnackBar } = useContext(CustomSnackBarContext);
- useEffect(() => {
- try {
- compute(values, input);
- } catch (exception: unknown) {
- if (exception instanceof Error)
- showSnackBar(exception.message, 'error');
- }
- }, [values, showSnackBar]);
-
- return null; // This component doesn't render anything
- };
return (
({
>
{(formikProps) => (
-
+
{children}
diff --git a/src/pages/string/join/index.tsx b/src/pages/string/join/index.tsx
index 8fc59b2..7369976 100644
--- a/src/pages/string/join/index.tsx
+++ b/src/pages/string/join/index.tsx
@@ -10,7 +10,7 @@ import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
import ToolInfo from '../../../components/ToolInfo';
-import Separator from '../../../tools/Separator';
+import Separator from '../../../components/Separator';
import Examples from '../../../components/examples/Examples';
const initialValues = {
diff --git a/src/pages/video/gif/change-speed/change-speed.service.test.ts b/src/pages/video/gif/change-speed/change-speed.service.test.ts
new file mode 100644
index 0000000..967fc9b
--- /dev/null
+++ b/src/pages/video/gif/change-speed/change-speed.service.test.ts
@@ -0,0 +1,6 @@
+import { expect, describe, it } from 'vitest';
+// import { } from './service';
+//
+// describe('change-speed', () => {
+//
+// })
\ No newline at end of file
diff --git a/src/pages/video/gif/change-speed/index.tsx b/src/pages/video/gif/change-speed/index.tsx
new file mode 100644
index 0000000..b4b8799
--- /dev/null
+++ b/src/pages/video/gif/change-speed/index.tsx
@@ -0,0 +1,150 @@
+import { Box } from '@mui/material';
+import React, { useState } from 'react';
+import * as Yup from 'yup';
+import ToolFileInput from '../../../../components/input/ToolFileInput';
+import ToolFileResult from '../../../../components/result/ToolFileResult';
+import ToolOptions from '../../../../components/options/ToolOptions';
+import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
+import ToolInputAndResult from '../../../../components/ToolInputAndResult';
+import Typography from '@mui/material/Typography';
+import { FrameOptions, GifReader, GifWriter } from 'omggif';
+import { gifBinaryToFile } from '../../../../utils/gif';
+
+const initialValues = {
+ newSpeed: 200
+};
+const validationSchema = Yup.object({
+ // splitSeparator: Yup.string().required('The separator is required')
+});
+export default function ChangeSpeed() {
+ const [input, setInput] = useState(null);
+ const [result, setResult] = useState(null);
+
+ const compute = (optionsValues: typeof initialValues, input: File) => {
+ const { newSpeed } = optionsValues;
+
+ const processImage = async (file: File, newSpeed: number) => {
+ const reader = new FileReader();
+ reader.readAsArrayBuffer(file);
+
+ reader.onload = async () => {
+ const arrayBuffer = reader.result;
+
+ if (arrayBuffer instanceof ArrayBuffer) {
+ const intArray = new Uint8Array(arrayBuffer);
+
+ const reader = new GifReader(intArray as Buffer);
+ const info = reader.frameInfo(0);
+ const imageDataArr: ImageData[] = new Array(reader.numFrames())
+ .fill(0)
+ .map((_, k) => {
+ const image = new ImageData(info.width, info.height);
+
+ reader.decodeAndBlitFrameRGBA(k, image.data as any);
+
+ return image;
+ });
+ const gif = new GifWriter(
+ [],
+ imageDataArr[0].width,
+ imageDataArr[0].height,
+ { loop: 20 }
+ );
+
+ // Decode the GIF
+ imageDataArr.forEach((imageData) => {
+ const palette = [];
+ const pixels = new Uint8Array(imageData.width * imageData.height);
+
+ const { data } = imageData;
+ for (let j = 0, k = 0, jl = data.length; j < jl; j += 4, k++) {
+ const r = Math.floor(data[j] * 0.1) * 10;
+ const g = Math.floor(data[j + 1] * 0.1) * 10;
+ const b = Math.floor(data[j + 2] * 0.1) * 10;
+ const color = (r << 16) | (g << 8) | (b << 0);
+
+ const index = palette.indexOf(color);
+
+ if (index === -1) {
+ pixels[k] = palette.length;
+ palette.push(color);
+ } else {
+ pixels[k] = index;
+ }
+ }
+
+ // Force palette to be power of 2
+
+ let powof2 = 1;
+ while (powof2 < palette.length) powof2 <<= 1;
+ palette.length = powof2;
+
+ const delay = newSpeed / 10; // Delay in hundredths of a sec (100 = 1s)
+ const options: FrameOptions = {
+ // @ts-ignore
+ palette: new Uint32Array(palette),
+ delay: delay
+ };
+ gif.addFrame(
+ 0,
+ 0,
+ imageData.width,
+ imageData.height,
+ // @ts-ignore
+ pixels,
+ options
+ );
+ });
+ const newFile = gifBinaryToFile(gif.getOutputBuffer(), file.name);
+
+ setResult(newFile);
+ }
+ };
+ };
+
+ processImage(input, newSpeed);
+ };
+ return (
+
+
+ }
+ result={
+
+ }
+ />
+ [
+ {
+ title: 'New GIF speed',
+ component: (
+
+ setFieldValue('newSpeed', val)}
+ description={'Default new GIF speed.'}
+ InputProps={{ endAdornment: ms }}
+ type={'number'}
+ />
+
+ )
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ validationSchema={validationSchema}
+ />
+
+ );
+}
diff --git a/src/pages/video/gif/change-speed/meta.ts b/src/pages/video/gif/change-speed/meta.ts
new file mode 100644
index 0000000..65d55e7
--- /dev/null
+++ b/src/pages/video/gif/change-speed/meta.ts
@@ -0,0 +1,14 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+// import image from '@assets/text.png';
+
+export const tool = defineTool('gif', {
+ name: 'Change speed',
+ path: 'change-speed',
+ // image,
+ description:
+ 'This online utility lets you change the speed of a GIF animation. You can speed it up or slow it down. You can set the same constant delay between all frames or change the delays of individual frames. You can also play both the input and output GIFs at the same time and compare their speeds',
+ shortDescription: '',
+ keywords: ['change', 'speed'],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/video/gif/change-speed/service.ts b/src/pages/video/gif/change-speed/service.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/pages/video/gif/index.ts b/src/pages/video/gif/index.ts
new file mode 100644
index 0000000..92e5375
--- /dev/null
+++ b/src/pages/video/gif/index.ts
@@ -0,0 +1,3 @@
+import { tool as gifChangeSpeed } from './change-speed/meta';
+
+export const gifTools = [gifChangeSpeed];
diff --git a/src/pages/video/index.ts b/src/pages/video/index.ts
new file mode 100644
index 0000000..db8a652
--- /dev/null
+++ b/src/pages/video/index.ts
@@ -0,0 +1,3 @@
+import { gifTools } from './gif';
+
+export const videoTools = [...gifTools];
diff --git a/src/tools/index.ts b/src/tools/index.ts
index e0f645a..9b2c682 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -3,11 +3,13 @@ import { imageTools } from '../pages/image';
import { DefinedTool } from './defineTool';
import { capitalizeFirstLetter } from '../utils/string';
import { numberTools } from '../pages/number';
+import { videoTools } from '../pages/video';
export const tools: DefinedTool[] = [
...imageTools,
...stringTools,
- ...numberTools
+ ...numberTools,
+ ...videoTools
];
const categoriesDescriptions: { type: string; value: string }[] = [
{
@@ -24,6 +26,11 @@ const categoriesDescriptions: { type: string; value: string }[] = [
type: 'number',
value:
'Tools for working with numbers – generate number sequences, convert numbers to words and words to numbers, sort, round, factor numbers, and much more.'
+ },
+ {
+ type: 'gif',
+ value:
+ 'Tools for working with GIF animations – create transparent GIFs, extract GIF frames, add text to GIF, crop, rotate, reverse GIFs, and much more.'
}
];
export const filterTools = (
diff --git a/src/utils/gif.ts b/src/utils/gif.ts
new file mode 100644
index 0000000..e63f0d2
--- /dev/null
+++ b/src/utils/gif.ts
@@ -0,0 +1,18 @@
+import { GifBinary } from 'omggif';
+
+export function gifBinaryToFile(
+ gifBinary: GifBinary,
+ fileName: string,
+ mimeType: string = 'image/gif'
+): File {
+ // Convert GifBinary to Uint8Array
+ const uint8Array = new Uint8Array(gifBinary.length);
+ for (let i = 0; i < gifBinary.length; i++) {
+ uint8Array[i] = gifBinary[i];
+ }
+
+ const blob = new Blob([uint8Array], { type: mimeType });
+
+ // Create File from Blob
+ return new File([blob], fileName, { type: mimeType });
+}