Stuff
This commit is contained in:
parent
de93ee957b
commit
2ca44089b3
|
@ -1,13 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>imgproc</title>
|
||||
<script defer type="module" src="/src/main.ts"></script>
|
||||
</head>
|
||||
<body>
|
||||
<body class="bg-white dark:bg-gray-800">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"@popperjs/core": "^2.11.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.3",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"@types/node": "^18.15.12",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"chart.js": "^4.2.1",
|
||||
"classnames": "^2.3.2",
|
||||
|
|
|
@ -10,6 +10,9 @@ devDependencies:
|
|||
'@tsconfig/svelte':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
'@types/node':
|
||||
specifier: ^18.15.12
|
||||
version: 18.15.12
|
||||
autoprefixer:
|
||||
specifier: ^10.4.7
|
||||
version: 10.4.7(postcss@8.4.21)
|
||||
|
@ -60,7 +63,7 @@ devDependencies:
|
|||
version: 4.9.3
|
||||
vite:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
version: 4.2.0(@types/node@18.15.12)
|
||||
|
||||
packages:
|
||||
|
||||
|
@ -420,7 +423,7 @@ packages:
|
|||
magic-string: 0.29.0
|
||||
svelte: 3.55.1
|
||||
svelte-hmr: 0.15.1(svelte@3.55.1)
|
||||
vite: 4.2.0
|
||||
vite: 4.2.0(@types/node@18.15.12)
|
||||
vitefu: 0.2.4(vite@4.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -430,6 +433,10 @@ packages:
|
|||
resolution: {integrity: sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==}
|
||||
dev: true
|
||||
|
||||
/@types/node@18.15.12:
|
||||
resolution: {integrity: sha512-Wha1UwsB3CYdqUm2PPzh/1gujGCNtWVUYF0mB00fJFoR4gTyWTDPjSm+zBF787Ahw8vSGgBja90MkgFwvB86Dg==}
|
||||
dev: true
|
||||
|
||||
/@types/pug@2.0.6:
|
||||
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
|
||||
dev: true
|
||||
|
@ -1483,7 +1490,7 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/vite@4.2.0:
|
||||
/vite@4.2.0(@types/node@18.15.12):
|
||||
resolution: {integrity: sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
|
@ -1508,6 +1515,7 @@ packages:
|
|||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 18.15.12
|
||||
esbuild: 0.17.15
|
||||
postcss: 8.4.21
|
||||
resolve: 1.22.2
|
||||
|
@ -1524,7 +1532,7 @@ packages:
|
|||
vite:
|
||||
optional: true
|
||||
dependencies:
|
||||
vite: 4.2.0
|
||||
vite: 4.2.0(@types/node@18.15.12)
|
||||
dev: true
|
||||
|
||||
/wrappy@1.0.2:
|
||||
|
|
225
src/App.svelte
225
src/App.svelte
|
@ -1,84 +1,189 @@
|
|||
<script lang="ts">
|
||||
import { Button, ButtonGroup, Fileupload, Spinner } from 'flowbite-svelte';
|
||||
import { applyFilter, getHistogram } from './lib/processor';
|
||||
import { Button, ButtonGroup, Spinner, Range, P } from 'flowbite-svelte';
|
||||
import {
|
||||
applyFilter,
|
||||
grayscale,
|
||||
threshold as threshold,
|
||||
} from './lib/engine/processor';
|
||||
import {
|
||||
boxFilter,
|
||||
sharpening,
|
||||
type FilterFn,
|
||||
edgeDetection,
|
||||
sobel,
|
||||
} from './lib/filters';
|
||||
import { Line } from 'svelte-chartjs';
|
||||
sharpeningFilter,
|
||||
laplaceFilter,
|
||||
sobelFilter,
|
||||
gaussianBlurFilter,
|
||||
boxBlurFilter,
|
||||
removeColorChannelFilter,
|
||||
thresholdFilter,
|
||||
enhanceColorChannelFilter,
|
||||
setBrightnessFilter,
|
||||
} from './lib/engine/filters';
|
||||
import 'chart.js/auto';
|
||||
import ImageUpload from './lib/components/ImageUpload.svelte';
|
||||
import demoImage from './assets/image.jpeg';
|
||||
import { ColorChannel } from './lib/engine/types';
|
||||
|
||||
let files: FileList;
|
||||
let originalSrc: string;
|
||||
let filterSrc: string;
|
||||
let originalSrc: string | null = demoImage;
|
||||
let src: string | null = demoImage;
|
||||
let processing = false;
|
||||
let histogram: { red: number[]; green: number[]; blue: number[] };
|
||||
|
||||
$: if (files) {
|
||||
originalSrc = URL.createObjectURL(files[0]);
|
||||
getHistogram(originalSrc).then((h) => (histogram = h));
|
||||
let thresholdValue = 128;
|
||||
let brightnessValue = 1;
|
||||
|
||||
type Filter = {
|
||||
name: string;
|
||||
src: string;
|
||||
};
|
||||
|
||||
let filters: Filter[] = [];
|
||||
|
||||
async function apply(name: string, filter: FilterFn) {
|
||||
processing = true;
|
||||
src = await applyFilter(src, filter);
|
||||
filters = [...filters, { name, src }];
|
||||
processing = false;
|
||||
}
|
||||
|
||||
async function apply(filter: FilterFn) {
|
||||
processing = true;
|
||||
filterSrc = await applyFilter(originalSrc, filter);
|
||||
processing = false;
|
||||
function handleUpload(e: CustomEvent) {
|
||||
src = e.detail.src;
|
||||
originalSrc = e.detail.src;
|
||||
}
|
||||
|
||||
function undo() {
|
||||
filters = filters.slice(0, -1);
|
||||
src = filters[filters.length - 1]?.src || originalSrc;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
filters = [];
|
||||
src = originalSrc;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-4 space-y-2 w-screen">
|
||||
<Fileupload bind:files />
|
||||
<div class="p-4 space-y-2 h-screen flex flex-col">
|
||||
<div class="w-max">
|
||||
<ImageUpload on:upload={handleUpload} />
|
||||
</div>
|
||||
|
||||
{#if src}
|
||||
<P>Filters</P>
|
||||
<div class="flex">
|
||||
<ButtonGroup>
|
||||
<Button on:click={() => apply(boxFilter(2))}>Box Filter</Button>
|
||||
<Button on:click={() => apply(sharpening)}>Sharpening</Button>
|
||||
<Button on:click={() => apply(edgeDetection)}>Edge detection</Button>
|
||||
<Button on:click={() => apply(sobel)}>Sobel</Button>
|
||||
<Button on:click={() => apply('Box blur', boxBlurFilter)}
|
||||
>Box blur</Button
|
||||
>
|
||||
<Button on:click={() => apply('Gaussian blur', gaussianBlurFilter)}>
|
||||
Gaussian blur
|
||||
</Button>
|
||||
<Button on:click={() => apply('Laplace', laplaceFilter)}>Laplace</Button
|
||||
>
|
||||
<Button on:click={() => apply('Sobel', sobelFilter)}>Sobel</Button>
|
||||
<Button on:click={() => apply('Sharpening', sharpeningFilter)}>
|
||||
Sharpening
|
||||
</Button>
|
||||
<Button on:click={() => apply('Grayscale', grayscale)}>
|
||||
Grayscale
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<div class="flex space-x-2 w-full">
|
||||
<img src={originalSrc} alt="" class="w-1/2" />
|
||||
<div class="grow" />
|
||||
|
||||
<div class="gap-y-2">
|
||||
<Button on:click={undo}>Undo</Button>
|
||||
<Button color="red" on:click={reset}>Reset</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-4 w-96">
|
||||
<Button
|
||||
on:click={() =>
|
||||
apply(`Treshold: ${thresholdValue}`, thresholdFilter(thresholdValue))}
|
||||
>
|
||||
Threshold
|
||||
</Button>
|
||||
<P class="w-16">{thresholdValue}</P>
|
||||
<Range bind:value={thresholdValue} min={0} max={255} />
|
||||
</div>
|
||||
<div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="red"
|
||||
on:click={() =>
|
||||
apply('Remove Red', removeColorChannelFilter(ColorChannel.red))}
|
||||
>
|
||||
Remove Red
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
on:click={() =>
|
||||
apply('Remove Green', removeColorChannelFilter(ColorChannel.green))}
|
||||
>
|
||||
Remove Green
|
||||
</Button>
|
||||
<Button
|
||||
color="blue"
|
||||
on:click={() =>
|
||||
apply('Remove Blue', removeColorChannelFilter(ColorChannel.blue))}
|
||||
>
|
||||
Remove Blue
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="red"
|
||||
on:click={() =>
|
||||
apply('Enhance Red', enhanceColorChannelFilter(ColorChannel.red))}
|
||||
>
|
||||
Enhance Red
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
on:click={() =>
|
||||
apply(
|
||||
'Enhance Green',
|
||||
enhanceColorChannelFilter(ColorChannel.green),
|
||||
)}
|
||||
>
|
||||
Enhance Green
|
||||
</Button>
|
||||
<Button
|
||||
color="blue"
|
||||
on:click={() =>
|
||||
apply('Enhance Blue', enhanceColorChannelFilter(ColorChannel.blue))}
|
||||
>
|
||||
Enhance Blue
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-4 w-96">
|
||||
<Button
|
||||
on:click={() =>
|
||||
apply(
|
||||
`Brightness: ${brightnessValue}`,
|
||||
setBrightnessFilter(brightnessValue),
|
||||
)}
|
||||
>
|
||||
Brightness
|
||||
</Button>
|
||||
<P class="w-16">{brightnessValue}</P>
|
||||
<Range bind:value={brightnessValue} min={0} max={5} step={0.1} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grow flex">
|
||||
<div class="w-64" />
|
||||
<div class="grow flex justify-center items-center">
|
||||
<img {src} alt="" />
|
||||
{#if processing}
|
||||
<div class="flex items-center justify-center w-1/2">
|
||||
<div class="absolute">
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else if filterSrc}
|
||||
<img src={filterSrc} alt="" class="w-1/2" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="w-1/2">
|
||||
<Line
|
||||
data={{
|
||||
labels: new Array(256).fill(0).map((_, i) => i),
|
||||
datasets: [
|
||||
{
|
||||
fill: true,
|
||||
label: 'Red',
|
||||
borderColor: 'red',
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.2)',
|
||||
data: histogram?.red,
|
||||
},
|
||||
{
|
||||
fill: true,
|
||||
label: 'Green',
|
||||
borderColor: 'green',
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.2)',
|
||||
data: histogram?.green,
|
||||
},
|
||||
{
|
||||
fill: true,
|
||||
label: 'Blue',
|
||||
borderColor: 'blue',
|
||||
backgroundColor: 'rgba(0, 0, 255, 0.2)',
|
||||
data: histogram?.blue,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div class="w-64">
|
||||
{#each filters as filter}
|
||||
<P>{filter.name}</P>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
BIN
src/assets/banana.png
Normal file
BIN
src/assets/banana.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
Before Width: | Height: | Size: 393 KiB |
BIN
src/assets/image.jpeg
Normal file
BIN
src/assets/image.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
40
src/lib/components/Histogram.svelte
Normal file
40
src/lib/components/Histogram.svelte
Normal file
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import { Line } from 'svelte-chartjs';
|
||||
|
||||
type HistogramData = {
|
||||
red: number[];
|
||||
green: number[];
|
||||
blue: number[];
|
||||
};
|
||||
|
||||
export let histogram: HistogramData;
|
||||
</script>
|
||||
|
||||
<Line
|
||||
data={{
|
||||
labels: new Array(256).fill(0).map((_, i) => i),
|
||||
datasets: [
|
||||
{
|
||||
fill: true,
|
||||
label: 'Red',
|
||||
borderColor: 'red',
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.2)',
|
||||
data: histogram.red,
|
||||
},
|
||||
{
|
||||
fill: true,
|
||||
label: 'Green',
|
||||
borderColor: 'green',
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.2)',
|
||||
data: histogram.green,
|
||||
},
|
||||
{
|
||||
fill: true,
|
||||
label: 'Blue',
|
||||
borderColor: 'blue',
|
||||
backgroundColor: 'rgba(0, 0, 255, 0.2)',
|
||||
data: histogram.blue,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
13
src/lib/components/ImageUpload.svelte
Normal file
13
src/lib/components/ImageUpload.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { Fileupload } from 'flowbite-svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleFileChange(e) {
|
||||
const src = URL.createObjectURL(e.currentTarget.files[0]);
|
||||
dispatch('upload', { src });
|
||||
}
|
||||
</script>
|
||||
|
||||
<Fileupload on:change={handleFileChange} />
|
101
src/lib/engine/filters.ts
Normal file
101
src/lib/engine/filters.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import {
|
||||
grayscale,
|
||||
clone,
|
||||
add,
|
||||
applyMatrix,
|
||||
removeColorChannel,
|
||||
threshold,
|
||||
enhanceColorChannel,
|
||||
setBrightness,
|
||||
} from './processor';
|
||||
import type { ColorChannel } from './types';
|
||||
|
||||
export type FilterFn = (imageData: ImageData) => Promise<void> | void;
|
||||
|
||||
export const boxBlurFilter: FilterFn = async (imageData: ImageData) => {
|
||||
await applyMatrix(imageData, {
|
||||
matrix: [
|
||||
[1, 1, 1],
|
||||
[1, 1, 1],
|
||||
[1, 1, 1],
|
||||
],
|
||||
div: 9,
|
||||
});
|
||||
};
|
||||
|
||||
export const gaussianBlurFilter: FilterFn = async (imageData: ImageData) => {
|
||||
await applyMatrix(imageData, {
|
||||
matrix: [
|
||||
[1, 2, 1],
|
||||
[2, 4, 2],
|
||||
[1, 2, 1],
|
||||
],
|
||||
div: 16,
|
||||
});
|
||||
};
|
||||
|
||||
export const laplaceFilter: FilterFn = async (imageData: ImageData) => {
|
||||
await grayscale(imageData);
|
||||
await applyMatrix(imageData, {
|
||||
matrix: [
|
||||
[0, -1, 0],
|
||||
[-1, 4, -1],
|
||||
[0, -1, 0],
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export const sobelFilter: FilterFn = async (imageData: ImageData) => {
|
||||
await grayscale(imageData);
|
||||
const data2 = clone(imageData);
|
||||
|
||||
await Promise.all([
|
||||
applyMatrix(imageData, {
|
||||
matrix: [
|
||||
[-1, 0, 1],
|
||||
[-2, 0, 2],
|
||||
[-1, 0, 1],
|
||||
],
|
||||
}),
|
||||
|
||||
applyMatrix(data2, {
|
||||
matrix: [
|
||||
[-1, -2, -1],
|
||||
[0, 0, 0],
|
||||
[1, 2, 1],
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
await add(imageData, data2);
|
||||
};
|
||||
|
||||
export const sharpeningFilter: FilterFn = async (imageData: ImageData) => {
|
||||
await applyMatrix(imageData, {
|
||||
matrix: [
|
||||
[0, -1, 0],
|
||||
[-1, 5, -1],
|
||||
[0, -1, 0],
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export const thresholdFilter =
|
||||
(thresholdValue: number): FilterFn =>
|
||||
(imageData: ImageData) =>
|
||||
threshold(imageData, thresholdValue);
|
||||
|
||||
export const removeColorChannelFilter =
|
||||
(colorChannel: ColorChannel): FilterFn =>
|
||||
(imageData: ImageData) =>
|
||||
removeColorChannel(imageData, colorChannel);
|
||||
|
||||
export const enhanceColorChannelFilter =
|
||||
(colorChannel: ColorChannel): FilterFn =>
|
||||
(imageData: ImageData) =>
|
||||
enhanceColorChannel(imageData, colorChannel);
|
||||
|
||||
export const setBrightnessFilter =
|
||||
(brightness: number): FilterFn =>
|
||||
(imageData: ImageData) =>
|
||||
setBrightness(imageData, brightness);
|
182
src/lib/engine/processor.ts
Normal file
182
src/lib/engine/processor.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
import type { FilterFn } from './filters';
|
||||
import type { ColorChannel } from './types';
|
||||
import type { AddParams } from './workers/add';
|
||||
import type { ApplyMarixParams } from './workers/applyMatrix';
|
||||
import type { EnhanceColorChannelParams } from './workers/enhanceColorChannel';
|
||||
import type { HistogramData } from './workers/getHistogram';
|
||||
import type { RemoveColorChannelParams } from './workers/removeColorChannel';
|
||||
import type { SetBrightnessParams } from './workers/setBrightness';
|
||||
import type { SubtractParams } from './workers/subtract';
|
||||
import type { TresholdParams } from './workers/treshold';
|
||||
|
||||
export type Filter = {
|
||||
matrix: number[][];
|
||||
div?: number;
|
||||
};
|
||||
|
||||
export async function applyFilter(
|
||||
src: string,
|
||||
filter: FilterFn,
|
||||
): Promise<string> {
|
||||
const image = await loadImage(src);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
await filter(imageData);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const url = canvas.toDataURL();
|
||||
canvas.remove();
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export async function applyMatrix(imageData: ImageData, filter: Filter) {
|
||||
const newImageData = await runInWorker<ApplyMarixParams, ImageData>(
|
||||
'./workers/applyMatrix.ts',
|
||||
{ imageData, filter },
|
||||
);
|
||||
imageData.data.set(newImageData.data);
|
||||
}
|
||||
|
||||
async function loadImage(src: string) {
|
||||
const image = new Image();
|
||||
image.src = src;
|
||||
return new Promise<HTMLImageElement>((res, rej) => {
|
||||
image.onload = () => res(image);
|
||||
image.onerror = rej;
|
||||
});
|
||||
}
|
||||
|
||||
async function getImageData(src: string): Promise<ImageData> {
|
||||
const image = await loadImage(src);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
export async function getHistogram(src: string) {
|
||||
const imageData = await getImageData(src);
|
||||
const histogram = await runInWorker<ImageData, HistogramData>(
|
||||
'./workers/getHistogram.ts',
|
||||
imageData,
|
||||
);
|
||||
return histogram;
|
||||
}
|
||||
|
||||
async function runInWorker<I, O>(url: string, data: I) {
|
||||
return new Promise<O>((res, rej) => {
|
||||
if (!window.Worker) {
|
||||
rej('Worker is not supported in this browser');
|
||||
return;
|
||||
}
|
||||
|
||||
const workerUrl = new URL(url, import.meta.url);
|
||||
const worker = new Worker(workerUrl, { type: 'module' });
|
||||
|
||||
worker.onmessage = (e: MessageEvent<O>) => res(e.data);
|
||||
worker.postMessage(data);
|
||||
});
|
||||
}
|
||||
|
||||
export async function grayscale(imageData: ImageData) {
|
||||
const newImageData = await runInWorker<ImageData, ImageData>(
|
||||
'./workers/grayscale.ts',
|
||||
imageData,
|
||||
);
|
||||
imageData.data.set(newImageData.data);
|
||||
}
|
||||
|
||||
export async function add(imageData1: ImageData, imageData2: ImageData) {
|
||||
const res = await runInWorker<AddParams, AddParams>('./workers/add.ts', {
|
||||
imageData1,
|
||||
imageData2,
|
||||
});
|
||||
imageData1.data.set(res.imageData1.data);
|
||||
imageData2.data.set(res.imageData2.data);
|
||||
}
|
||||
|
||||
export async function subtract(imageData1: ImageData, imageData2: ImageData) {
|
||||
const res = await runInWorker<SubtractParams, SubtractParams>(
|
||||
'./workers/subtract.ts',
|
||||
{
|
||||
imageData1,
|
||||
imageData2,
|
||||
},
|
||||
);
|
||||
imageData1.data.set(res.imageData1.data);
|
||||
imageData2.data.set(res.imageData2.data);
|
||||
}
|
||||
|
||||
export function clone(imageData: ImageData): ImageData {
|
||||
return {
|
||||
width: imageData.width,
|
||||
height: imageData.height,
|
||||
colorSpace: imageData.colorSpace,
|
||||
data: new Uint8ClampedArray(imageData.data),
|
||||
};
|
||||
}
|
||||
|
||||
export async function threshold(imageData: ImageData, threshold: number) {
|
||||
const newImageData = await runInWorker<TresholdParams, ImageData>(
|
||||
'./workers/treshold.ts',
|
||||
{
|
||||
imageData,
|
||||
threshold,
|
||||
},
|
||||
);
|
||||
imageData.data.set(newImageData.data);
|
||||
}
|
||||
|
||||
export async function removeColorChannel(
|
||||
imageData: ImageData,
|
||||
colorChannel: ColorChannel,
|
||||
) {
|
||||
const newImageData = await runInWorker<RemoveColorChannelParams, ImageData>(
|
||||
'./workers/removeColorChannel.ts',
|
||||
{
|
||||
imageData,
|
||||
colorChannel,
|
||||
},
|
||||
);
|
||||
imageData.data.set(newImageData.data);
|
||||
}
|
||||
|
||||
export async function enhanceColorChannel(
|
||||
imageData: ImageData,
|
||||
colorChannel: ColorChannel,
|
||||
) {
|
||||
const newImageData = await runInWorker<EnhanceColorChannelParams, ImageData>(
|
||||
'./workers/enhanceColorChannel.ts',
|
||||
{
|
||||
imageData,
|
||||
colorChannel,
|
||||
},
|
||||
);
|
||||
imageData.data.set(newImageData.data);
|
||||
}
|
||||
|
||||
export async function setBrightness(imageData: ImageData, brightness: number) {
|
||||
const newImageData = await runInWorker<SetBrightnessParams, ImageData>(
|
||||
'./workers/setBrightness.ts',
|
||||
{
|
||||
imageData,
|
||||
brightness,
|
||||
},
|
||||
);
|
||||
imageData.data.set(newImageData.data);
|
||||
}
|
49
src/lib/engine/proxy.ts
Normal file
49
src/lib/engine/proxy.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
export type Pixel = {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
};
|
||||
|
||||
export class ImageDataProxy {
|
||||
constructor(private readonly imageData: ImageData) {}
|
||||
|
||||
public getPixel(x: number, y: number): Pixel {
|
||||
const { data, width, height } = this.imageData;
|
||||
|
||||
if (x > width - 1) x = width - 1;
|
||||
else if (x < 0) x = 0;
|
||||
|
||||
if (y > height - 1) y = height - 1;
|
||||
else if (y < 0) y = 0;
|
||||
|
||||
const i = (y * width + x) * 4;
|
||||
return {
|
||||
r: data[i],
|
||||
g: data[i + 1],
|
||||
b: data[i + 2],
|
||||
a: data[i + 3],
|
||||
};
|
||||
}
|
||||
|
||||
public setPixel(x: number, y: number, pixel: Pixel) {
|
||||
const { data, width, height } = this.imageData;
|
||||
|
||||
if (x > width - 1 || y > height - 1) return;
|
||||
|
||||
if (pixel.r > 255) pixel.r = 255;
|
||||
else if (pixel.r < 0) pixel.r = 0;
|
||||
|
||||
if (pixel.g > 255) pixel.g = 255;
|
||||
else if (pixel.g < 0) pixel.g = 0;
|
||||
|
||||
if (pixel.b > 255) pixel.b = 255;
|
||||
else if (pixel.b < 0) pixel.b = 0;
|
||||
|
||||
const i = (y * width + x) * 4;
|
||||
data[i] = pixel.r;
|
||||
data[i + 1] = pixel.g;
|
||||
data[i + 2] = pixel.b;
|
||||
data[i + 3] = pixel.a;
|
||||
}
|
||||
}
|
5
src/lib/engine/types.ts
Normal file
5
src/lib/engine/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum ColorChannel {
|
||||
red = 0,
|
||||
green = 1,
|
||||
blue = 2,
|
||||
}
|
8
src/lib/engine/worker.ts
Normal file
8
src/lib/engine/worker.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
type WorkerFn<T> = (data: any) => T;
|
||||
|
||||
export function registerWorker<T>(func: WorkerFn<T>) {
|
||||
self.onmessage = (e: MessageEvent<T>) => {
|
||||
const result = func(e.data);
|
||||
self.postMessage(result);
|
||||
};
|
||||
}
|
27
src/lib/engine/workers/add.ts
Normal file
27
src/lib/engine/workers/add.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { ImageDataProxy } from '../proxy';
|
||||
import { registerWorker } from '../worker';
|
||||
|
||||
export type AddParams = {
|
||||
imageData1: ImageData;
|
||||
imageData2: ImageData;
|
||||
};
|
||||
|
||||
registerWorker(({ imageData1, imageData2 }: AddParams) => {
|
||||
const data1 = new ImageDataProxy(imageData1);
|
||||
const data2 = new ImageDataProxy(imageData2);
|
||||
|
||||
for (let y = 0; y < imageData1.height; y++) {
|
||||
for (let x = 0; x < imageData1.width; x++) {
|
||||
const pix1 = data1.getPixel(x, y);
|
||||
const pix2 = data2.getPixel(x, y);
|
||||
data1.setPixel(x, y, {
|
||||
r: pix1.r + pix2.r,
|
||||
g: pix1.g + pix2.g,
|
||||
b: pix1.b + pix2.b,
|
||||
a: pix1.a,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { imageData1, imageData2 };
|
||||
});
|
48
src/lib/engine/workers/applyMatrix.ts
Normal file
48
src/lib/engine/workers/applyMatrix.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { clone, type Filter } from '../processor';
|
||||
import { ImageDataProxy, type Pixel } from '../proxy';
|
||||
import { registerWorker } from '../worker';
|
||||
|
||||
export type ApplyMarixParams = {
|
||||
imageData: ImageData;
|
||||
filter: Filter;
|
||||
};
|
||||
|
||||
registerWorker(({ imageData, filter }: ApplyMarixParams) => {
|
||||
const { matrix, div = 1 } = filter;
|
||||
|
||||
const proxy = new ImageDataProxy(imageData);
|
||||
|
||||
const newImageData = clone(imageData);
|
||||
const newProxy = new ImageDataProxy(newImageData);
|
||||
|
||||
const offset = (matrix.length - 1) / 2;
|
||||
|
||||
for (let y = 0; y < imageData.height; y++) {
|
||||
for (let x = 0; x < imageData.width; x++) {
|
||||
const pix = proxy.getPixel(x, y);
|
||||
const sum: Pixel = { r: 0, g: 0, b: 0, a: pix.a };
|
||||
|
||||
for (let my = 0; my < matrix.length; my++) {
|
||||
for (let mx = 0; mx < matrix[0].length; mx++) {
|
||||
const mpix = proxy.getPixel(x + mx - offset, y + my - offset);
|
||||
const mult = matrix[my][mx];
|
||||
sum.r += mpix.r * mult;
|
||||
sum.g += mpix.g * mult;
|
||||
sum.b += mpix.b * mult;
|
||||
}
|
||||
}
|
||||
|
||||
if (div !== 1) {
|
||||
sum.r /= div;
|
||||
sum.g /= div;
|
||||
sum.b /= div;
|
||||
}
|
||||
|
||||
newProxy.setPixel(x, y, sum);
|
||||
}
|
||||
}
|
||||
|
||||
return newImageData;
|
||||
});
|
||||
|
||||
export {};
|
17
src/lib/engine/workers/enhanceColorChannel.ts
Normal file
17
src/lib/engine/workers/enhanceColorChannel.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { ColorChannel } from '../types';
|
||||
import { registerWorker } from '../worker';
|
||||
|
||||
export type EnhanceColorChannelParams = {
|
||||
imageData: ImageData;
|
||||
colorChannel: ColorChannel;
|
||||
};
|
||||
|
||||
registerWorker(({ imageData, colorChannel }: EnhanceColorChannelParams) => {
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
data[i + colorChannel] = 255;
|
||||
}
|
||||
return imageData;
|
||||
});
|
||||
|
||||
export {};
|
29
src/lib/engine/workers/getHistogram.ts
Normal file
29
src/lib/engine/workers/getHistogram.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { ImageDataProxy } from '../proxy';
|
||||
import { registerWorker } from '../worker';
|
||||
|
||||
export type HistogramData = {
|
||||
red: number[];
|
||||
green: number[];
|
||||
blue: number[];
|
||||
};
|
||||
|
||||
registerWorker((imageData: ImageData): HistogramData => {
|
||||
const data = new ImageDataProxy(imageData);
|
||||
|
||||
const red = new Array(256).fill(0);
|
||||
const green = new Array(256).fill(0);
|
||||
const blue = new Array(256).fill(0);
|
||||
|
||||
for (let y = 0; y < imageData.height; y++) {
|
||||
for (let x = 0; x < imageData.width; x++) {
|
||||
const pix = data.getPixel(x, y);
|
||||
red[pix.r]++;
|
||||
green[pix.g]++;
|
||||
blue[pix.b]++;
|
||||
}
|
||||
}
|
||||
|
||||
return { red, green, blue };
|
||||
});
|
||||
|
||||
export {};
|
14
src/lib/engine/workers/grayscale.ts
Normal file
14
src/lib/engine/workers/grayscale.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { registerWorker } from '../worker';
|
||||
|
||||
registerWorker((imageData: ImageData) => {
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const val = 0.299 * data[i] + 0.587 * data[i] + 0.114 * data[i];
|
||||
data[i] = val;
|
||||
data[i + 1] = val;
|
||||
data[i + 2] = val;
|
||||
}
|
||||
return imageData;
|
||||
});
|
||||
|
||||
export {};
|
17
src/lib/engine/workers/removeColorChannel.ts
Normal file
17
src/lib/engine/workers/removeColorChannel.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { ColorChannel } from '../types';
|
||||
import { registerWorker } from '../worker';
|
||||
|
||||
export type RemoveColorChannelParams = {
|
||||
imageData: ImageData;
|
||||
colorChannel: ColorChannel;
|
||||
};
|
||||
|
||||
registerWorker(({ imageData, colorChannel }: RemoveColorChannelParams) => {
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
data[i + colorChannel] = 0;
|
||||
}
|
||||
return imageData;
|
||||
});
|
||||
|
||||
export {};
|
17
src/lib/engine/workers/setBrightness.ts
Normal file
17
src/lib/engine/workers/setBrightness.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { registerWorker } from '../worker';
|
||||
|
||||
export type SetBrightnessParams = {
|
||||
imageData: ImageData;
|
||||
brightness: number;
|
||||
};
|
||||
|
||||
registerWorker(({ imageData, brightness }: SetBrightnessParams) => {
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if ((i + 1) % 4 === 0) continue;
|
||||
data[i] *= brightness;
|
||||
}
|
||||
return imageData;
|
||||
});
|
||||
|
||||
export {};
|
27
src/lib/engine/workers/subtract.ts
Normal file
27
src/lib/engine/workers/subtract.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { ImageDataProxy } from '../proxy';
|
||||
import { registerWorker } from '../worker';
|
||||
|
||||
export type SubtractParams = {
|
||||
imageData1: ImageData;
|
||||
imageData2: ImageData;
|
||||
};
|
||||
|
||||
registerWorker(({ imageData1, imageData2 }: SubtractParams) => {
|
||||
const data1 = new ImageDataProxy(imageData1);
|
||||
const data2 = new ImageDataProxy(imageData2);
|
||||
|
||||
for (let y = 0; y < imageData1.height; y++) {
|
||||
for (let x = 0; x < imageData1.width; x++) {
|
||||
const pix1 = data1.getPixel(x, y);
|
||||
const pix2 = data2.getPixel(x, y);
|
||||
data1.setPixel(x, y, {
|
||||
r: pix1.r - pix2.r,
|
||||
g: pix1.g - pix2.g,
|
||||
b: pix1.b - pix2.b,
|
||||
a: pix1.a,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { imageData1, imageData2 };
|
||||
});
|
20
src/lib/engine/workers/treshold.ts
Normal file
20
src/lib/engine/workers/treshold.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { registerWorker } from '../worker';
|
||||
|
||||
export type TresholdParams = {
|
||||
imageData: ImageData;
|
||||
threshold: number;
|
||||
};
|
||||
|
||||
registerWorker(({ imageData, threshold }: TresholdParams) => {
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
let val = 0.299 * data[i] + 0.587 * data[i] + 0.114 * data[i];
|
||||
val = val > threshold ? 255 : 0;
|
||||
data[i] = val;
|
||||
data[i + 1] = val;
|
||||
data[i + 2] = val;
|
||||
}
|
||||
return imageData;
|
||||
});
|
||||
|
||||
export {};
|
|
@ -1,66 +0,0 @@
|
|||
import { applyMatrix, grayscale, type Filter, clone, add } from './processor';
|
||||
|
||||
export type FilterFn = (imageData: ImageData) => void;
|
||||
|
||||
export function boxFilter(amount = 2): FilterFn {
|
||||
return (imageData: ImageData) => {
|
||||
const size = amount * 2 + 1;
|
||||
const matrix = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
const row = [];
|
||||
for (let j = 0; j < size; j++) {
|
||||
row.push(1);
|
||||
}
|
||||
matrix.push(row);
|
||||
}
|
||||
|
||||
applyMatrix(imageData, {
|
||||
div: size ** 2,
|
||||
matrix,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const sharpening: FilterFn = (imageData: ImageData) => {
|
||||
applyMatrix(imageData, {
|
||||
matrix: [
|
||||
[0, -1, 0],
|
||||
[-1, 5, -1],
|
||||
[0, -1, 0],
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export const edgeDetection: FilterFn = (imageData: ImageData) => {
|
||||
grayscale(imageData);
|
||||
applyMatrix(imageData, {
|
||||
matrix: [
|
||||
[0, -1, 0],
|
||||
[-1, 4, -1],
|
||||
[0, -1, 0],
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export const sobel: FilterFn = (imageData: ImageData) => {
|
||||
grayscale(imageData);
|
||||
const data2 = clone(imageData);
|
||||
|
||||
applyMatrix(imageData, {
|
||||
matrix: [
|
||||
[-1, 0, 1],
|
||||
[-2, 0, 2],
|
||||
[-1, 0, 1],
|
||||
],
|
||||
});
|
||||
|
||||
applyMatrix(data2, {
|
||||
matrix: [
|
||||
[-1, -2, -1],
|
||||
[0, 0, 0],
|
||||
[1, 2, 1],
|
||||
],
|
||||
});
|
||||
|
||||
add(imageData, data2);
|
||||
};
|
|
@ -1,209 +0,0 @@
|
|||
import type { FilterFn } from './filters';
|
||||
|
||||
export type Filter = {
|
||||
matrix: number[][];
|
||||
div?: number;
|
||||
};
|
||||
|
||||
export async function applyFilter(
|
||||
src: string,
|
||||
filter: FilterFn,
|
||||
): Promise<string> {
|
||||
const image = await loadImage(src);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
filter(imageData);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const url = canvas.toDataURL();
|
||||
canvas.remove();
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
async function loadImage(src: string) {
|
||||
const image = new Image();
|
||||
image.src = src;
|
||||
return new Promise<HTMLImageElement>((res, rej) => {
|
||||
image.onload = () => res(image);
|
||||
image.onerror = rej;
|
||||
});
|
||||
}
|
||||
|
||||
async function getImageData(src: string): Promise<ImageData> {
|
||||
const image = await loadImage(src);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
export async function getHistogram(src: string) {
|
||||
const imageData = await getImageData(src);
|
||||
|
||||
const data = new ImageDataProxy(imageData);
|
||||
|
||||
const red = new Array(256).fill(0);
|
||||
const green = new Array(256).fill(0);
|
||||
const blue = new Array(256).fill(0);
|
||||
|
||||
for (let y = 0; y < imageData.height; y++) {
|
||||
for (let x = 0; x < imageData.width; x++) {
|
||||
const pix = data.getPixel(x, y);
|
||||
red[pix.r]++;
|
||||
green[pix.g]++;
|
||||
blue[pix.b]++;
|
||||
}
|
||||
}
|
||||
|
||||
return { red, green, blue };
|
||||
}
|
||||
|
||||
export function grayscale(imageData: ImageData) {
|
||||
const data = new ImageDataProxy(imageData);
|
||||
for (let y = 0; y < imageData.height; y++) {
|
||||
for (let x = 0; x < imageData.width; x++) {
|
||||
const pix = data.getPixel(x, y);
|
||||
const avg = pix.r * 0.299 + pix.g * 0.587 + pix.b * 0.114;
|
||||
data.setPixel(x, y, { r: avg, g: avg, b: avg, a: pix.a });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function add(imageData1: ImageData, imageData2: ImageData) {
|
||||
const data1 = new ImageDataProxy(imageData1);
|
||||
const data2 = new ImageDataProxy(imageData2);
|
||||
|
||||
for (let y = 0; y < imageData1.height; y++) {
|
||||
for (let x = 0; x < imageData1.width; x++) {
|
||||
const pix1 = data1.getPixel(x, y);
|
||||
const pix2 = data2.getPixel(x, y);
|
||||
data1.setPixel(x, y, {
|
||||
r: pix1.r + pix2.r,
|
||||
g: pix1.g + pix2.g,
|
||||
b: pix1.b + pix2.b,
|
||||
a: pix1.a,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function subtract(imageData1: ImageData, imageData2: ImageData) {
|
||||
const data1 = new ImageDataProxy(imageData1);
|
||||
const data2 = new ImageDataProxy(imageData2);
|
||||
|
||||
for (let y = 0; y < imageData1.height; y++) {
|
||||
for (let x = 0; x < imageData1.width; x++) {
|
||||
const pix1 = data1.getPixel(x, y);
|
||||
const pix2 = data2.getPixel(x, y);
|
||||
data1.setPixel(x, y, {
|
||||
r: pix1.r - pix2.r,
|
||||
g: pix1.g - pix2.g,
|
||||
b: pix1.b - pix2.b,
|
||||
a: pix1.a,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clone(imageData: ImageData): ImageData {
|
||||
return {
|
||||
width: imageData.width,
|
||||
height: imageData.height,
|
||||
colorSpace: imageData.colorSpace,
|
||||
data: new Uint8ClampedArray(imageData.data),
|
||||
};
|
||||
}
|
||||
|
||||
export function applyMatrix(imageData: ImageData, filter: Filter) {
|
||||
const { matrix, div = 1 } = filter;
|
||||
|
||||
const data = new ImageDataProxy(imageData);
|
||||
|
||||
const offset = -(matrix.length - 1) / 2;
|
||||
|
||||
for (let y = 0; y < imageData.height; y++) {
|
||||
for (let x = 0; x < imageData.width; x++) {
|
||||
const pix = data.getPixel(x, y);
|
||||
let sum: Pixel = { r: 0, g: 0, b: 0, a: pix.a };
|
||||
|
||||
for (let my = 0; my < matrix.length; my++) {
|
||||
for (let mx = 0; mx < matrix[my].length; mx++) {
|
||||
const mpix = data.getPixel(x + mx - offset, y + my - offset);
|
||||
const val = matrix[my][mx];
|
||||
sum.r += mpix.r * val;
|
||||
sum.g += mpix.g * val;
|
||||
sum.b += mpix.b * val;
|
||||
}
|
||||
}
|
||||
|
||||
sum.r /= div;
|
||||
sum.g /= div;
|
||||
sum.b /= div;
|
||||
|
||||
data.setPixel(x, y, sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Pixel = {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
};
|
||||
|
||||
export class ImageDataProxy {
|
||||
constructor(private readonly imageData: ImageData) {}
|
||||
|
||||
public getPixel(x: number, y: number): Pixel {
|
||||
const { data, width, height } = this.imageData;
|
||||
|
||||
if (x > width - 1) x = width - 1;
|
||||
else if (x < 0) x = 0;
|
||||
|
||||
if (y > height - 1) y = height - 1;
|
||||
else if (y < 0) y = 0;
|
||||
|
||||
const i = (y * width + x) * 4;
|
||||
return {
|
||||
r: data[i],
|
||||
g: data[i + 1],
|
||||
b: data[i + 2],
|
||||
a: data[i + 3],
|
||||
};
|
||||
}
|
||||
|
||||
public setPixel(x: number, y: number, pixel: Pixel) {
|
||||
const { data, width, height } = this.imageData;
|
||||
|
||||
if (x > width - 1 || y > height - 1) return;
|
||||
|
||||
if (pixel.r > 255) pixel.r = 255;
|
||||
else if (pixel.r < 0) pixel.r = 0;
|
||||
|
||||
if (pixel.g > 255) pixel.g = 255;
|
||||
else if (pixel.g < 0) pixel.g = 0;
|
||||
|
||||
if (pixel.b > 255) pixel.b = 255;
|
||||
else if (pixel.b < 0) pixel.b = 0;
|
||||
|
||||
const i = (y * width + x) * 4;
|
||||
data[i] = pixel.r;
|
||||
data[i + 1] = pixel.g;
|
||||
data[i + 2] = pixel.b;
|
||||
data[i + 3] = pixel.a;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user