Stuff
This commit is contained in:
		
							parent
							
								
									de93ee957b
								
							
						
					
					
						commit
						2ca44089b3
					
				@ -1,13 +1,13 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en" class="dark">
 | 
				
			||||||
  <head>
 | 
					  <head>
 | 
				
			||||||
    <meta charset="UTF-8" />
 | 
					    <meta charset="UTF-8" />
 | 
				
			||||||
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
 | 
					    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
    <title>imgproc</title>
 | 
					    <title>imgproc</title>
 | 
				
			||||||
 | 
					    <script defer type="module" src="/src/main.ts"></script>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body>
 | 
					  <body class="bg-white dark:bg-gray-800">
 | 
				
			||||||
    <div id="app"></div>
 | 
					    <div id="app"></div>
 | 
				
			||||||
    <script type="module" src="/src/main.ts"></script>
 | 
					 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@
 | 
				
			|||||||
    "@popperjs/core": "^2.11.7",
 | 
					    "@popperjs/core": "^2.11.7",
 | 
				
			||||||
    "@sveltejs/vite-plugin-svelte": "^2.0.3",
 | 
					    "@sveltejs/vite-plugin-svelte": "^2.0.3",
 | 
				
			||||||
    "@tsconfig/svelte": "^3.0.0",
 | 
					    "@tsconfig/svelte": "^3.0.0",
 | 
				
			||||||
 | 
					    "@types/node": "^18.15.12",
 | 
				
			||||||
    "autoprefixer": "^10.4.7",
 | 
					    "autoprefixer": "^10.4.7",
 | 
				
			||||||
    "chart.js": "^4.2.1",
 | 
					    "chart.js": "^4.2.1",
 | 
				
			||||||
    "classnames": "^2.3.2",
 | 
					    "classnames": "^2.3.2",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										16
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@ -10,6 +10,9 @@ devDependencies:
 | 
				
			|||||||
  '@tsconfig/svelte':
 | 
					  '@tsconfig/svelte':
 | 
				
			||||||
    specifier: ^3.0.0
 | 
					    specifier: ^3.0.0
 | 
				
			||||||
    version: 3.0.0
 | 
					    version: 3.0.0
 | 
				
			||||||
 | 
					  '@types/node':
 | 
				
			||||||
 | 
					    specifier: ^18.15.12
 | 
				
			||||||
 | 
					    version: 18.15.12
 | 
				
			||||||
  autoprefixer:
 | 
					  autoprefixer:
 | 
				
			||||||
    specifier: ^10.4.7
 | 
					    specifier: ^10.4.7
 | 
				
			||||||
    version: 10.4.7(postcss@8.4.21)
 | 
					    version: 10.4.7(postcss@8.4.21)
 | 
				
			||||||
@ -60,7 +63,7 @@ devDependencies:
 | 
				
			|||||||
    version: 4.9.3
 | 
					    version: 4.9.3
 | 
				
			||||||
  vite:
 | 
					  vite:
 | 
				
			||||||
    specifier: ^4.2.0
 | 
					    specifier: ^4.2.0
 | 
				
			||||||
    version: 4.2.0
 | 
					    version: 4.2.0(@types/node@18.15.12)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
packages:
 | 
					packages:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -420,7 +423,7 @@ packages:
 | 
				
			|||||||
      magic-string: 0.29.0
 | 
					      magic-string: 0.29.0
 | 
				
			||||||
      svelte: 3.55.1
 | 
					      svelte: 3.55.1
 | 
				
			||||||
      svelte-hmr: 0.15.1(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)
 | 
					      vitefu: 0.2.4(vite@4.2.0)
 | 
				
			||||||
    transitivePeerDependencies:
 | 
					    transitivePeerDependencies:
 | 
				
			||||||
      - supports-color
 | 
					      - supports-color
 | 
				
			||||||
