2026-03-08 22:43:34 +00:00
|
|
|
<script lang="ts">
|
|
|
|
|
import Dither from 'canvas-dither'
|
|
|
|
|
import ErrorMessage from './ErrorMessage.svelte';
|
|
|
|
|
import { onMount } from 'svelte';
|
|
|
|
|
|
|
|
|
|
const DITHERING_ALGORITHMS = {
|
2026-03-09 22:18:35 +00:00
|
|
|
atkinson: (imageData: ImageData) => Dither.atkinson(imageData),
|
|
|
|
|
bayer: (imageData: ImageData) => Dither.bayer(imageData, 170),
|
|
|
|
|
floydSteinberg: (imageData: ImageData) => Dither.floydsteinberg(imageData),
|
|
|
|
|
threshold: (imageData: ImageData) => Dither.threshold(imageData, 2),
|
2026-03-08 22:43:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let canvas: HTMLCanvasElement;
|
|
|
|
|
let offscreenCanvas: OffscreenCanvas;
|
|
|
|
|
let ctx = $derived(canvas.getContext('2d'))
|
|
|
|
|
let offscreenCtx: OffscreenCanvasRenderingContext2D;
|
2026-03-09 21:05:49 +00:00
|
|
|
let selectedImage: HTMLImageElement | null = $state(null)
|
2026-03-08 22:43:34 +00:00
|
|
|
let errorMessage = $state('')
|
2026-03-09 21:05:49 +00:00
|
|
|
let selectedAlgorithm = $state('atkinson')
|
|
|
|
|
|
|
|
|
|
let maxImageWidth = $state(0)
|
|
|
|
|
let imageHeightRatio = $state(0)
|
|
|
|
|
let imageWidthPc = $state(100)
|
2026-03-08 22:43:34 +00:00
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
offscreenCanvas = new OffscreenCanvas(1000, 1000)
|
|
|
|
|
offscreenCtx = offscreenCanvas.getContext('2d')
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-09 21:05:49 +00:00
|
|
|
$effect(() => {
|
|
|
|
|
if (!selectedImage) {
|
|
|
|
|
console.log(`Selected Image is null, terminating effect...`)
|
|
|
|
|
return;
|
2026-03-08 22:43:34 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-09 21:05:49 +00:00
|
|
|
maxImageWidth = selectedImage.width
|
|
|
|
|
imageHeightRatio = selectedImage.height / selectedImage.width
|
|
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
|
|
|
|
|
|
|
|
canvas.width = maxImageWidth * (imageWidthPc/100)
|
|
|
|
|
canvas.height = imageHeightRatio * canvas.width
|
|
|
|
|
ctx.drawImage(selectedImage, 0, 0, canvas.width, canvas.height)
|
|
|
|
|
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
|
|
|
ctx.putImageData(DITHERING_ALGORITHMS[selectedAlgorithm](imageData), 0, 0)
|
|
|
|
|
})
|
2026-03-08 22:43:34 +00:00
|
|
|
|
|
|
|
|
const handleImageChange = async (event: any) => {
|
|
|
|
|
const file = event.target.files[0];
|
|
|
|
|
if (!file) {
|
|
|
|
|
errorMessage = "No file selected, try again"
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 21:05:49 +00:00
|
|
|
const imgObjectUrl = URL.createObjectURL(file);
|
|
|
|
|
selectedImage = await createImage(imgObjectUrl);
|
2026-03-08 22:43:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createImage(objectUrl: string): Promise<HTMLImageElement> {
|
|
|
|
|
return new Promise<HTMLImageElement>((resolve) => {
|
|
|
|
|
const img = new Image();
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
resolve(img);
|
|
|
|
|
}
|
|
|
|
|
img.src = objectUrl;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveImage() {
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
link.download = 'dithered-image.png';
|
|
|
|
|
link.href = canvas.toDataURL();
|
|
|
|
|
link.click();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
2026-03-09 21:05:49 +00:00
|
|
|
<ErrorMessage errorMessage={errorMessage} />
|
2026-03-08 22:43:34 +00:00
|
|
|
|
|
|
|
|
<h2 class="page-dither__subtitle">1: Select a file</h2>
|
|
|
|
|
|
|
|
|
|
<form class="file-form">
|
|
|
|
|
<label for="file"> Select a file<br />
|
|
|
|
|
<input type="file" name="file" id="file" onchange={handleImageChange} accept="image/*" >
|
|
|
|
|
</label>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<h2 class="page-dither__subtitle">2: Select a dithering algorithm</h2>
|
|
|
|
|
|
|
|
|
|
<ul class="algorithms">
|
2026-03-09 21:05:49 +00:00
|
|
|
{#each Object.keys(DITHERING_ALGORITHMS) as key}
|
2026-03-08 22:43:34 +00:00
|
|
|
<li class="algorithms__item">
|
2026-03-09 21:05:49 +00:00
|
|
|
<button class="algorithms__button" onclick={() => selectedAlgorithm = key}>{key}</button>
|
2026-03-08 22:43:34 +00:00
|
|
|
</li>
|
|
|
|
|
{/each}
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="canvas-container">
|
|
|
|
|
<canvas id="photo-canvas" height="250" bind:this={canvas} onload={() => ctx = canvas.getContext('2d')}></canvas>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-09 21:05:49 +00:00
|
|
|
<h2 class="page-dither__subtitle">
|
|
|
|
|
3: Remix
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
<form>
|
|
|
|
|
<label for="resolution">
|
|
|
|
|
Image Ratio
|
|
|
|
|
<input type="range" min="1" max="100" step="5" bind:value={imageWidthPc} width="100%"/>
|
|
|
|
|
</label>
|
|
|
|
|
<p>{maxImageWidth * imageWidthPc / 100}</p>
|
|
|
|
|
</form>
|
2026-03-08 22:43:34 +00:00
|
|
|
|
|
|
|
|
<h2 class="page-dither__subtitle">
|
2026-03-09 21:05:49 +00:00
|
|
|
4: Save the file
|
2026-03-08 22:43:34 +00:00
|
|
|
</h2>
|
|
|
|
|
<button onclick={saveImage}>Save Image</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.algorithms {
|
|
|
|
|
list-style: none;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: auto;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.algorithms__button {
|
|
|
|
|
box-shadow: 5px 2px blue;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: var()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.canvas-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
width: auto;
|
|
|
|
|
padding: 14px;
|
|
|
|
|
height: min-content
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#photo-canvas {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</style>
|