Multiple refactors

- Use aria-labelledby instead of aria-label where possible
- Replace magic numbers with explicitly defined constants
- Add input textboxes for values previously only changed with sliders
This commit is contained in:
daylily
2025-04-03 18:06:00 -04:00
parent 09d5022b1d
commit ccdb91a243
22 changed files with 168 additions and 49 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
<ModeWatcher /> <ModeWatcher />
<Toaster position="bottom-center" duration={2000} /> <Toaster position="bottom-center" duration={2000} />
<div class="max-w-screen-xl min-h-screen m-auto p-8 stack gap-4"> <div class="max-w-screen-xl min-h-dvh m-auto p-8 stack gap-4">
{#if unsupported} {#if unsupported}
<Unsupported /> <Unsupported />
{:else} {:else}
@@ -0,0 +1,43 @@
<script lang="ts">
import { cn, showNumber } from '$lib/utils'
import type { HTMLAttributes } from 'svelte/elements'
interface Props extends Omit<HTMLAttributes<HTMLInputElement>, 'type' | 'inputmode' | 'onchange' | 'value'> {
min: number
max: number
value: number
onchange: (value: number) => void
}
const { min, max, value, onchange, class: classNames, ...restProps }: Props = $props()
const size = $derived(Math.max(String(min).length, String(max).length))
</script>
<input
type="text"
inputmode="numeric"
class={cn(
'bg-muted rounded-md focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-1 focus-visible:ring-offset-2 px-1 mx-0.5',
classNames,
)}
{size}
maxlength={size}
style:field-sizing="content"
value={showNumber(value)}
onchange={e => {
let n = Number(e.currentTarget.value)
if (isNaN(n)) {
e.currentTarget.value = showNumber(value)
return
}
if (n < min) n = min
if (n > max) n = max
e.currentTarget.value = showNumber(n)
onchange(n)
}}
{...restProps}
/>
+10 -3
View File
@@ -1,15 +1,22 @@
<script lang="ts"> <script lang="ts">
import IconInfo from '~icons/material-symbols/info' import type { PopoverTriggerProps } from 'bits-ui'
import IconInfo from '~icons/material-symbols/info'
import * as Popover from '$lib/components/ui/popover' import * as Popover from '$lib/components/ui/popover'
interface Props extends PopoverTriggerProps {}
const { children, ...restProps }: Props = $props()
</script> </script>
<Popover.Root> <Popover.Root>
<Popover.Trigger aria-label="More info"> <Popover.Trigger aria-label="More info" {...restProps}>
<IconInfo class="text-muted-foreground text-sm" aria-hidden /> <IconInfo class="text-muted-foreground text-sm" aria-hidden />
</Popover.Trigger> </Popover.Trigger>
<Popover.Content class="text-sm"> <Popover.Content class="text-sm">
<slot /> {#if children}
{@render children()}
{/if}
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>
+10
View File
@@ -0,0 +1,10 @@
export const INKCLIP_VID = 0x1209
export const INKCLIP_PID = 0xc9c9
export const INKCLIP_WIDTH = 200 // px
export const INKCLIP_HEIGHT = 200 // px
export const INKCLIP_ASPECT_RATIO = INKCLIP_WIDTH / INKCLIP_HEIGHT
export const BYTES_IN_A_ROW = INKCLIP_WIDTH / 8
export const WRITE_TIME = 2000 // ms
+2 -1
View File
@@ -1,8 +1,9 @@
import { INKCLIP_PID, INKCLIP_VID } from '$lib/constants'
import { getContext, setContext } from 'svelte' import { getContext, setContext } from 'svelte'
import { toast } from 'svelte-sonner' import { toast } from 'svelte-sonner'
function isInkclip(dev: HIDDevice) { function isInkclip(dev: HIDDevice) {
return dev.vendorId == 0xc0de && dev.productId == 0xcafe return dev.vendorId == INKCLIP_VID && dev.productId == INKCLIP_PID
} }
export interface DeviceContext { export interface DeviceContext {
+4 -3
View File
@@ -4,6 +4,7 @@ import { withTransform } from '$lib/image/transform'
import type { ConversionConfig } from './config.svelte' import type { ConversionConfig } from './config.svelte'
import { Scaler } from '$lib/image/scaler' import { Scaler } from '$lib/image/scaler'
import { Quantizer } from '$lib/image/quantizer' import { Quantizer } from '$lib/image/quantizer'
import { INKCLIP_HEIGHT, INKCLIP_WIDTH } from '$lib/constants'
export interface RenderedContext { export interface RenderedContext {
rendered: number[] | null rendered: number[] | null
@@ -15,7 +16,7 @@ export function getRenderedContext(): Readonly<RenderedContext> {
return getContext(RenderedContextToken) return getContext(RenderedContextToken)
} }
const scaler = new Scaler(200, 200) const scaler = new Scaler(INKCLIP_WIDTH, INKCLIP_HEIGHT)
export function createRenderedContext( export function createRenderedContext(
imageCtx: Readonly<ImageContext>, imageCtx: Readonly<ImageContext>,
@@ -33,7 +34,7 @@ export function createRenderedContext(
}) })
) )
const canvas = new OffscreenCanvas(200, 200) const canvas = new OffscreenCanvas(INKCLIP_WIDTH, INKCLIP_HEIGHT)
const canvasCtx = canvas.getContext('2d', { const canvasCtx = canvas.getContext('2d', {
willReadFrequently: true, willReadFrequently: true,
})! })!
@@ -49,7 +50,7 @@ export function createRenderedContext(
withTransform(canvasCtx, config.transform, () => { withTransform(canvasCtx, config.transform, () => {
const bg = config.backgroundColor const bg = config.backgroundColor
canvasCtx.fillStyle = `rgb(${bg} ${bg} ${bg})` canvasCtx.fillStyle = `rgb(${bg} ${bg} ${bg})`
canvasCtx.fillRect(0, 0, 200, 200) canvasCtx.fillRect(0, 0, INKCLIP_WIDTH, INKCLIP_HEIGHT)
const { dx, dy, dWidth, dHeight } = scaler.scale(config.scaleMode, bitmap) const { dx, dy, dWidth, dHeight } = scaler.scale(config.scaleMode, bitmap)
canvasCtx.drawImage(bitmap, dx, dy, dWidth, dHeight) canvasCtx.drawImage(bitmap, dx, dy, dWidth, dHeight)
}) })
+7 -2
View File
@@ -1,3 +1,5 @@
import { INKCLIP_HEIGHT, INKCLIP_WIDTH } from '$lib/constants'
export type Rotation = 0 | 90 | 180 | 270 export type Rotation = 0 | 90 | 180 | 270
export type Side = 'obverse' | 'reverse' export type Side = 'obverse' | 'reverse'
export type Operation = 'cw' | 'ccw' | 'h' | 'v' export type Operation = 'cw' | 'ccw' | 'h' | 'v'
@@ -52,15 +54,18 @@ function withCtx<T>(ctx: Context2D, pre: () => void, action: () => T): T {
} }
export function withTransform<T>(ctx: Context2D, transform: Transform, action: () => T): T { export function withTransform<T>(ctx: Context2D, transform: Transform, action: () => T): T {
const centerX = INKCLIP_WIDTH / 2
const centerY = INKCLIP_HEIGHT / 2
return withCtx( return withCtx(
ctx, ctx,
() => { () => {
ctx.translate(100, 100) ctx.translate(centerX, centerY)
if (transform.side === 'reverse') { if (transform.side === 'reverse') {
ctx.scale(-1, 1) ctx.scale(-1, 1)
} }
ctx.rotate((transform.rotation / 180) * Math.PI) ctx.rotate((transform.rotation / 180) * Math.PI)
ctx.translate(-100, -100) ctx.translate(-centerX, -centerY)
}, },
action action
) )
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button' import { Button } from '$lib/components/ui/button'
import { INKCLIP_PID, INKCLIP_VID } from '$lib/constants'
import { getDeviceContext } from '$lib/contexts/device.svelte' import { getDeviceContext } from '$lib/contexts/device.svelte'
const deviceCtx = getDeviceContext() const deviceCtx = getDeviceContext()
@@ -9,8 +10,8 @@
const devs = await navigator.hid.requestDevice({ const devs = await navigator.hid.requestDevice({
filters: [ filters: [
{ {
vendorId: 0xc0de, vendorId: INKCLIP_VID,
productId: 0xcafe, productId: INKCLIP_PID,
}, },
], ],
}) })
@@ -2,6 +2,7 @@
import { Label } from '$lib/components/ui/label' import { Label } from '$lib/components/ui/label'
import { Slider } from '$lib/components/ui/slider' import { Slider } from '$lib/components/ui/slider'
import MoreInfo from '$lib/components/MoreInfo.svelte' import MoreInfo from '$lib/components/MoreInfo.svelte'
import ImplicitNumericInput from '$lib/components/ImplicitNumericInput.svelte'
import { getConversionConfig } from '$lib/contexts/config.svelte' import { getConversionConfig } from '$lib/contexts/config.svelte'
@@ -10,10 +11,18 @@
let value = $derived(config.backgroundColor) let value = $derived(config.backgroundColor)
</script> </script>
<div class="stack gap-4" role="group" aria-label="Background color"> <div class="stack gap-4" role="group" aria-labelledby="background-color-input-label">
<div class="row gap-1 text-sm"> <div class="row gap-1 text-sm">
<Label id="background-color-input-label">Background Color</Label> <Label id="background-color-input-label">Background Color</Label>
<span class="font-normal text-muted-foreground"> = {value} </span> <span class="font-normal text-muted-foreground">
=<ImplicitNumericInput
min={0}
max={0xff}
{value}
onchange={v => (config.bias = value = v)}
aria-labelledby="background-color-input-label"
/>
</span>
<MoreInfo>The color used for transparent pixels.</MoreInfo> <MoreInfo>The color used for transparent pixels.</MoreInfo>
</div> </div>
@@ -22,7 +31,7 @@
bind:value bind:value
onValueCommit={() => (config.backgroundColor = value)} onValueCommit={() => (config.backgroundColor = value)}
min={0} min={0}
max={255} max={0xff}
step={1} step={1}
aria-labelledby="background-color-input-label" aria-labelledby="background-color-input-label"
/> />
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { DEFAULT_DITHERING_KERNEL } from '$lib/image/quantizer'
import { Transform } from '$lib/image/transform' import { Transform } from '$lib/image/transform'
import { Button } from '$lib/components/ui/button' import { Button } from '$lib/components/ui/button'
@@ -15,6 +14,8 @@
import { getConversionConfig } from '$lib/contexts/config.svelte' import { getConversionConfig } from '$lib/contexts/config.svelte'
import { getImageContext } from '$lib/contexts/image.svelte' import { getImageContext } from '$lib/contexts/image.svelte'
import { INKCLIP_HEIGHT, INKCLIP_WIDTH } from '$lib/constants'
import { DEFAULT_DITHERING_KERNEL } from '$lib/image/quantizer'
const imageCtx = getImageContext() const imageCtx = getImageContext()
const config = getConversionConfig() const config = getConversionConfig()
@@ -23,13 +24,13 @@
function imageNonSquare() { function imageNonSquare() {
if (imageCtx.image === null) return false if (imageCtx.image === null) return false
return imageCtx.image.height !== imageCtx.image.width return imageCtx.image.height * INKCLIP_WIDTH !== imageCtx.image.width * INKCLIP_HEIGHT
} }
function restoreDefaultImageSettings() { function restoreDefaultImageSettings() {
config.scaleMode = 'fit' config.scaleMode = 'fit'
config.transform = new Transform() config.transform = new Transform()
config.backgroundColor = 255 config.backgroundColor = 0xff
config.ditheringKernel = DEFAULT_DITHERING_KERNEL config.ditheringKernel = DEFAULT_DITHERING_KERNEL
config.contrast = 0 config.contrast = 0
config.bias = 0 config.bias = 0
@@ -2,6 +2,7 @@
import { Label } from '$lib/components/ui/label' import { Label } from '$lib/components/ui/label'
import { Slider } from '$lib/components/ui/slider' import { Slider } from '$lib/components/ui/slider'
import MoreInfo from '$lib/components/MoreInfo.svelte' import MoreInfo from '$lib/components/MoreInfo.svelte'
import ImplicitNumericInput from '$lib/components/ImplicitNumericInput.svelte'
import { getConversionConfig } from '$lib/contexts/config.svelte' import { getConversionConfig } from '$lib/contexts/config.svelte'
@@ -10,10 +11,18 @@
let value = $derived(Math.round(config.bias * 100)) let value = $derived(Math.round(config.bias * 100))
</script> </script>
<div class="stack gap-4" role="group" aria-label="Bias"> <div class="stack gap-4" role="group" aria-labelledby="bias-input-label">
<div class="row gap-1 text-sm"> <div class="row gap-1 text-sm">
<Label id="bias-input-label">Bias</Label> <Label id="bias-input-label">Bias</Label>
<span class="font-normal text-muted-foreground"> = {value}% </span> <span class="font-normal text-muted-foreground">
=<ImplicitNumericInput
min={-100}
max={100}
{value}
onchange={v => (config.bias = (value = v) / 100)}
aria-labelledby="bias-input-label"
/>%
</span>
<MoreInfo> <MoreInfo>
Whether the conversion algorithm should bias the entire image towards white (negative) or black (positive). Whether the conversion algorithm should bias the entire image towards white (negative) or black (positive).
</MoreInfo> </MoreInfo>
@@ -2,6 +2,7 @@
import { Label } from '$lib/components/ui/label' import { Label } from '$lib/components/ui/label'
import { Slider } from '$lib/components/ui/slider' import { Slider } from '$lib/components/ui/slider'
import MoreInfo from '$lib/components/MoreInfo.svelte' import MoreInfo from '$lib/components/MoreInfo.svelte'
import ImplicitNumericInput from '$lib/components/ImplicitNumericInput.svelte'
import { getConversionConfig } from '$lib/contexts/config.svelte' import { getConversionConfig } from '$lib/contexts/config.svelte'
@@ -10,10 +11,18 @@
let value = $derived(Math.round(config.contrast * 100)) let value = $derived(Math.round(config.contrast * 100))
</script> </script>
<div class="stack gap-4" role="group" aria-label="Contrast"> <div class="stack gap-4" role="group" aria-labelledby="contrast-input-label">
<div class="row gap-1 text-sm"> <div class="row gap-1 text-sm">
<Label id="contrast-input-label">Contrast</Label> <Label id="contrast-input-label">Contrast</Label>
<span class="font-normal text-muted-foreground"> = {value}% </span> <span class="font-normal text-muted-foreground">
=<ImplicitNumericInput
min={0}
max={100}
{value}
onchange={v => (config.contrast = (value = v) / 100)}
aria-labelledby="contrast-input-label"
/>%
</span>
<MoreInfo> <MoreInfo>
How eager the conversion algorithm should push colors towards the two ends (black and white) of the grayscale. How eager the conversion algorithm should push colors towards the two ends (black and white) of the grayscale.
</MoreInfo> </MoreInfo>
@@ -11,7 +11,7 @@
let { checked, onchange }: Props = $props() let { checked, onchange }: Props = $props()
</script> </script>
<div class="stack gap-2" role="group" aria-label="Dither"> <div class="stack gap-2" role="group" aria-labelledby="dither-switch-label">
<div class="row gap-1"> <div class="row gap-1">
<Label id="dither-switch-label">Dither</Label> <Label id="dither-switch-label">Dither</Label>
<MoreInfo>Dithering uses different dot densities to simulate shades of gray.</MoreInfo> <MoreInfo>Dithering uses different dot densities to simulate shades of gray.</MoreInfo>
@@ -28,7 +28,11 @@
} }
</script> </script>
<div class={cn('grow stack gap-2', hidden ? ['invisible'] : [])} role="group" aria-label="Dithering kernel"> <div
class={cn('grow stack gap-2', hidden ? ['invisible'] : [])}
role="group"
aria-labelledby="dithering-kernel-select-label"
>
<div class="row gap-1"> <div class="row gap-1">
<Label id="dithering-kernel-select-label">Dithering Kernel</Label> <Label id="dithering-kernel-select-label">Dithering Kernel</Label>
@@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import * as Alert from '$lib/components/ui/alert' import * as Alert from '$lib/components/ui/alert'
import IconAspectRatio from '~icons/material-symbols/aspect-ratio-outline' import IconAspectRatio from '~icons/material-symbols/aspect-ratio-outline'
import { INKCLIP_ASPECT_RATIO } from '$lib/constants'
</script> </script>
<Alert.Root> <Alert.Root>
<IconAspectRatio aria-hidden /> <IconAspectRatio aria-hidden />
<Alert.Title>Image is not 1:1 ratio</Alert.Title> <Alert.Title>Image is not {String(Math.round(INKCLIP_ASPECT_RATIO * 100) / 100)}:1 ratio</Alert.Title>
<Alert.Description>You need to choose how to scale your image.</Alert.Description> <Alert.Description>You need to choose how to scale your image.</Alert.Description>
</Alert.Root> </Alert.Root>
@@ -57,7 +57,7 @@
config.transform = config.transform.op(op as Operation) config.transform = config.transform.op(op as Operation)
}} }}
> >
<Icon aria-hidden /> <Icon aria-label={description} />
</Button> </Button>
{/snippet} {/snippet}
</Tooltip.Trigger> </Tooltip.Trigger>
@@ -12,4 +12,4 @@
const filesCtx = getFilesContext() const filesCtx = getFilesContext()
</script> </script>
<Input bind:ref type="file" accept="image/*" bind:files={filesCtx.files} /> <Input bind:ref type="file" accept="image/*" bind:files={filesCtx.files} aria-labelledby="preview-section-label" />
@@ -5,6 +5,7 @@
import { cn } from '$lib/utils' import { cn } from '$lib/utils'
import { drawQuantizedData, freshContext } from './common.svelte' import { drawQuantizedData, freshContext } from './common.svelte'
import { getRenderedContext } from '$lib/contexts/rendered.svelte' import { getRenderedContext } from '$lib/contexts/rendered.svelte'
import { INKCLIP_HEIGHT, INKCLIP_WIDTH } from '$lib/constants'
const renderedCtx = getRenderedContext() const renderedCtx = getRenderedContext()
const hasRendered = $derived(renderedCtx.rendered !== null) const hasRendered = $derived(renderedCtx.rendered !== null)
@@ -23,15 +24,17 @@
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative" role="img" aria-labelledby="preview-1x-label"> <div class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative" role="img" aria-labelledby="preview-1x-label">
<div class="p-[3px] border-[1px] border-[#888]"> <div class="p-[3px] border-[1px] border-[#888]">
<canvas <canvas
class={cn('w-[200px] h-[200px]', hasRendered || 'hidden')} class={cn(hasRendered || 'hidden')}
style:image-rendering="pixelated" style:image-rendering="pixelated"
style:width="{INKCLIP_WIDTH}px"
style:height="{INKCLIP_HEIGHT}px"
bind:this={canvasEl} bind:this={canvasEl}
height={200} height={INKCLIP_HEIGHT}
width={200} width={INKCLIP_WIDTH}
></canvas> ></canvas>
{#if !hasRendered} {#if !hasRendered}
<div class="col justify-center w-[200px] h-[200px] text-muted"> <div class="col justify-center text-muted" style:width="{INKCLIP_WIDTH}px" style:height="{INKCLIP_HEIGHT}px">
<IconHideImage class="text-3xl" aria-label="No image" /> <IconHideImage class="text-3xl" aria-label="No image" />
</div> </div>
{/if} {/if}
@@ -6,6 +6,7 @@
import { drawQuantizedData, freshContext } from './common.svelte' import { drawQuantizedData, freshContext } from './common.svelte'
import { getFilesContext } from '$lib/contexts/files.svelte' import { getFilesContext } from '$lib/contexts/files.svelte'
import { getRenderedContext } from '$lib/contexts/rendered.svelte' import { getRenderedContext } from '$lib/contexts/rendered.svelte'
import { INKCLIP_HEIGHT, INKCLIP_WIDTH } from '$lib/constants'
interface Props { interface Props {
fileInputEl: HTMLInputElement | null fileInputEl: HTMLInputElement | null
@@ -43,11 +44,13 @@
> >
<div class="p-[6px] border-[1px] border-[#888]"> <div class="p-[6px] border-[1px] border-[#888]">
<canvas <canvas
class={cn('w-[400px] h-[400px]', hasRendered || 'hidden')} class={cn(hasRendered || 'hidden')}
style:image-rendering="pixelated" style:image-rendering="pixelated"
style:width="{INKCLIP_WIDTH * 2}px"
style:height="{INKCLIP_HEIGHT * 2}px"
bind:this={canvasEl} bind:this={canvasEl}
height={200} height={INKCLIP_HEIGHT}
width={200} width={INKCLIP_WIDTH}
></canvas> ></canvas>
{#if !hasRendered} {#if !hasRendered}
<button <button
@@ -55,9 +58,11 @@
ondragleave={() => (fileOverDragZone = false)} ondragleave={() => (fileOverDragZone = false)}
ondragover={() => (fileOverDragZone = true)} ondragover={() => (fileOverDragZone = true)}
class={cn( class={cn(
'col justify-center w-[400px] h-[400px] rounded-xl hover:cursor-pointer text-muted', `col justify-center rounded-xl hover:cursor-pointer text-muted`,
fileOverDragZone && 'outline-dashed', fileOverDragZone && 'outline-dashed',
)} )}
style:width="{INKCLIP_WIDTH * 2}px"
style:height="{INKCLIP_HEIGHT * 2}px"
tabindex={-1} tabindex={-1}
aria-label="Choose file" aria-label="Choose file"
> >
+12 -8
View File
@@ -1,3 +1,5 @@
import { INKCLIP_HEIGHT, INKCLIP_WIDTH } from '$lib/constants'
export function freshContext(el: HTMLCanvasElement) { export function freshContext(el: HTMLCanvasElement) {
const ctx = el.getContext('2d')! const ctx = el.getContext('2d')!
ctx.imageSmoothingEnabled = false ctx.imageSmoothingEnabled = false
@@ -5,17 +7,19 @@ export function freshContext(el: HTMLCanvasElement) {
} }
export function drawQuantizedData(ctx: CanvasRenderingContext2D, data: number[]) { export function drawQuantizedData(ctx: CanvasRenderingContext2D, data: number[]) {
const imageData = ctx.createImageData(200, 200) const imageData = ctx.createImageData(INKCLIP_WIDTH, INKCLIP_HEIGHT)
for (let y = 0; y < 200; y++) { for (let y = 0; y < INKCLIP_HEIGHT; y++) {
for (let x = 0; x < 200; x++) { for (let x = 0; x < INKCLIP_WIDTH; x++) {
const colorIx = data.at(y * 200 + x) const dataIx = y * INKCLIP_WIDTH + x
const bitmapIx = dataIx * 4
const colorIx = data.at(dataIx)
const color = colorIx === 0 ? 0xcc : 0x11 const color = colorIx === 0 ? 0xcc : 0x11
imageData.data[y * 800 + x * 4] = color imageData.data[bitmapIx] = color
imageData.data[y * 800 + x * 4 + 1] = color imageData.data[bitmapIx + 1] = color
imageData.data[y * 800 + x * 4 + 2] = color imageData.data[bitmapIx + 2] = color
imageData.data[y * 800 + x * 4 + 3] = 0xff imageData.data[bitmapIx + 3] = 0xff
} }
} }
ctx.putImageData(imageData, 0, 0) ctx.putImageData(imageData, 0, 0)
+7 -6
View File
@@ -4,6 +4,7 @@
import { toast } from 'svelte-sonner' import { toast } from 'svelte-sonner'
import { getDeviceContext } from '$lib/contexts/device.svelte' import { getDeviceContext } from '$lib/contexts/device.svelte'
import { getRenderedContext } from '$lib/contexts/rendered.svelte' import { getRenderedContext } from '$lib/contexts/rendered.svelte'
import { BYTES_IN_A_ROW, INKCLIP_HEIGHT, INKCLIP_WIDTH, WRITE_TIME } from '$lib/constants'
interface Props { interface Props {
onprogress: (inPropgress: boolean) => void onprogress: (inPropgress: boolean) => void
@@ -31,15 +32,15 @@
} }
} }
const buffer = new Uint8Array(5000) const buffer = new Uint8Array(INKCLIP_HEIGHT * BYTES_IN_A_ROW)
for (let y = 0; y < 200; y++) { for (let y = 0; y < INKCLIP_HEIGHT; y++) {
for (let xStride = 0; xStride < 25; xStride++) { for (let xStride = 0; xStride < BYTES_IN_A_ROW; xStride++) {
let cell = 0x0 let cell = 0x0
for (let xStroll = 0; xStroll < 8; xStroll++) { for (let xStroll = 0; xStroll < 8; xStroll++) {
const index = y * 200 + xStride * 8 + xStroll const index = y * INKCLIP_WIDTH + xStride * 8 + xStroll
cell |= renderedCtx.rendered[index] << xStroll cell |= renderedCtx.rendered[index] << xStroll
} }
buffer[y * 25 + xStride] = cell buffer[y * BYTES_IN_A_ROW + xStride] = cell
} }
} }
@@ -53,7 +54,7 @@
} finally { } finally {
setTimeout(() => { setTimeout(() => {
inProgress = false inProgress = false
}, 2000) }, WRITE_TIME)
} }
toast.success('Wrote pattern to device') toast.success('Wrote pattern to device')
+4
View File
@@ -4,3 +4,7 @@ import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export function showNumber(n: number): string {
return String(n).replace('-', '').replace('Infinity', '∞').replace('NaN', '⁇')
}