@ -430,6 +433,10 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==}
 | 
					    resolution: {integrity: sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==}
 | 
				
			||||||
    dev: true
 | 
					    dev: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /@types/node@18.15.12:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-Wha1UwsB3CYdqUm2PPzh/1gujGCNtWVUYF0mB00fJFoR4gTyWTDPjSm+zBF787Ahw8vSGgBja90MkgFwvB86Dg==}
 | 
				
			||||||
 | 
					    dev: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /@types/pug@2.0.6:
 | 
					  /@types/pug@2.0.6:
 | 
				
			||||||
    resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
 | 
					    resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
 | 
				
			||||||
    dev: true
 | 
					    dev: true
 | 
				
			||||||
@ -1483,7 +1490,7 @@ packages:
 | 
				
			|||||||
    dev: true
 | 
					    dev: true
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /vite@4.2.0:
 | 
					  /vite@4.2.0(@types/node@18.15.12):
 | 
				
			||||||
    resolution: {integrity: sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==}
 | 
					    resolution: {integrity: sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==}
 | 
				
			||||||
    engines: {node: ^14.18.0 || >=16.0.0}
 | 
					    engines: {node: ^14.18.0 || >=16.0.0}
 | 
				
			||||||
    hasBin: true
 | 
					    hasBin: true
 | 
				
			||||||
@ -1508,6 +1515,7 @@ packages:
 | 
				
			|||||||
      terser:
 | 
					      terser:
 | 
				
			||||||
        optional: true
 | 
					        optional: true
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      '@types/node': 18.15.12
 | 
				
			||||||
      esbuild: 0.17.15
 | 
					      esbuild: 0.17.15
 | 
				
			||||||
      postcss: 8.4.21
 | 
					      postcss: 8.4.21
 | 
				
			||||||
      resolve: 1.22.2
 | 
					      resolve: 1.22.2
 | 
				
			||||||
