feat: change gif speed

This commit is contained in:
Ibrahima G. Coulibaly 2024-06-27 12:39:38 +01:00
commit f4e1c06270
18 changed files with 420 additions and 104 deletions

View file

@ -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: '' },

View file

@ -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';

View file

@ -34,7 +34,10 @@ const RadioWithTextField = <T,>({
/>
<TextFieldWithDesc
value={value}
onChange={onTextChange}
onChange={(val) => {
if (typeof val === 'string') onTextChange(val);
else onTextChange(val.target.value);
}}
description={description}
/>
</Box>

View file

@ -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 (
<Box>
<TextField
@ -20,6 +22,7 @@ const TextFieldWithDesc = ({
sx={{ backgroundColor: 'white' }}
value={value}
onChange={(event) => onChange(event.target.value)}
{...props}
/>
<Typography fontSize={12} mt={1}>
{description}

View file

@ -6,6 +6,28 @@ import { Formik, FormikProps, FormikValues, useFormikContext } from 'formik';
import ToolOptionGroups, { ToolOptionGroup } from './ToolOptionGroups';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
const FormikListenerComponent = <T,>({
initialValues,
input,
compute
}: {
initialValues: T;
input: any;
compute: (optionsValues: T, input: any) => void;
}) => {
const { values } = useFormikContext<typeof initialValues>();
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<T extends FormikValues>({
children,
initialValues,
@ -24,21 +46,7 @@ export default function ToolOptions<T extends FormikValues>({
formRef?: RefObject<FormikProps<T>>;
}) {
const theme = useTheme();
const FormikListenerComponent = () => {
const { values } = useFormikContext<typeof initialValues>();
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 (
<Box
sx={{
@ -62,7 +70,11 @@ export default function ToolOptions<T extends FormikValues>({
>
{(formikProps) => (
<Stack direction={'row'} spacing={2}>
<FormikListenerComponent />
<FormikListenerComponent
compute={compute}
input={input}
initialValues={initialValues}
/>
<ToolOptionGroups groups={getGroups(formikProps)} />
{children}
</Stack>

View file

@ -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 = {

View file

@ -0,0 +1,6 @@
import { expect, describe, it } from 'vitest';
// import { } from './service';
//
// describe('change-speed', () => {
//
// })

View file

@ -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<File | null>(null);
const [result, setResult] = useState<File | null>(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 (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/gif']}
title={'Input GIF'}
/>
}
result={
<ToolFileResult
title={'Output GIF with new speed'}
value={result}
extension={'gif'}
/>
}
/>
<ToolOptions
compute={compute}
getGroups={({ values, setFieldValue }) => [
{
title: 'New GIF speed',
component: (
<Box>
<TextFieldWithDesc
value={values.newSpeed}
onChange={(val) => setFieldValue('newSpeed', val)}
description={'Default new GIF speed.'}
InputProps={{ endAdornment: <Typography>ms</Typography> }}
type={'number'}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);
}

View file

@ -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'))
});

View file

@ -0,0 +1,3 @@
import { tool as gifChangeSpeed } from './change-speed/meta';
export const gifTools = [gifChangeSpeed];

3
src/pages/video/index.ts Normal file
View file

@ -0,0 +1,3 @@
import { gifTools } from './gif';
export const videoTools = [...gifTools];

View file

@ -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 = (

18
src/utils/gif.ts Normal file
View file

@ -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 });
}