Skip to content

Commit

Permalink
feat: change pgn opacity
Browse files Browse the repository at this point in the history
  • Loading branch information
iib0011 committed Mar 8, 2025
1 parent 5d04e57 commit 4e1d246
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 45 deletions.
36 changes: 14 additions & 22 deletions .idea/workspace.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

145 changes: 131 additions & 14 deletions src/pages/tools/image/png/change-opacity/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,74 @@ import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import { CardExampleType } from '@components/examples/ToolExamples';
import { ToolComponentProps } from '@tools/defineTool';
import { updateNumberField } from '@utils/string';
import { Box } from '@mui/material';
import SimpleRadio from '@components/options/SimpleRadio';

type InitialValuesType = {
opacity: number;
mode: 'solid' | 'gradient';
gradientType: 'linear' | 'radial';
gradientDirection: 'left-to-right' | 'inside-out';
areaLeft: number;
areaTop: number;
areaWidth: number;
areaHeight: number;
};

const initialValues: InitialValuesType = {
opacity: 1
opacity: 0.5,
mode: 'solid',
gradientType: 'linear',
gradientDirection: 'left-to-right',
areaLeft: 0,
areaTop: 0,
areaWidth: 100,
areaHeight: 100
};

const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Semi-transparent PNG',
description: 'Make an image 50% transparent',
sampleOptions: {
opacity: 0.5
opacity: 0.5,
mode: 'solid',
gradientType: 'linear',
gradientDirection: 'left-to-right',
areaLeft: 0,
areaTop: 0,
areaWidth: 100,
areaHeight: 100
},
sampleResult: ''
},
{
title: 'Slightly Faded PNG',
description: 'Create a subtle transparency effect',
sampleOptions: {
opacity: 0.8
opacity: 0.8,
mode: 'solid',
gradientType: 'linear',
gradientDirection: 'left-to-right',
areaLeft: 0,
areaTop: 0,
areaWidth: 100,
areaHeight: 100
},
sampleResult: ''
},
{
title: 'Radial Gradient Opacity',
description: 'Apply a radial gradient opacity effect',
sampleOptions: {
opacity: 0.8,
mode: 'gradient',
gradientType: 'radial',
gradientDirection: 'inside-out',
areaLeft: 25,
areaTop: 25,
areaWidth: 50,
areaHeight: 50
},
sampleResult: ''
}
Expand All @@ -41,7 +86,7 @@ export default function ChangeOpacity({ title }: ToolComponentProps) {

const compute = (values: InitialValuesType, input: any) => {
if (input) {
changeOpacity(input, values.opacity).then(setResult);
changeOpacity(input, values).then(setResult);
}
};
return (
Expand All @@ -64,20 +109,92 @@ export default function ChangeOpacity({ title }: ToolComponentProps) {
/>
}
initialValues={initialValues}
exampleCards={exampleCards}
// exampleCards={exampleCards}
getGroups={({ values, updateField }) => [
{
title: 'Opacity Settings',
component: (
<TextFieldWithDesc
description="Set opacity between 0 (transparent) and 1 (opaque)"
value={values.opacity}
onOwnChange={(val) =>
updateNumberField(val, 'opacity', updateField)
}
type="number"
inputProps={{ step: 0.1, min: 0, max: 1 }}
/>
<Box>
<TextFieldWithDesc
description="Set opacity between 0 (transparent) and 1 (opaque)"
value={values.opacity}
onOwnChange={(val) =>
updateNumberField(val, 'opacity', updateField)
}
type="number"
inputProps={{ step: 0.1, min: 0, max: 1 }}
/>
<SimpleRadio
onClick={() => updateField('mode', 'solid')}
checked={values.mode === 'solid'}
description={'Set the same opacity level for all pixels'}
title={'Apply Solid Opacity'}
/>
<SimpleRadio
onClick={() => updateField('mode', 'gradient')}
checked={values.mode === 'gradient'}
description={'Change opacity in a gradient'}
title={'Apply Gradient Opacity'}
/>
</Box>
)
},
{
title: 'Gradient Options',
component: (
<Box>
<SimpleRadio
onClick={() => updateField('gradientType', 'linear')}
checked={values.gradientType === 'linear'}
description={'Linear opacity direction'}
title={'Linear Gradient'}
/>
<SimpleRadio
onClick={() => updateField('gradientType', 'radial')}
checked={values.gradientType === 'radial'}
description={'Radial opacity direction'}
title={'Radial Gradient'}
/>
</Box>
)
},
{
title: 'Opacity Area',
component: (
<Box>
<TextFieldWithDesc
description="Left position"
value={values.areaLeft}
onOwnChange={(val) =>
updateNumberField(val, 'areaLeft', updateField)
}
type="number"
/>
<TextFieldWithDesc
description="Top position"
value={values.areaTop}
onOwnChange={(val) =>
updateNumberField(val, 'areaTop', updateField)
}
type="number"
/>
<TextFieldWithDesc
description="Width"
value={values.areaWidth}
onOwnChange={(val) =>
updateNumberField(val, 'areaWidth', updateField)
}
type="number"
/>
<TextFieldWithDesc
description="Height"
value={values.areaHeight}
onOwnChange={(val) =>
updateNumberField(val, 'areaHeight', updateField)
}
type="number"
/>
</Box>
)
}
]}
Expand Down
100 changes: 91 additions & 9 deletions src/pages/tools/image/png/change-opacity/service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
export async function changeOpacity(
file: File,
opacity: number
): Promise<File> {
interface OpacityOptions {
opacity: number;
mode: 'solid' | 'gradient';
gradientType: 'linear' | 'radial';
gradientDirection: 'left-to-right' | 'inside-out';
areaLeft: number;
areaTop: number;
areaWidth: number;
areaHeight: number;
}

export async function changeOpacity(file: File, options: OpacityOptions): Promise<File> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
Expand All @@ -13,13 +21,14 @@ export async function changeOpacity(
reject(new Error('Canvas context not supported'));
return;
}

canvas.width = img.width;
canvas.height = img.height;

ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = opacity;
ctx.drawImage(img, 0, 0);
if (options.mode === 'solid') {
applySolidOpacity(ctx, img, options);
} else {
applyGradientOpacity(ctx, img, options);
}

canvas.toBlob((blob) => {
if (blob) {
Expand All @@ -31,9 +40,82 @@ export async function changeOpacity(
}, 'image/png');
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = <string>event.target?.result;
img.src = event.target?.result as string;
};
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsDataURL(file);
});
}

function applySolidOpacity(
ctx: CanvasRenderingContext2D,
img: HTMLImageElement,
options: OpacityOptions
) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalAlpha = options.opacity;
ctx.drawImage(img, 0, 0);
}