@ -1524,7 +1532,7 @@ packages:
 | 
				
			|||||||
      vite:
 | 
					      vite:
 | 
				
			||||||
        optional: true
 | 
					        optional: true
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      vite: 4.2.0
 | 
					      vite: 4.2.0(@types/node@18.15.12)
 | 
				
			||||||
    dev: true
 | 
					    dev: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /wrappy@1.0.2:
 | 
					  /wrappy@1.0.2:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										241
									
								
								src/App.svelte
									
									
									
									
									
								
							
							
						
						
									
										241
									
								
								src/App.svelte
									
									
									
									
									
								
							@ -1,84 +1,189 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { Button, ButtonGroup, Fileupload, Spinner } from 'flowbite-svelte';
 | 
					  import { Button, ButtonGroup, Spinner, Range, P } from 'flowbite-svelte';
 | 
				
			||||||
  import { applyFilter, getHistogram } from './lib/processor';
 | 
					  import {
 | 
				
			||||||
 | 
					    applyFilter,
 | 
				
			||||||
 | 
					    grayscale,
 | 
				
			||||||
 | 
					    threshold as threshold,
 | 
				
			||||||
 | 
					  } from './lib/engine/processor';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
    boxFilter,
 | 
					 | 
				
			||||||
    sharpening,
 | 
					 | 
				
			||||||
    type FilterFn,
 | 
					    type FilterFn,
 | 
				
			||||||
    edgeDetection,
 | 
					    sharpeningFilter,
 | 
				
			||||||
    sobel,
 | 
					    laplaceFilter,
 | 
				
			||||||
  } from './lib/filters';
 | 
					    sobelFilter,
 | 
				
			||||||
  import { Line } from 'svelte-chartjs';
 | 
					    gaussianBlurFilter,
 | 
				
			||||||
 | 
					    boxBlurFilter,
 | 
				
			||||||
 | 
					    removeColorChannelFilter,
 | 
				
			||||||
 | 
					    thresholdFilter,
 | 
				
			||||||
 | 
					    enhanceColorChannelFilter,
 | 
				
			||||||
 | 
					    setBrightnessFilter,
 | 
				
			||||||
 | 
					  } from './lib/engine/filters';
 | 
				
			||||||
  import 'chart.js/auto';
 | 
					  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 | null = demoImage;
 | 
				
			||||||
  let originalSrc: string;
 | 
					  let src: string | null = demoImage;
 | 
				
			||||||
  let filterSrc: string;
 | 
					 | 
				
			||||||
  let processing = false;
 | 
					  let processing = false;
 | 
				
			||||||
  let histogram: { red: number[]; green: number[]; blue: number[] };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: if (files) {
 | 
					  let thresholdValue = 128;
 | 
				
			||||||
    originalSrc = URL.createObjectURL(files[0]);
 | 
					  let brightnessValue = 1;
 | 
				
			||||||
    getHistogram(originalSrc).then((h) => (histogram = h));
 | 
					
 | 
				
			||||||
 | 
					  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) {
 | 
					  function handleUpload(e: CustomEvent) {
 | 
				
			||||||
    processing = true;
 | 
					    src = e.detail.src;
 | 
				
			||||||
    filterSrc = await applyFilter(originalSrc, filter);
 | 
					    originalSrc = e.detail.src;
 | 
				
			||||||
    processing = false;
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function undo() {
 | 
				
			||||||
 | 
					    filters = filters.slice(0, -1);
 | 
				
			||||||
 | 
					    src = filters[filters.length - 1]?.src || originalSrc;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function reset() {
 | 
				
			||||||
 | 
					    filters = [];
 | 
				
			||||||
 | 
					    src = originalSrc;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="p-4 space-y-2 w-screen">
 | 
					<div class="p-4 space-y-2 h-screen flex flex-col">
 | 
				
			||||||
  <Fileupload bind:files />
 | 
					  <div class="w-max">
 | 
				
			||||||
 | 
					    <ImageUpload on:upload={handleUpload} />
 | 
				
			||||||
  <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>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="w-1/2">
 | 
					  {#if src}
 | 
				
			||||||
    <Line
 | 
					    <P>Filters</P>
 | 
				
			||||||
      data={{
 | 
					    <div class="flex">
 | 
				
			||||||
        labels: new Array(256).fill(0).map((_, i) => i),
 | 
					      <ButtonGroup>
 | 
				
			||||||
        datasets: [
 | 
					        <Button on:click={() => apply('Box blur', boxBlurFilter)}
 | 
				
			||||||
          {
 | 
					          >Box blur</Button
 | 
				
			||||||
            fill: true,
 | 
					        >
 | 
				
			||||||
            label: 'Red',
 | 
					        <Button on:click={() => apply('Gaussian blur', gaussianBlurFilter)}>
 | 
				
			||||||
            borderColor: 'red',
 | 
					          Gaussian blur
 | 
				
			||||||
            backgroundColor: 'rgba(255, 0, 0, 0.2)',
 | 
					        </Button>
 | 
				
			||||||
            data: histogram?.red,
 | 
					        <Button on:click={() => apply('Laplace', laplaceFilter)}>Laplace</Button
 | 
				
			||||||
          },
 | 
					        >
 | 
				
			||||||
          {
 | 
					        <Button on:click={() => apply('Sobel', sobelFilter)}>Sobel</Button>
 | 
				
			||||||
            fill: true,
 | 
					        <Button on:click={() => apply('Sharpening', sharpeningFilter)}>
 | 
				
			||||||
            label: 'Green',
 | 
					          Sharpening
 | 
				
			||||||
            borderColor: 'green',
 | 
					        </Button>
 | 
				
			||||||
            backgroundColor: 'rgba(0, 255, 0, 0.2)',
 | 
					        <Button on:click={() => apply('Grayscale', grayscale)}>
 | 
				
			||||||
            data: histogram?.green,
 | 
					          Grayscale
 | 
				
			||||||
          },
 | 
					        </Button>
 | 
				
			||||||
          {
 | 
					      </ButtonGroup>
 | 
				
			||||||
            fill: true,
 | 
					
 | 
				
			||||||
            label: 'Blue',
 | 
					      <div class="grow" />
 | 
				
			||||||
            borderColor: 'blue',
 | 
					
 | 
				
			||||||
            backgroundColor: 'rgba(0, 0, 255, 0.2)',
 | 
					      <div class="gap-y-2">
 | 
				
			||||||
            data: histogram?.blue,
 | 
					        <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>
 | 
				
			||||||
</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