run user code yippie

main
Kyle Zheng 2024-06-30 04:45:24 -04:00
rodzic 7f7873b26f
commit f7f5b094be
23 zmienionych plików z 4215 dodań i 3520 usunięć

Wyświetl plik

@ -3,21 +3,33 @@ import { defineConfig } from "@solidjs/start/config";
import UnoCSS from "unocss/vite";
import wasmpack from "vite-plugin-wasm-pack";
// b/c I'm using vite() instead of static config object
// getting a new plugin each time vite() runs messes up unocss styles on the `body`
const SharedUnoCss = UnoCSS();
export default defineConfig({
server: { preset: "vercel" },
ssr: true,
vite: {
plugins: [UnoCSS(), wasmpack(["./fuqr"])],
resolve: {
alias: {
// https://christopher.engineering/en/blog/lucide-icons-with-vite-dev-server/
"lucide-solid/icons": fileURLToPath(
new URL(
"./node_modules/lucide-solid/dist/source/icons",
import.meta.url
)
),
vite({ router }) {
return {
plugins:
// https://github.com/nksaraf/vinxi/issues/262
// wasmpack copies pkg to node_modules asynchronously so triggering buildStart 3 times causes errors
router === "client"
? [SharedUnoCss, wasmpack(["./fuqr"])]
: [SharedUnoCss],
resolve: {
alias: {
// https://christopher.engineering/en/blog/lucide-icons-with-vite-dev-server/
"lucide-solid/icons": fileURLToPath(
new URL(
"./node_modules/lucide-solid/dist/source/icons",
import.meta.url
)
),
},
},
},
};
},
});

84
fuqr/pkg/fuqr.d.ts vendored
Wyświetl plik

@ -15,27 +15,11 @@ export function get_matrix(input: string, qr_options: QrOptions): Matrix;
export function get_svg(input: string, qr_options: QrOptions, svg_options: SvgOptions): SvgResult;
/**
*/
export enum Module {
DataOFF = 0,
DataON = 1,
FinderOFF = 2,
FinderON = 3,
AlignmentOFF = 4,
AlignmentON = 5,
TimingOFF = 6,
TimingON = 7,
FormatOFF = 8,
FormatON = 9,
VersionOFF = 10,
VersionON = 11,
Unset = 12,
}
/**
*/
export enum Toggle {
Background = 0,
BackgroundPixels = 1,
ForegroundPixels = 2,
export enum ECL {
Low = 0,
Medium = 1,
Quartile = 2,
High = 3,
}
/**
*/
@ -51,14 +35,6 @@ export enum Mask {
}
/**
*/
export enum ECL {
Low = 0,
Medium = 1,
Quartile = 2,
High = 3,
}
/**
*/
export enum Mode {
Numeric = 0,
Alphanumeric = 1,
@ -66,12 +42,36 @@ export enum Mode {
}
/**
*/
export enum Toggle {
Background = 0,
BackgroundPixels = 1,
ForegroundPixels = 2,
}
/**
*/
export enum QrError {
InvalidEncoding = 0,
ExceedsMaxCapacity = 1,
}
/**
*/
export enum Module {
DataOFF = 0,
DataON = 1,
FinderOFF = 2,
FinderON = 3,
AlignmentOFF = 4,
AlignmentON = 5,
TimingOFF = 6,
TimingON = 7,
FormatOFF = 8,
FormatON = 9,
VersionOFF = 10,
VersionON = 11,
Unset = 12,
}
/**
*/
export class Margin {
free(): void;
/**
@ -193,11 +193,6 @@ export class SvgOptions {
*/
constructor();
/**
* @param {number} margin
* @returns {SvgOptions}
*/
margin(margin: number): SvgOptions;
/**
* @param {number} unit
* @returns {SvgOptions}
*/
@ -213,20 +208,20 @@ export class SvgOptions {
*/
background(background: string): SvgOptions;
/**
* @param {Uint8Array} scale_matrix
* @param {Uint8Array | undefined} [scale_x_matrix]
* @returns {SvgOptions}
*/
scale_x_matrix(scale_matrix: Uint8Array): SvgOptions;
scale_x_matrix(scale_x_matrix?: Uint8Array): SvgOptions;
/**
* @param {Uint8Array} scale_matrix
* @param {Uint8Array | undefined} [scale_y_matrix]
* @returns {SvgOptions}
*/
scale_y_matrix(scale_matrix: Uint8Array): SvgOptions;
scale_y_matrix(scale_y_matrix?: Uint8Array): SvgOptions;
/**
* @param {Uint8Array} scale_matrix
* @param {Uint8Array | undefined} [scale_matrix]
* @returns {SvgOptions}
*/
scale_matrix(scale_matrix: Uint8Array): SvgOptions;
scale_matrix(scale_matrix?: Uint8Array): SvgOptions;
/**
* @param {Toggle} toggle
* @returns {SvgOptions}
@ -301,6 +296,10 @@ export interface InitOutput {
readonly __wbg_set_matrix_mask: (a: number, b: number) => void;
readonly matrix_width: (a: number) => number;
readonly matrix_height: (a: number) => number;
readonly __wbg_version_free: (a: number) => void;
readonly __wbg_get_version_0: (a: number) => number;
readonly __wbg_set_version_0: (a: number, b: number) => void;
readonly version_new: (a: number) => number;
readonly __wbg_qroptions_free: (a: number) => void;
readonly qroptions_new: () => number;
readonly qroptions_min_version: (a: number, b: number) => number;
@ -310,7 +309,6 @@ export interface InitOutput {
readonly qroptions_margin: (a: number, b: number) => number;
readonly __wbg_svgoptions_free: (a: number) => void;
readonly svgoptions_new: () => number;
readonly svgoptions_margin: (a: number, b: number) => number;
readonly svgoptions_unit: (a: number, b: number) => number;
readonly svgoptions_foreground: (a: number, b: number, c: number) => number;
readonly svgoptions_background: (a: number, b: number, c: number) => number;
@ -331,10 +329,6 @@ export interface InitOutput {
readonly __wbg_set_svgresult_mask: (a: number, b: number) => void;
readonly get_matrix: (a: number, b: number, c: number, d: number) => void;
readonly get_svg: (a: number, b: number, c: number, d: number, e: number) => void;
readonly __wbg_version_free: (a: number) => void;
readonly __wbg_get_version_0: (a: number) => number;
readonly __wbg_set_version_0: (a: number, b: number) => void;
readonly version_new: (a: number) => number;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;

Wyświetl plik

@ -223,22 +223,22 @@ export function get_svg(input, qr_options, svg_options) {
/**
*/
export const Module = Object.freeze({ DataOFF:0,"0":"DataOFF",DataON:1,"1":"DataON",FinderOFF:2,"2":"FinderOFF",FinderON:3,"3":"FinderON",AlignmentOFF:4,"4":"AlignmentOFF",AlignmentON:5,"5":"AlignmentON",TimingOFF:6,"6":"TimingOFF",TimingON:7,"7":"TimingON",FormatOFF:8,"8":"FormatOFF",FormatON:9,"9":"FormatON",VersionOFF:10,"10":"VersionOFF",VersionON:11,"11":"VersionON",Unset:12,"12":"Unset", });
/**
*/
export const Toggle = Object.freeze({ Background:0,"0":"Background",BackgroundPixels:1,"1":"BackgroundPixels",ForegroundPixels:2,"2":"ForegroundPixels", });
export const ECL = Object.freeze({ Low:0,"0":"Low",Medium:1,"1":"Medium",Quartile:2,"2":"Quartile",High:3,"3":"High", });
/**
*/
export const Mask = Object.freeze({ M0:0,"0":"M0",M1:1,"1":"M1",M2:2,"2":"M2",M3:3,"3":"M3",M4:4,"4":"M4",M5:5,"5":"M5",M6:6,"6":"M6",M7:7,"7":"M7", });
/**
*/
export const ECL = Object.freeze({ Low:0,"0":"Low",Medium:1,"1":"Medium",Quartile:2,"2":"Quartile",High:3,"3":"High", });
/**
*/
export const Mode = Object.freeze({ Numeric:0,"0":"Numeric",Alphanumeric:1,"1":"Alphanumeric",Byte:2,"2":"Byte", });
/**
*/
export const Toggle = Object.freeze({ Background:0,"0":"Background",BackgroundPixels:1,"1":"BackgroundPixels",ForegroundPixels:2,"2":"ForegroundPixels", });
/**
*/
export const QrError = Object.freeze({ InvalidEncoding:0,"0":"InvalidEncoding",ExceedsMaxCapacity:1,"1":"ExceedsMaxCapacity", });
/**
*/
export const Module = Object.freeze({ DataOFF:0,"0":"DataOFF",DataON:1,"1":"DataON",FinderOFF:2,"2":"FinderOFF",FinderON:3,"3":"FinderON",AlignmentOFF:4,"4":"AlignmentOFF",AlignmentON:5,"5":"AlignmentON",TimingOFF:6,"6":"TimingOFF",TimingON:7,"7":"TimingON",FormatOFF:8,"8":"FormatOFF",FormatON:9,"9":"FormatON",VersionOFF:10,"10":"VersionOFF",VersionON:11,"11":"VersionON",Unset:12,"12":"Unset", });
const MarginFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
@ -635,15 +635,6 @@ export class SvgOptions {
return this;
}
/**
* @param {number} margin
* @returns {SvgOptions}
*/
margin(margin) {
const ptr = this.__destroy_into_raw();
const ret = wasm.svgoptions_margin(ptr, margin);
return SvgOptions.__wrap(ret);
}
/**
* @param {number} unit
* @returns {SvgOptions}
*/
@ -675,35 +666,35 @@ export class SvgOptions {
return SvgOptions.__wrap(ret);
}
/**
* @param {Uint8Array} scale_matrix
* @param {Uint8Array | undefined} [scale_x_matrix]
* @returns {SvgOptions}
*/
scale_x_matrix(scale_matrix) {
scale_x_matrix(scale_x_matrix) {
const ptr = this.__destroy_into_raw();
const ptr0 = passArray8ToWasm0(scale_matrix, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
var ptr0 = isLikeNone(scale_x_matrix) ? 0 : passArray8ToWasm0(scale_x_matrix, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
const ret = wasm.svgoptions_scale_x_matrix(ptr, ptr0, len0);
return SvgOptions.__wrap(ret);
}
/**
* @param {Uint8Array} scale_matrix
* @param {Uint8Array | undefined} [scale_y_matrix]
* @returns {SvgOptions}
*/
scale_y_matrix(scale_matrix) {
scale_y_matrix(scale_y_matrix) {
const ptr = this.__destroy_into_raw();
const ptr0 = passArray8ToWasm0(scale_matrix, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
var ptr0 = isLikeNone(scale_y_matrix) ? 0 : passArray8ToWasm0(scale_y_matrix, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
const ret = wasm.svgoptions_scale_y_matrix(ptr, ptr0, len0);
return SvgOptions.__wrap(ret);
}
/**
* @param {Uint8Array} scale_matrix
* @param {Uint8Array | undefined} [scale_matrix]
* @returns {SvgOptions}
*/
scale_matrix(scale_matrix) {
const ptr = this.__destroy_into_raw();
const ptr0 = passArray8ToWasm0(scale_matrix, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
var ptr0 = isLikeNone(scale_matrix) ? 0 : passArray8ToWasm0(scale_matrix, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
const ret = wasm.svgoptions_scale_matrix(ptr, ptr0, len0);
return SvgOptions.__wrap(ret);
}

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -32,6 +32,10 @@ export function __wbg_get_matrix_mask(a: number): number;
export function __wbg_set_matrix_mask(a: number, b: number): void;
export function matrix_width(a: number): number;
export function matrix_height(a: number): number;
export function __wbg_version_free(a: number): void;
export function __wbg_get_version_0(a: number): number;
export function __wbg_set_version_0(a: number, b: number): void;
export function version_new(a: number): number;
export function __wbg_qroptions_free(a: number): void;
export function qroptions_new(): number;
export function qroptions_min_version(a: number, b: number): number;
@ -41,7 +45,6 @@ export function qroptions_mask(a: number, b: number): number;
export function qroptions_margin(a: number, b: number): number;
export function __wbg_svgoptions_free(a: number): void;
export function svgoptions_new(): number;
export function svgoptions_margin(a: number, b: number): number;
export function svgoptions_unit(a: number, b: number): number;
export function svgoptions_foreground(a: number, b: number, c: number): number;
export function svgoptions_background(a: number, b: number, c: number): number;
@ -62,10 +65,6 @@ export function __wbg_get_svgresult_mask(a: number): number;
export function __wbg_set_svgresult_mask(a: number, b: number): void;
export function get_matrix(a: number, b: number, c: number, d: number): void;
export function get_svg(a: number, b: number, c: number, d: number, e: number): void;
export function __wbg_version_free(a: number): void;
export function __wbg_get_version_0(a: number): number;
export function __wbg_set_version_0(a: number, b: number): void;
export function version_new(a: number): number;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_free(a: number, b: number, c: number): void;
export function __wbindgen_malloc(a: number, b: number): number;

Wyświetl plik

@ -10,8 +10,11 @@
"@kobalte/core": "^0.13.1",
"@solidjs/router": "^0.13.3",
"@solidjs/start": "^1.0.0",
"@srsholmes/solid-code-input": "^0.0.18",
"@unocss/reset": "^0.59.4",
"highlight.js": "^11.9.0",
"lucide-solid": "^0.378.0",
"qr-scanner-wechat": "^0.1.3",
"solid-js": "^1.8.17",
"unocss": "^0.59.4",
"vinxi": "^0.3.11"

Plik diff jest za duży Load Diff

Wyświetl plik

@ -6,15 +6,21 @@ type Props = {
onClick?: () => void;
onMouseDown?: () => void;
children: JSX.Element;
tooltip?: string;
disabled?: boolean;
};
export function FlatButton(props: Props) {
return (
<Button
class={`inline-flex justify-center items-center gap-1 border rounded-md hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) ${
props.class || "px-3 py-2"
}`}
title={props.tooltip}
classList={{
"inline-flex justify-center items-center gap-1 border rounded-md hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) disabled:(pointer-events-none opacity-50)":
true,
[props.class ?? "px-3 py-2"]: true,
}}
onMouseDown={props.onMouseDown}
onClick={props.onClick}
disabled={props.disabled}
>
{props.children}
</Button>

Wyświetl plik

@ -29,7 +29,7 @@ type ItemProps = {
export function ButtonGroupItem(props: ItemProps) {
return (
<ToggleGroup.Item
class="px-3 py-2 min-w-8 leading-tight border-l first:(border-none rounded-l-md) last:rounded-r-md text-fore-subtle aria-pressed:(bg-fore-base/10 text-fore-base) hover:bg-fore-base/10 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)"
class="flex-grow-1 px-3 py-2 min-w-8 leading-tight border-l first:(border-none rounded-l-md) last:rounded-r-md text-fore-subtle aria-pressed:(bg-fore-base/10 text-fore-base) hover:bg-fore-base/10 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)"
value={props.value}
aria-label={props.value ?? props.ariaLabel}
title={props.title ? props.value ?? props.ariaLabel : undefined}

Wyświetl plik

@ -4,7 +4,7 @@ type Props = {
};
export function ColorInput(props: Props) {
return (
<label class="border rounded-md font-mono inline-flex items-center p-1.5 gap-2 cursor-pointer hover:bg-fore-base/5">
<label class="border rounded-md font-mono inline-flex items-center py-1.5 px-2 gap-2 cursor-pointer hover:bg-fore-base/5">
{props.color}
<input
class="rounded-sm border-none w-6 h-6 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)"

Wyświetl plik

@ -1,249 +0,0 @@
import { For, Show, batch, type JSX } from "solid-js";
import { useQrContext } from "~/lib/QrContext";
import {
ECL_NAMES,
ECL_VALUE,
MASK_KEY,
MASK_NAMES,
MASK_VALUE,
MODE_KEY,
MODE_NAMES,
MODE_VALUE,
} from "~/lib/options";
import { FlatButton } from "./Button";
import { ButtonGroup, ButtonGroupItem } from "./ButtonGroup";
import { ColorInput } from "./ColorInput";
import { ImageInput } from "./ImageInput";
import { ModeTextInput } from "./ModeTextInput";
import { NumberInput } from "./NumberInput";
import { Select } from "./Select";
import { Switch } from "./Switch";
import { useSvgContext } from "~/lib/SvgContext";
export function Editor(props: any) {
const { inputQr, setInputQr } = useQrContext();
const {
svgOptions,
setSvgOptions,
selections,
scaleX,
scaleY,
setScaleXInPlace,
setScaleYInPlace,
} = useSvgContext();
// const [logoSize, setLogoSize] = createSignal(25);
return (
<div>
<div class="flex flex-col gap-2 flex-1 p-4">
<ModeTextInput setValue={(s) => setInputQr("text", s)} />
<Row title="Mode">
<Select
values={MODE_NAMES}
value={MODE_KEY[inputQr.mode!]}
setValue={(name) => setInputQr("mode", MODE_VALUE[name])}
/>
</Row>
<Row title="Min version">
<NumberInput
min={1}
max={40}
value={inputQr.minVersion}
setValue={(v) => setInputQr("minVersion", v)}
/>
</Row>
<Row title="Min error tolerance">
<ButtonGroup
value={ECL_NAMES[inputQr.minEcl]}
setValue={(v) => setInputQr("minEcl", ECL_VALUE[v])}
>
<For each={ECL_NAMES}>
{(name) => <ButtonGroupItem value={name}>{name}</ButtonGroupItem>}
</For>
</ButtonGroup>
</Row>
<Row title="Mask pattern">
<ButtonGroup
value={MASK_KEY[inputQr.mask!]}
setValue={(name) => setInputQr("mask", MASK_VALUE[name])}
>
<For each={MASK_NAMES}>
{(value) => (
<ButtonGroupItem value={value}>{value}</ButtonGroupItem>
)}
</For>
</ButtonGroup>
</Row>
{/* <Row title="Margin">
<NumberInput
min={0}
max={10}
step={1}
value={inputQr.margin.top}
setValue={(v) => setInputQr("margin", (prev) => prev.setTop(v))}
/>
</Row> */}
<Row title="Foreground">
<ColorInput
color={svgOptions.fgColor}
setColor={(v) => setSvgOptions("fgColor", v)}
/>
<Switch
value={svgOptions.pixelateFgImg}
setValue={(v) => setSvgOptions("pixelateFgImg", v)}
label="Disable smoothing"
/>
<FlatButton
class="text-sm px-2 py-2"
onMouseDown={() => {
batch(() => {
let tmp = svgOptions.fgColor;
setSvgOptions("fgColor", svgOptions.bgColor);
setSvgOptions("bgColor", tmp);
});
}}
>
Swap
</FlatButton>
</Row>
<Row title="Background">
<div class="flex flex-col items-start gap-1">
<ColorInput
color={svgOptions.bgColor}
setColor={(v) => setSvgOptions("bgColor", v)}
/>
<ImageInput
value={svgOptions.bgImgFile}
setValue={(v) => setSvgOptions("bgImgFile", v)}
/>
<Switch
value={svgOptions.pixelateBgImg}
setValue={(v) => setSvgOptions("pixelateBgImg", v)}
label="Disable smoothing"
/>
</div>
</Row>
<Row title="Logo">
<div class="flex flex-col gap-1 w-full">
<ImageInput
value={svgOptions.fgImgFile}
setValue={(v) => setSvgOptions("fgImgFile", v)}
/>
{/* TODO need better img management than just size */}
{/* <NumberInput
min={0}
max={100}
step={0.1}
value={logoSize()}
setValue={setLogoSize}
/> */}
</div>
</Row>
<Row title="Scale X">
<NumberInput
min={0}
max={200}
step={1}
value={
selections().length
? scaleX()[
selections()[0].top * Math.sqrt(scaleX().length) +
selections()[0].left
]
: 100
}
setValue={(v) => {
if (!selections().length) return;
setScaleXInPlace((prev) => {
let width = Math.sqrt(prev.length);
selections().forEach((sel) => {
for (let i = sel.top; i < sel.bot; i++) {
for (let j = sel.left; j < sel.right; j++) {
prev[i * width + j] = v;
}
}
});
return prev;
});
}}
/>
</Row>
<Row title="Scale Y">
<NumberInput
min={0}
max={200}
step={1}
value={
selections().length
? scaleY()[
selections()[0].top * Math.sqrt(scaleY().length) +
selections()[0].left
]
: 100
}
setValue={(v) => {
if (!selections().length) return;
setScaleYInPlace((prev) => {
let width = Math.sqrt(prev.length);
selections().forEach((sel) => {
for (let i = sel.top; i < sel.bot; i++) {
for (let j = sel.left; j < sel.right; j++) {
prev[i * width + j] = v;
}
}
});
return prev;
});
}}
/>
</Row>
</div>
</div>
);
}
function Row(props: {
title: string;
children: JSX.Element;
sparkle?: boolean;
}) {
// This should be <label/> but clicking selects first button in buttongroup
return (
<div class="flex gap-2">
<Show when={props.sparkle}>
<AnimatedSparkle />
</Show>
<span class="w-30 py-2 text-left text-sm flex-shrink-0">
{props.title}
</span>
{props.children}
</div>
);
}
function AnimatedSparkle() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 29 29"
class="w-5 h-5 absolute -translate-x-full mt-2"
>
<path
d="M14.6 5.8c.5 0 .8 7.4 1.2 7.8.4.4 7.9.5 7.9 1 0 .6-7.5.9-7.8 1.3-.4.4-.5 7.8-1 7.9-.6 0-1-7.5-1.3-7.9-.4-.4-7.9-.5-7.9-1 0-.6 7.5-.9 7.9-1.3.3-.4.4-7.8 1-7.8z"
style="fill:#fca4a4"
class="animate-pulse"
/>
<path
d="M25.5 7.5c.1.3-2.3.6-2.4.8-.2.2-.3 2.6-.5 2.7-.3 0-.5-2.3-.7-2.5-.3-.2-2.7-.2-2.8-.5 0-.2 2.3-.5 2.5-.7.2-.2.2-2.7.5-2.7.2 0 .5 2.3.7 2.4.2.2 2.7.3 2.7.5z"
style="fill:#fca4a4"
class="animate-pulse"
/>
<path
d="M11.3 21.9c0 .3-3 .1-3.2.3-.2.3 0 3.2-.3 3.2s-.1-3-.4-3.2c-.2-.2-3 0-3-.3s2.8-.1 3-.3c.3-.3 0-3.2.4-3.2.3 0 .1 3 .3 3.2.3.2 3.2 0 3.2.3z"
style="fill:#fca4a4"
class="animate-pulse"
/>
</svg>
);
}

Wyświetl plik

@ -3,7 +3,7 @@ type Props = {
};
/** No `value` prop b/c textarea cannot be controlled */
export function ModeTextInput(props: Props) {
export function TextInput(props: Props) {
const onInput = debounce(props.setValue, 300);
return (
<div class="flex flex-col gap-2">

Wyświetl plik

@ -59,7 +59,7 @@ export function RenderGrid(props: Props) {
let selection: BoxSelection | null;
function onMouseMove(e: MouseEvent) {
function onPointerMove(e: MouseEvent) {
const { x, y } = getPos(e);
if (mode() === Mode.Select) {
@ -142,7 +142,7 @@ export function RenderGrid(props: Props) {
}
}
const onMouseUp = () => {
const onPointerUp = () => {
if (selection != null) {
setSelectionsInPlace((prev) => {
// @ts-expect-error i'm right unless somehow this isn't synchronous
@ -150,13 +150,13 @@ export function RenderGrid(props: Props) {
return prev;
});
}
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", onPointerUp);
};
onCleanup(() => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", onPointerUp);
});
const [mode, setMode] = createSignal(Mode.Select);
@ -229,8 +229,8 @@ export function RenderGrid(props: Props) {
prevX = x;
prevY = y;
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("pointerup", onPointerUp);
document.addEventListener("pointermove", onPointerMove);
}}
></canvas>
</>

Wyświetl plik

@ -0,0 +1,152 @@
import { For, createSignal, type JSX } from "solid-js";
import { useQrContext, type RenderFunc } from "~/lib/QrContext";
import {
ECL_NAMES,
ECL_VALUE,
MASK_KEY,
MASK_NAMES,
MASK_VALUE,
MODE_KEY,
MODE_NAMES,
MODE_VALUE,
} from "~/lib/options";
import { FlatButton } from "../Button";
import { ButtonGroup, ButtonGroupItem } from "../ButtonGroup";
import { TextInput } from "../ModeTextInput";
import { NumberInput } from "../NumberInput";
import { Select } from "../Select";
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import { CodeInput } from "@srsholmes/solid-code-input";
import "../../styles/atom-one-dark.css";
hljs.registerLanguage("javascript", javascript);
export function Editor(props: any) {
const { inputQr, setInputQr, setRenderFunc } = useQrContext();
const [code, setCode] = createSignal(` // qr, ctx are args
const pixelSize = 10;
ctx.canvas.width = qr.matrixWidth * pixelSize;
ctx.canvas.height = qr.matrixHeight * pixelSize;
ctx.fillStyle = "rgb(255, 255, 255)";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = "rgb(0, 0, 0)";
for (let y = 0; y < qr.matrixHeight; y++) {
for (let x = 0; x < qr.matrixWidth; x++) {
const module = qr.matrix[y * qr.matrixWidth + x];
if (module & 1) {
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}
}
}
`);
const [prevCode, setPrevCode] = createSignal(code());
const saveCode = () => {
setRenderFunc(() => new Function("qr", "ctx", code()) as RenderFunc);
};
return (
<div class="flex flex-col gap-2 flex-1 p-4">
<TextInput setValue={(s) => setInputQr("text", s)} />
<Row title="Encoding" tooltip="Also known as Mode">
<Select
values={MODE_NAMES}
value={MODE_KEY[inputQr.mode!]}
setValue={(name) => setInputQr("mode", MODE_VALUE[name])}
/>
</Row>
<Row title="Min symbol size" tooltip="Also known as Version">
<NumberInput
min={1}
max={40}
value={inputQr.minVersion}
setValue={(v) => setInputQr("minVersion", v)}
/>
</Row>
<Row
title="Min error tolerance"
tooltip="Also known as Error Correction Level"
>
<ButtonGroup
value={ECL_NAMES[inputQr.minEcl]}
setValue={(v) => setInputQr("minEcl", ECL_VALUE[v])}
>
<For each={ECL_NAMES}>
{(name) => <ButtonGroupItem value={name}>{name}</ButtonGroupItem>}
</For>
</ButtonGroup>
</Row>
<Row title="Mask pattern">
<ButtonGroup
value={MASK_KEY[inputQr.mask!]}
setValue={(name) => setInputQr("mask", MASK_VALUE[name])}
>
<For each={MASK_NAMES}>
{(value) => (
<ButtonGroupItem value={value}>{value}</ButtonGroupItem>
)}
</For>
</ButtonGroup>
</Row>
<Row title="Margin">
<NumberInput
min={0}
max={10}
step={1}
value={inputQr.margin.top}
setValue={(v) =>
setInputQr("margin", { top: v, bottom: v, left: v, right: v })
}
/>
</Row>
<div>
<div class="flex justify-between my-2">
<div class="text-sm py-2">Render function</div>
<FlatButton class="px-3 py-1" disabled={prevCode() === code()} onMouseDown={saveCode}>
{prevCode() === code() ? "Saved" : "Save"}
</FlatButton>
</div>
<div
class="hljs-wrapper"
onKeyDown={(e) => {
if (e.ctrlKey && e.key === "s") {
e.preventDefault();
saveCode();
}
}}
>
<CodeInput
autoHeight={true}
value={code()}
onChange={setCode}
highlightjs={hljs}
language="javascript"
resize="both"
/>
</div>
</div>
</div>
);
}
function Row(props: {
tooltip?: string;
title: string;
children: JSX.Element;
}) {
// This should be <label/> but clicking selects first button in buttongroup
return (
<div title={props.tooltip}>
<div class="text-sm py-2">{props.title}</div>
{props.children}
</div>
);
}

Wyświetl plik

@ -0,0 +1,113 @@
import { QrError } from "fuqr";
import { Match, Show, Switch, createEffect } from "solid-js";
import { useQrContext, type OutputQr } from "~/lib/QrContext";
import {
ECL_LABELS,
ECL_NAMES,
MASK_KEY,
MODE_KEY,
MODE_NAMES,
} from "~/lib/options";
export default function QrPreview() {
const { inputQr, outputQr } = useQrContext();
return (
<>
<Show
when={typeof outputQr() !== "number"}
fallback={
<div class="aspect-[1/1] border rounded-md flex justify-center items-center">
<Switch>
<Match when={outputQr() === QrError.ExceedsMaxCapacity}>
Data exceeds max capacity
</Match>
<Match when={outputQr() === QrError.InvalidEncoding}>
{`Input cannot be encoded in ${
// @ts-expect-error props.mode not null b/c InvalidEncoding implies mode
MODE_NAMES[inputQr.mode + 1]
} mode`}
</Match>
</Switch>
</div>
}
>
<RenderedQrCode />
</Show>
</>
);
}
/** This component assumes outputQr() is not QrError, this simplifies effects and types
*
* Original problem:
* When using Show, effects tracking the when signal run before refs inside Show become valid.
* The result was no effect running after initial mount, so no render.
* Running the effect in the ref function caused double rendering for future mounts.
*/
function RenderedQrCode() {
const { outputQr: _outputQr, renderFunc } = useQrContext();
const outputQr = _outputQr as () => OutputQr
const fullWidth = () => {
const output = outputQr();
return output.version * 4 + 17 + output.margin.left + output.margin.right;
};
const fullHeight = () => {
const output = outputQr();
return output.version * 4 + 17 + output.margin.top + output.margin.bottom;
};
let qrCanvas: HTMLCanvasElement;
createEffect(() => {
const ctx = qrCanvas.getContext("2d")!;
ctx.clearRect(0, 0, qrCanvas.width, qrCanvas.height);
renderFunc()(outputQr(), ctx);
});
return (
<>
<div
class="aspect-[1/1] border rounded-md relative overflow-hidden"
style={{
"background-image":
"repeating-conic-gradient(#ddd 0% 25%, #aaa 25% 50%)",
"background-position": "50%",
"background-size": `${(1 / fullWidth()) * 100}% ${
(1 / fullHeight()) * 100
}%`,
}}
>
<canvas
class="w-full h-full"
ref={qrCanvas!}
></canvas>
</div>
<div class="p-4 grid grid-cols-2 gap-y-2 text-sm text-left">
<div class="">
Symbol size{" "}
<div class="font-bold text-base whitespace-pre">
{outputQr().version} ({outputQr().version * 4 + 17}x
{outputQr().version * 4 + 17} pixels)
</div>
</div>
<div class="">
Error tolerance{" "}
<div class="font-bold text-base whitespace-pre">
{ECL_NAMES[outputQr().ecl]} ({ECL_LABELS[outputQr().ecl]})
</div>
</div>
<div class="">
Encoding{" "}
<span class="font-bold text-base">{MODE_KEY[outputQr().mode]}</span>
</div>
<div class="">
Mask{" "}
<span class="font-bold text-base">{MASK_KEY[outputQr().mask]}</span>
</div>
</div>
</>
);
}

Wyświetl plik

@ -112,17 +112,17 @@ export default function SvgPreview() {
</div>
<div class="p-4 grid grid-cols-2 gap-y-2 text-sm text-left">
<div class="">
Version{" "}
<span class="font-bold text-base whitespace-pre">
Symbol size{" "}
<div class="font-bold text-base whitespace-pre">
{svgResult()!.version["0"]} ({svgResult()!.version["0"] * 4 + 17}x
{svgResult()!.version["0"] * 4 + 17} pixels)
</span>
</div>
</div>
<div class="">
Error tolerance{" "}
<span class="font-bold text-base whitespace-pre">
<div class="font-bold text-base whitespace-pre">
{ECL_NAMES[svgResult()!.ecl]} ({ECL_LABELS[svgResult()!.ecl]})
</span>
</div>
</div>
<div class="">
Encoding{" "}

Wyświetl plik

@ -11,7 +11,7 @@ export default createHandler(() => (
<link rel="icon" href="/favicon.svg" />
{assets}
</head>
<body class="bg-back-base text-fore-base my-8 [--un-default-border-color:fg-subtle] ">
<body class="bg-back-base text-fore-base my-8 [--un-default-border-color:fg-subtle]">
<div id="app">{children}</div>
{scripts}
</body>

Wyświetl plik

@ -1,12 +1,23 @@
import {
createContext,
createEffect,
createSignal,
useContext,
type Accessor,
type JSX,
type Setter,
} from "solid-js";
import { ECL, Mode, Mask, QrError } from "fuqr";
import {
ECL,
Mode,
Mask,
QrError,
Module,
QrOptions,
Version,
Margin,
get_matrix,
} from "fuqr";
import { createStore, type SetStoreFunction } from "solid-js/store";
type InputQr = {
@ -37,6 +48,10 @@ export type OutputQr = {
bottom: number;
left: number;
};
/** Stored as value b/c Matrix is a ptr which becomes null after use */
matrix: Module[];
matrixWidth: number;
matrixHeight: number;
};
export const QrContext = createContext<{
@ -44,8 +59,12 @@ export const QrContext = createContext<{
setInputQr: SetStoreFunction<InputQr>;
outputQr: Accessor<OutputQr | QrError>;
setOutputQr: Setter<OutputQr | QrError>;
renderFunc: Accessor<RenderFunc>;
setRenderFunc: Setter<RenderFunc>;
}>();
export type RenderFunc = (qr: OutputQr, ctx: CanvasRenderingContext2D) => void;
export function QrContextProvider(props: { children: JSX.Element }) {
const [inputQr, setInputQr] = createStore<InputQr>({
text: "Greetings traveler",
@ -65,8 +84,59 @@ export function QrContextProvider(props: { children: JSX.Element }) {
QrError.InvalidEncoding
);
const [renderFunc, setRenderFunc] = createSignal<RenderFunc>(defaultRender);
createEffect(() => {
try {
// NOTE: Version and Margin cannot be reused, so must be created each time
let qrOptions = new QrOptions()
.min_version(new Version(inputQr.minVersion))
.min_ecl(inputQr.minEcl)
.mask(inputQr.mask!) // null makes more sense than undefined
.mode(inputQr.mode!) // null makes more sense than undefined
.margin(new Margin(10))
.margin(
new Margin(0)
.setTop(inputQr.margin.top)
.setRight(inputQr.margin.right)
.setBottom(inputQr.margin.bottom)
.setLeft(inputQr.margin.left)
);
let m = get_matrix(inputQr.text, qrOptions);
setOutputQr({
text: inputQr.text,
matrix: m.value,
matrixWidth: m.version["0"] * 4 + 17 + m.margin.left + m.margin.right,
matrixHeight: m.version["0"] * 4 + 17 + m.margin.top + m.margin.bottom,
version: m.version["0"],
ecl: m.ecl,
mode: m.mode,
mask: m.mask,
margin: {
top: m.margin.top,
right: m.margin.right,
bottom: m.margin.bottom,
left: m.margin.left,
},
});
} catch (e) {
setOutputQr(e as QrError);
}
});
return (
<QrContext.Provider value={{ inputQr, setInputQr, outputQr, setOutputQr }}>
<QrContext.Provider
value={{
inputQr,
setInputQr,
outputQr,
setOutputQr,
renderFunc,
setRenderFunc,
}}
>
{props.children}
</QrContext.Provider>
);
@ -79,3 +149,24 @@ export function useQrContext() {
}
return context;
}
function defaultRender(qr: OutputQr, ctx: CanvasRenderingContext2D) {
const pixelSize = 10;
ctx.canvas.width = qr.matrixWidth * pixelSize;
ctx.canvas.height = qr.matrixHeight * pixelSize;
ctx.fillStyle = "rgb(255, 255, 255)";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = "rgb(0, 0, 0)";
for (let y = 0; y < qr.matrixHeight; y++) {
for (let x = 0; x < qr.matrixWidth; x++) {
const module = qr.matrix[y * qr.matrixWidth + x];
if (module & 1) {
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}
}
}
}

Wyświetl plik

@ -1,162 +0,0 @@
import {
createContext,
createEffect,
createSignal,
untrack,
useContext,
type Accessor,
type JSX,
type Setter,
} from "solid-js";
import { createStore, type SetStoreFunction } from "solid-js/store";
import {
QrOptions,
Version,
Margin,
get_matrix,
SvgOptions,
get_svg,
type QrError,
type SvgResult,
} from "fuqr";
import { useQrContext } from "./QrContext";
type RenderOptions = {
bgColor: string;
fgColor: string;
bgImgFile: File | null;
fgImgFile: File | null;
pixelateBgImg: boolean;
pixelateFgImg: boolean;
};
export type BoxSelection = {
top: number;
bot: number;
left: number;
right: number;
};
export const SvgContext = createContext<{
svgOptions: RenderOptions;
setSvgOptions: SetStoreFunction<RenderOptions>;
selections: Accessor<BoxSelection[]>;
setSelectionsInPlace: Setter<BoxSelection[]>;
scaleX: Accessor<number[]>;
setScaleXInPlace: Setter<number[]>;
scaleY: Accessor<number[]>;
setScaleYInPlace: Setter<number[]>;
svgResult: Accessor<SvgResult | null>;
setSvgResult: Setter<SvgResult | null>;
}>();
export function SvgContextProvider(props: { children: JSX.Element }) {
const [svgOptions, setSvgOptions] = createStore<RenderOptions>({
bgColor: "#ffffff",
fgColor: "#000000",
bgImgFile: null,
fgImgFile: null,
pixelateFgImg: false,
pixelateBgImg: false,
});
const { inputQr, outputQr, setOutputQr } = useQrContext();
const [selections, setSelectionsInPlace] = createSignal<BoxSelection[]>([], {
equals: false,
});
const [scaleX, setScaleXInPlace] = createSignal<number[]>([], {
equals: false,
});
const [scaleY, setScaleYInPlace] = createSignal<number[]>([], {
equals: false,
});
const [svgResult, setSvgResult] = createSignal<SvgResult | null>(null);
createEffect(() => {
try {
// NOTE: Version and Margin cannot be reused, so must be created each time
let qrOptions = new QrOptions()
.min_version(new Version(inputQr.minVersion))
.min_ecl(inputQr.minEcl)
.mask(inputQr.mask!) // null makes more sense than undefined
.mode(inputQr.mode!) // null makes more sense than undefined
.margin(new Margin(10))
.margin(
new Margin(0)
.setTop(inputQr.margin.top)
.setRight(inputQr.margin.right)
.setBottom(inputQr.margin.bottom)
.setLeft(inputQr.margin.left)
);
let m = get_matrix(inputQr.text, qrOptions);
setOutputQr({
text: inputQr.text,
version: m.version["0"],
ecl: m.ecl,
mode: m.mode,
mask: m.mask,
margin: {
top: m.margin.top,
right: m.margin.right,
bottom: m.margin.bottom,
left: m.margin.left,
},
});
const matrixLength = m.width() * m.height();
if (matrixLength !== untrack(scaleX).length) {
console.log("setScale");
setSelectionsInPlace([]);
setScaleXInPlace(Array(matrixLength).fill(100));
setScaleYInPlace(Array(matrixLength).fill(100));
}
qrOptions = new QrOptions()
.min_version(new Version(m.version[0]))
.min_ecl(m.ecl)
.mask(m.mask)
.mode(m.mode);
let svgOpts = new SvgOptions()
.foreground(svgOptions.fgColor)
.background(svgOptions.bgColor)
.scale_x_matrix(new Uint8Array(scaleX()))
.scale_y_matrix(new Uint8Array(scaleY()));
// infallible b/c outputQr contains successful options
setSvgResult(get_svg(inputQr.text, qrOptions, svgOpts));
} catch (e) {
setOutputQr(e as QrError);
setSvgResult(null);
}
});
return (
<SvgContext.Provider
value={{
svgOptions,
setSvgOptions,
svgResult,
setSvgResult,
selections,
setSelectionsInPlace,
scaleX,
setScaleXInPlace,
scaleY,
setScaleYInPlace,
}}
>
{props.children}
</SvgContext.Provider>
);
}
export function useSvgContext() {
const context = useContext(SvgContext);
if (!context) {
throw new Error("useSvgContext: used outside SvgContextProvider");
}
return context;
}

Wyświetl plik

@ -1,9 +1,9 @@
import { Switch, Match, createSignal } from "solid-js";
import { createSignal } from "solid-js";
import { clientOnly } from "@solidjs/start";
import { Editor } from "~/components/editor/QrEditor";
import { FlatButton } from "~/components/Button";
import QrPreview from "~/components/preview/QrPreview";
import init from "fuqr";
import { Editor } from "~/components/Editor";
import SvgPreview from "~/components/qr/SvgPreview";
import { SvgContextProvider } from "~/lib/SvgContext";
const QrContextProvider = clientOnly(async () => {
await init();
@ -12,38 +12,17 @@ const QrContextProvider = clientOnly(async () => {
};
});
// const MODULE_NAMES = [
// "Data",
// "Finder",
// "Alignment",
// "Timing",
// "Format",
// "Version",
// ] as const;
enum Stage {
Create,
Customize,
}
export default function Home() {
const [stage, setStage] = createSignal(Stage.Create);
return (
<QrContextProvider>
<SvgContextProvider>
<main class="max-w-screen-lg mx-auto">
<Switch>
<Match when={stage() == Stage.Create}>
<div class="flex gap-4 flex-wrap">
<Editor />
<div class="flex-1 min-w-200px sticky top-0 self-start p-4">
<SvgPreview />
</div>
</div>
</Match>
{/* <Match when={stage() == Stage.Customize}></Match> */}
</Switch>
<main class="max-w-screen-2xl mx-auto p-4">
<div class="flex gap-4 flex-wrap">
<Editor />
<div class="flex-grow-1 min-w-200px sticky top-0 self-start p-4">
<QrPreview />
</div>
</div>
</main>
</SvgContextProvider>
</QrContextProvider>
);
}

Wyświetl plik

@ -0,0 +1,101 @@
/*
Atom One Dark by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
base: #282c34
mono-1: #abb2bf
mono-2: #818896
mono-3: #5c6370
hue-1: #56b6c2
hue-2: #61aeee
hue-3: #c678dd
hue-4: #98c379
hue-5: #e06c75
hue-5-2: #be5046
hue-6: #d19a66
hue-6-2: #e6c07b
*/
.hljs-wrapper > * {
border-radius: 4px;
}
.hljs-wrapper > * > * {
border-bottom-right-radius: 4px;
}
pre[class*='language-'] {
color: #abb2bf;
background: #282c34;
}
.hljs-comment,
.hljs-quote {
color: #5c6370;
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #c678dd;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string {
color: #98c379;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #d19a66;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #61aeee;
}
.hljs-built_in,
.hljs-title.class_,
.hljs-class .hljs-title {
color: #e6c07b;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}

Wyświetl plik

@ -15,6 +15,26 @@ export default defineConfig({
subtle: "#1a1b1c",
},
},
animation: {
"content-show": {
from: {
opacity: 0,
transform: "scale(0.96);",
},
to: {
opacity: 1,
},
},
"content-hide": {
from: {
opacity: 1,
},
to: {
opacity: 0,
transform: "scale(0.96);",
},
},
},
},
preflights: [
{