function applyGradientOpacity(
ctx: CanvasRenderingContext2D,
img: HTMLImageElement,
options: OpacityOptions
) {
const { areaLeft, areaTop, areaWidth, areaHeight } = options;

ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, 0, 0);

const gradient = options.gradientType === 'linear'
? createLinearGradient(ctx, options)
: createRadialGradient(ctx, options);

ctx.fillStyle = gradient;
ctx.fillRect(areaLeft, areaTop, areaWidth, areaHeight);
}

function createLinearGradient(
ctx: CanvasRenderingContext2D,
options: OpacityOptions
) {
const { areaLeft, areaTop, areaWidth, areaHeight } = options;
const gradient = ctx.createLinearGradient(
areaLeft,
areaTop,
areaLeft + areaWidth,
areaTop
);
gradient.addColorStop(0, `rgba(255,255,255,${options.opacity})`);
gradient.addColorStop(1, 'rgba(255,255,255,0)');
return gradient;
}

function createRadialGradient(
ctx: CanvasRenderingContext2D,
options: OpacityOptions
) {
const { areaLeft, areaTop, areaWidth, areaHeight } = options;
const centerX = areaLeft + areaWidth / 2;
const centerY = areaTop + areaHeight / 2;
const radius = Math.min(areaWidth, areaHeight) / 2;

const gradient = ctx.createRadialGradient(
centerX,
centerY,
0,
centerX,
centerY,
radius
);

if (options.gradientDirection === 'inside-out') {
gradient.addColorStop(0, `rgba(255,255,255,${options.opacity})`);
gradient.addColorStop(1, 'rgba(255,255,255,0)');
} else {
gradient.addColorStop(0, 'rgba(255,255,255,0)');
gradient.addColorStop(1, `rgba(255,255,255,${options.opacity})`);
}

return gradient;
}

0 comments on commit 4e1d246

Please sign in to comment.