thomaswilson-sveltekit/src/routes/dither/+page.svelte

159 lines
3.9 KiB
Svelte
Raw Normal View History

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 = {
atkinson: (imageData) => Dither.atkinson(imageData),
bayer: (imageData) => Dither.bayer(imageData, 170),
floydSteinberg: (imageData) => Dither.floydsteinberg(imageData),
threshold: (imageData) => Dither.threshold(imageData, 2),
}
let canvas: HTMLCanvasElement;
let offscreenCanvas: OffscreenCanvas;
let ctx = $derived(canvas.getContext('2d'))
let offscreenCtx: OffscreenCanvasRenderingContext2D;
let imgObjectUrl = $state('')
let errorMessage = $state('')
onMount(() => {
offscreenCanvas = new OffscreenCanvas(1000, 1000)
offscreenCtx = offscreenCanvas.getContext('2d')
})
const doesCtxExist = () => {
if (!ctx) {
errorMessage = "Problem finding context for on-screen canvas"
return false
}
return true;
}
const handleImageChange = async (event: any) => {
const file = event.target.files[0];
if (!file) {
errorMessage = "No file selected, try again"
return
}
imgObjectUrl = URL.createObjectURL(file);
const image = await createImage(imgObjectUrl);
// Change the sizes of canvases
canvas.width = image.width
canvas.height = image.height
offscreenCanvas.height = image.height
offscreenCanvas.width = image.width
// Now draw the images
drawImage(image)
}
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;
});
}
const clearCanvases = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
}
const drawImage = (theImage: HTMLImageElement) => {
if (!doesCtxExist()) return
clearCanvases()
ctx.drawImage(theImage, 0, 0, canvas.width, canvas.height);
offscreenCtx.drawImage(theImage, offscreenCanvas.width, offscreenCanvas.height)
}
function applyDitheringAlgorithm(algorithm: (imageData: ImageData) => ImageData) {
return async () => {
const freshImage = await createImage(imgObjectUrl);
drawImage(freshImage)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const ditheredImageData = algorithm(imageData);
ctx.putImageData(ditheredImageData, 0, 0);
}
}
function saveImage() {
if (!canvas) return;
const link = document.createElement('a');
link.download = 'dithered-image.png';
link.href = canvas.toDataURL();
link.click();
}
</script>
<ErrorMessage message={errorMessage} />
<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">
{#each Object.entries(DITHERING_ALGORITHMS) as [key, value]}
<li class="algorithms__item">
<button class="algorithms__button" onclick={applyDitheringAlgorithm(value)}>{key}</button>
</li>
{/each}
</ul>
<div class="canvas-container">
<canvas id="photo-canvas" height="250" bind:this={canvas} onload={() => ctx = canvas.getContext('2d')}></canvas>
</div>
<h2 class="page-dither__subtitle">
3: Save the file
</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>