From cd0d2a9572e6df5ce17541829a407e923dd51be2 Mon Sep 17 00:00:00 2001 From: daylily Date: Thu, 3 Apr 2025 02:36:01 -0400 Subject: [PATCH] Refactor codebase - Support file drag-and-drop - Fix wrong usage of tooltips - Add tailwind shorthands for layouting - Complete ARIA annotating --- src/App.svelte | 6 +- src/app.css | 24 +++- .../{Infotip.svelte => MoreInfo.svelte} | 9 +- src/lib/contexts/files.svelte.ts | 19 +++ src/lib/contexts/image.svelte.ts | 41 +++++++ .../{bitmap.svelte.ts => rendered.svelte.ts} | 47 ++++--- src/lib/image/scaler.ts | 4 + src/lib/image/transform.ts | 7 +- src/lib/layouts/Footer.svelte | 14 +-- src/lib/layouts/Main.svelte | 25 ++-- src/lib/layouts/Unsupported.svelte | 23 ++-- src/lib/layouts/connect/ConnectButton.svelte | 6 +- src/lib/layouts/connect/ConnectSection.svelte | 12 +- src/lib/layouts/edit/EditSection.svelte | 15 +-- .../controls/BackgroundColorSlider.svelte | 23 ++-- .../edit/controls/ControlsSection.svelte | 30 ++--- .../controls/conversion/BiasSlider.svelte | 32 +++-- .../controls/conversion/ContrastSlider.svelte | 30 +++-- .../conversion/dither/DitherControls.svelte | 16 +-- .../conversion/dither/DitherSwitch.svelte | 20 ++- .../dither/DitheringKernelDropdown.svelte | 26 ++-- .../dimensions/AspectRatioAlert.svelte | 2 +- .../dimensions/ScaleModeToggleGroup.svelte | 34 +++--- .../dimensions/TransformControls.svelte | 115 +++++++----------- .../layouts/edit/preview/FileSelect.svelte | 25 ++-- .../edit/preview/PreviewCanvas1x.svelte | 42 +++++++ .../edit/preview/PreviewCanvas2x.svelte | 71 +++++++++++ .../edit/preview/PreviewCanvases.svelte | 72 ----------- .../edit/preview/PreviewSection.svelte | 18 ++- src/lib/layouts/edit/preview/common.svelte.ts | 22 ++++ src/lib/layouts/write/WriteButton.svelte | 13 +- src/lib/layouts/write/WriteSection.svelte | 28 ++--- 32 files changed, 479 insertions(+), 392 deletions(-) rename src/lib/components/{Infotip.svelte => MoreInfo.svelte} (58%) create mode 100644 src/lib/contexts/files.svelte.ts create mode 100644 src/lib/contexts/image.svelte.ts rename src/lib/contexts/{bitmap.svelte.ts => rendered.svelte.ts} (56%) create mode 100644 src/lib/layouts/edit/preview/PreviewCanvas1x.svelte create mode 100644 src/lib/layouts/edit/preview/PreviewCanvas2x.svelte delete mode 100644 src/lib/layouts/edit/preview/PreviewCanvases.svelte create mode 100644 src/lib/layouts/edit/preview/common.svelte.ts diff --git a/src/App.svelte b/src/App.svelte index 3f8a50c..b94e72c 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -12,11 +12,11 @@ -
+
{#if unsupported} - + {:else} -
+
{/if}
diff --git a/src/app.css b/src/app.css index 4230920..2062e5f 100644 --- a/src/app.css +++ b/src/app.css @@ -69,10 +69,30 @@ @apply bg-background text-foreground; font-family: IBM Plex Sans Variable, IBM Plex Sans, Fira Sans, sans-serif; } + + ::selection { + @apply text-muted bg-muted-foreground; + } + + a { + @apply underline; + } } @layer utilities { - .multimodal { - @apply flex gap-1 items-end; + .stack { + @apply flex flex-col; + } + + .stack-h { + @apply flex; + } + + .row { + @apply flex items-center; + } + + .col { + @apply flex flex-col items-center; } } diff --git a/src/lib/components/Infotip.svelte b/src/lib/components/MoreInfo.svelte similarity index 58% rename from src/lib/components/Infotip.svelte rename to src/lib/components/MoreInfo.svelte index 006e69a..888b3a8 100644 --- a/src/lib/components/Infotip.svelte +++ b/src/lib/components/MoreInfo.svelte @@ -2,15 +2,12 @@ import IconInfo from '~icons/material-symbols/info' import * as Popover from '$lib/components/ui/popover' - import { Label } from '$lib/components/ui/label' - + + + diff --git a/src/lib/contexts/files.svelte.ts b/src/lib/contexts/files.svelte.ts new file mode 100644 index 0000000..8a9a705 --- /dev/null +++ b/src/lib/contexts/files.svelte.ts @@ -0,0 +1,19 @@ +import { getContext, setContext } from 'svelte' + +export interface FilesContext { + files: FileList +} + +export const FilesContextToken = Symbol('files') + +export function getFilesContext(): FilesContext { + return getContext(FilesContextToken) +} + +export function createFilesContext(): FilesContext { + const ctx: FilesContext = $state({ + files: new DataTransfer().files, + }) + + return setContext(FilesContextToken, ctx) +} diff --git a/src/lib/contexts/image.svelte.ts b/src/lib/contexts/image.svelte.ts new file mode 100644 index 0000000..d72a2f2 --- /dev/null +++ b/src/lib/contexts/image.svelte.ts @@ -0,0 +1,41 @@ +import { getContext, setContext } from 'svelte' +import type { FilesContext } from './files.svelte' +import { toast } from 'svelte-sonner' + +export interface ImageContext { + image: ImageBitmap | null +} + +export const ImageContextToken = Symbol('image') + +export function getImageContext(): Readonly { + return getContext(ImageContextToken) +} + +export function createImageContext(filesCtx: FilesContext): Readonly { + const ctx: ImageContext = $state({ + image: null, + }) + + async function updateImageBitmap() { + const files = filesCtx.files + + if (files === null || files.length < 1) { + ctx.image = null + return + } + + try { + ctx.image = await createImageBitmap(files[0]) + } catch (e) { + toast.error(`Error loading image file: ${e}`) + ctx.image = null + } + } + + $effect(() => { + updateImageBitmap() + }) + + return setContext(ImageContextToken, ctx) +} diff --git a/src/lib/contexts/bitmap.svelte.ts b/src/lib/contexts/rendered.svelte.ts similarity index 56% rename from src/lib/contexts/bitmap.svelte.ts rename to src/lib/contexts/rendered.svelte.ts index ca17aad..2a18110 100644 --- a/src/lib/contexts/bitmap.svelte.ts +++ b/src/lib/contexts/rendered.svelte.ts @@ -1,30 +1,30 @@ import { getContext, setContext } from 'svelte' -import type { ConversionConfig } from './config.svelte' -import { Quantizer } from '$lib/image/quantizer' +import type { ImageContext } from './image.svelte' import { withTransform } from '$lib/image/transform' +import type { ConversionConfig } from './config.svelte' import { Scaler } from '$lib/image/scaler' +import { Quantizer } from '$lib/image/quantizer' -const scaler = new Scaler(200, 200) - -export interface BitmapContext { - image: ImageBitmap | null +export interface RenderedContext { rendered: number[] | null } -export const BitmapContextToken = Symbol('bitmap') +export const RenderedContextToken = Symbol('rendered') -export function getBitmapContext(): BitmapContext { - return getContext(BitmapContextToken) +export function getRenderedContext(): Readonly { + return getContext(RenderedContextToken) } -export function createBitmapContext(config: ConversionConfig): BitmapContext { - const ctx: BitmapContext = $state({ - image: null, +const scaler = new Scaler(200, 200) + +export function createRenderedContext( + imageCtx: Readonly, + config: ConversionConfig +): Readonly { + const ctx: RenderedContext = $state({ rendered: null, }) - const canvas = new OffscreenCanvas(200, 200) - const quantizer = $derived( new Quantizer({ ditheringKernel: config.ditheringKernel, @@ -33,31 +33,30 @@ export function createBitmapContext(config: ConversionConfig): BitmapContext { }) ) + const canvas = new OffscreenCanvas(200, 200) + const canvasCtx = canvas.getContext('2d', { + willReadFrequently: true, + })! + $effect(() => { - const bitmap = ctx.image + const bitmap = imageCtx.image if (bitmap === null) { ctx.rendered = null return } - const canvasCtx = canvas.getContext('2d', { - willReadFrequently: true, - })! - withTransform(canvasCtx, config.transform, () => { - const nonNullBitmap = bitmap - const bg = config.backgroundColor canvasCtx.fillStyle = `rgb(${bg} ${bg} ${bg})` canvasCtx.fillRect(0, 0, 200, 200) - const { dx, dy, dWidth, dHeight } = scaler[config.scaleMode](nonNullBitmap) - canvasCtx.drawImage(nonNullBitmap, dx, dy, dWidth, dHeight) + const { dx, dy, dWidth, dHeight } = scaler.scale(config.scaleMode, bitmap) + canvasCtx.drawImage(bitmap, dx, dy, dWidth, dHeight) }) const quantizedData = quantizer.reduce(canvasCtx) ctx.rendered = quantizedData }) - return setContext(BitmapContextToken, ctx) + return setContext(RenderedContextToken, ctx) } diff --git a/src/lib/image/scaler.ts b/src/lib/image/scaler.ts index 79fb813..e1d92bd 100644 --- a/src/lib/image/scaler.ts +++ b/src/lib/image/scaler.ts @@ -72,4 +72,8 @@ export class Scaler { dHeight: this.canvasHeight, } } + + scale(mode: ScaleMode, image: ImageBitmap): DrawParameters { + return this[mode](image) + } } diff --git a/src/lib/image/transform.ts b/src/lib/image/transform.ts index fc79bfb..1988c13 100644 --- a/src/lib/image/transform.ts +++ b/src/lib/image/transform.ts @@ -1,5 +1,6 @@ export type Rotation = 0 | 90 | 180 | 270 export type Side = 'obverse' | 'reverse' +export type Operation = 'cw' | 'ccw' | 'h' | 'v' /** * A transform of a square image is represented by first rotating it by a multiple of 90 degrees, and then optionally @@ -32,11 +33,15 @@ export class Transform { const newAngle = (this.rotation + 180) % 360 return new Transform(newSide, newAngle as Rotation) } + + op(op: Operation): Transform { + return this[op]() + } } export type Context2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D -export function withCtx(ctx: Context2D, pre: () => void, action: () => T): T { +function withCtx(ctx: Context2D, pre: () => void, action: () => T): T { ctx.save() try { pre() diff --git a/src/lib/layouts/Footer.svelte b/src/lib/layouts/Footer.svelte index 825f10a..5996457 100644 --- a/src/lib/layouts/Footer.svelte +++ b/src/lib/layouts/Footer.svelte @@ -1,20 +1,20 @@ -