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",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@ -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:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										241
									
								
								src/App.svelte
									
									
									
									
									
								
							
							
						
						
									
										241
									
								
								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 />
 | 
			
		||||
 | 
			
		||||
  <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>
 | 
			
		||||
  </ButtonGroup>
 | 
			
		||||
 | 
			
		||||
  <div class="flex space-x-2 w-full">
 | 
			
		||||
    <img src={originalSrc} alt="" class="w-1/2" />
 | 
			
		||||
 | 
			
		||||
    {#if processing}
 | 
			
		||||
      <div class="flex items-center justify-center w-1/2">
 | 
			
		||||
        <Spinner />
 | 
			
		||||
      </div>
 | 
			
		||||
    {:else if filterSrc}
 | 
			
		||||
      <img src={filterSrc} alt="" class="w-1/2" />
 | 
			
		||||
    {/if}
 | 
			
		||||
<div class="p-4 space-y-2 h-screen flex flex-col">
 | 
			
		||||
  <div class="w-max">
 | 
			
		||||
    <ImageUpload on:upload={handleUpload} />
 | 
			
		||||
  </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,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  {#if src}
 | 
			
		||||
    <P>Filters</P>
 | 
			
		||||
    <div class="flex">
 | 
			
		||||
      <ButtonGroup>
 | 
			
		||||
        <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="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="absolute">
 | 
			
		||||
          <Spinner />
 | 
			
		||||
        </div>
 | 
			
		||||
      {/if}
 | 
			
		||||
    </div>
 | 
			
		||||
    <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.
										
									
								
							| 
		 Before Width: | Height: | Size: 1.0 MiB  | 
							
								
								
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user