web workers but sometimes flaky

main
Kyle Zheng 2024-08-21 23:50:21 -04:00
rodzic ce52b78911
commit 9ac333bfd9
10 zmienionych plików z 327 dodań i 255 usunięć

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.9 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.1 KiB

Wyświetl plik

@ -58,7 +58,7 @@ const Module = {
SeparatorOFF: 12,
};
export async function renderCanvas(qr, params, ctx) {
export async function renderCanvas(qr, params, canvas) {
const unit = 3;
const pixel = 1;
@ -68,10 +68,17 @@ export async function renderCanvas(qr, params, ctx) {
const bg = params["Background"];
const alignment = params["Alignment pattern"];
const timing = params["Timing pattern"];
const file = params["Image"];
let file = params["Image"];
if (file == null) {
file = await fetch(
"https://upload.wikimedia.org/wikipedia/commons/1/14/The_Widow_%28Boston_Public_Library%29_%28cropped%29.jpg"
).then((res) => res.blob());
}
const image = await createImageBitmap(file)
const pixelWidth = matrixWidth + 2 * margin;
const canvasSize = pixelWidth * unit;
const ctx = canvas.getContext("2d");
ctx.canvas.width = canvasSize;
ctx.canvas.height = canvasSize;
@ -91,27 +98,10 @@ export async function renderCanvas(qr, params, ctx) {
}
}
const image = new Image();
if (file != null) {
image.src = URL.createObjectURL(file);
} else {
// if canvas tainted, need to reload
// https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
image.crossOrigin = "anonymous";
image.src =
"https://upload.wikimedia.org/wikipedia/commons/1/14/The_Widow_%28Boston_Public_Library%29_%28cropped%29.jpg";
}
await image.decode();
ctx.filter = `brightness(${params["Brightness"]}) contrast(${params["Contrast"]})`;
ctx.drawImage(image, 0, 0, canvasSize, canvasSize);
ctx.filter = "none";
if (file != null) {
URL.revokeObjectURL(image.src);
}
const imageData = ctx.getImageData(0, 0, canvasSize, canvasSize);
const data = imageData.data;

Wyświetl plik

@ -89,7 +89,7 @@ export function renderSVG(qr, params) {
svg += `<circle cx="${x + 3.5}" cy="${y + 6.5}" r="0.5"/>`;
switch (params["Finder pattern"]) {
case "Atom": {
case "Atom":
let r1 = 0.98;
let r2 = 1.5;
@ -101,11 +101,11 @@ export function renderSVG(qr, params) {
svg += `M${x + 3.5},${y + 3.5 - 3 * r2}a${r1},${r2} 0,0,1 0,${6 * r2}a${r1},${r2} 0,0,1 0,${-6 * r2}`;
break;
}
case "Planet": {
case "Planet":
svg += `<path fill="none" stroke-width="0.1" stroke="${fg}" stroke-dasharray="0.5 0.65" d="`;
svg += `M${x + 3.5},${y + 0.5}a3,3 0,0,1 0,6a3,3 0,0,1 0-6`;
}
break;
}
svg += `"/></g>`;
}

Wyświetl plik

@ -0,0 +1,40 @@
let prevToken = { canceled: true };
onmessage = async ({ data: { type, url, qr, params, timeoutId } }) => {
prevToken.canceled = true;
const token = { canceled: false };
prevToken = token;
try {
switch (type) {
case "svg": {
const { renderSVG } = await import(url);
const svg = await renderSVG(qr, params);
if (token.canceled) {
return postMessage({ type: "canceled", timeoutId });
}
postMessage({ type, svg, timeoutId });
break;
}
case "canvas": {
const { renderCanvas } = await import(url);
const canvas = new OffscreenCanvas(0, 0);
await renderCanvas(qr, params, canvas);
if (token.canceled) {
return postMessage({ type: "canceled", timeoutId });
}
const bitmap = canvas.transferToImageBitmap();
postMessage({ type, bitmap, timeoutId }, [bitmap]);
break;
}
}
} catch (error) {
postMessage({
type: "error",
error,
timeoutId,
});
}
};

Wyświetl plik

@ -0,0 +1,39 @@
// pre-generated thumbnail qrcode
const PREVIEW_OUTPUTQR = {
text: "https://qrfra.me",
// prettier-ignore
matrix: [3,3,3,3,3,3,3,12,8,0,0,0,0,12,3,3,3,3,3,3,3,3,2,2,2,2,2,3,12,9,0,0,1,1,12,3,2,2,2,2,2,3,3,2,3,3,3,2,3,12,8,1,0,1,1,12,3,2,3,3,3,2,3,3,2,3,3,3,2,3,12,9,1,0,1,0,12,3,2,3,3,3,2,3,3,2,3,3,3,2,3,12,8,0,1,0,0,12,3,2,3,3,3,2,3,3,2,2,2,2,2,3,12,9,1,1,1,1,12,3,2,2,2,2,2,3,3,3,3,3,3,3,3,12,7,6,7,6,7,12,3,3,3,3,3,3,3,12,12,12,12,12,12,12,12,8,0,1,0,0,12,12,12,12,12,12,12,12,9,9,9,9,9,8,7,9,9,1,0,1,0,9,8,9,8,9,8,9,8,0,0,0,1,0,0,6,1,0,0,1,0,0,1,1,1,1,1,1,1,1,1,1,0,0,1,0,7,1,0,0,1,0,1,1,1,1,0,0,1,1,0,0,1,1,0,0,0,6,0,0,1,1,1,1,1,0,0,1,1,1,0,0,1,0,0,1,0,0,7,1,1,0,0,0,1,1,1,0,1,1,0,0,1,12,12,12,12,12,12,12,12,9,1,0,0,0,0,0,1,1,1,1,0,1,3,3,3,3,3,3,3,12,9,1,1,1,1,1,0,1,0,0,1,1,0,3,2,2,2,2,2,3,12,8,1,0,0,0,1,0,1,1,1,1,0,0,3,2,3,3,3,2,3,12,9,0,0,0,1,1,1,1,1,1,0,0,0,3,2,3,3,3,2,3,12,9,0,1,1,0,0,0,0,1,0,1,1,0,3,2,3,3,3,2,3,12,9,1,1,1,1,0,1,1,0,0,1,0,0,3,2,2,2,2,2,3,12,9,0,0,0,0,1,0,0,1,1,1,0,0,3,3,3,3,3,3,3,12,9,1,0,1,1,0,1,1,0,1,0,1,0],
version: 1,
ecl: 0, //ECL.Low
mode: 2, // Mode.Byte
mask: 2, // Mask.M2
};
onmessage = async ({ data: { type, url, params, timeoutId } }) => {
try {
switch (type) {
case "svg": {
const { renderSVG } = await import(url);
const svg = await renderSVG(PREVIEW_OUTPUTQR, params);
postMessage({ type, svg, timeoutId });
break;
}
case "canvas": {
const { renderCanvas } = await import(url);
const canvas = new OffscreenCanvas(0, 0);
await renderCanvas(PREVIEW_OUTPUTQR, params, canvas);
const bitmap = canvas.transferToImageBitmap();
postMessage({ type, bitmap, timeoutId }, [bitmap]);
break;
}
}
} catch (error) {
postMessage({
type: "error",
error,
timeoutId,
});
}
};

Wyświetl plik

@ -8,16 +8,10 @@ import {
defaultParams,
paramsEqual,
parseParamsSchema,
type Params,
type ParamsSchema,
} from "~/lib/params";
import { PRESET_CODE } from "~/lib/presets";
import {
PREVIEW_OUTPUTQR,
useQrContext,
type RenderCanvas,
type RenderSVG,
} from "~/lib/QrContext";
import { useQrContext, type RenderType } from "~/lib/QrContext";
import { FillButton, FlatButton } from "../Button";
import { Collapsible } from "../Collapsible";
import { IconButtonDialog } from "../Dialog";
@ -52,14 +46,13 @@ function isPreset(key: string): key is keyof typeof PRESET_CODE {
export function Editor(props: Props) {
const {
setInputQr,
setRenderSVG,
setRenderCanvas,
renderFuncKey,
setRenderFuncKey,
paramsSchema,
setParamsSchema,
params,
setParams,
renderKey,
setRenderKey,
setRender,
} = useQrContext();
const [code, setCode] = createSignal(PRESET_CODE.Square);
@ -79,6 +72,9 @@ export function Editor(props: Props) {
Minimal: "",
});
let thumbWorker: Worker | null = null;
const timeoutIdMap = new Map<NodeJS.Timeout, string>();
onMount(async () => {
const storedFuncKeys = localStorage.getItem(CUSTOM_FUNCS);
let keys;
@ -92,20 +88,22 @@ export function Editor(props: Props) {
for (const key of keys) {
if (isPreset(key)) {
// const tryThumb = localStorage.getItem(`${key}_thumb`);
// if (tryThumb != null) {
// setThumbs(key, tryThumb);
// continue;
// } else {
// No try-catch b/c presets should not have errors
const { renderSVG, renderCanvas, parsedParamsSchema } =
await importCode(PRESET_CODE[key]);
await updateThumbnail(key, renderSVG, renderCanvas, parsedParamsSchema);
// }
const tryThumb = localStorage.getItem(`${key}_thumb`);
if (tryThumb != null) {
setThumbs(key, tryThumb);
continue;
} else {
// preset CAN error out, e.g. when importing 3rd party dep
try {
const { type, url, parsedParamsSchema } = await importCode(
PRESET_CODE[key]
);
updateThumbnail(key, type, url, parsedParamsSchema);
} catch (e) {
// skippa
}
}
}
const thumb = localStorage.getItem(`${key}_thumb`) ?? FALLBACK_THUMB;
setThumbs(key, thumb);
}
});
@ -119,7 +117,7 @@ export function Editor(props: Props) {
};
const setExistingKey = (key: string) => {
setRenderFuncKey(key);
setRenderKey(key);
if (isPreset(key)) {
trySetCode(PRESET_CODE[key], false);
} else {
@ -133,50 +131,43 @@ export function Editor(props: Props) {
const importCode = async (code: string) => {
const blob = new Blob([code], { type: "text/javascript" });
// This url is cleaned up in trySetCode()
const url = URL.createObjectURL(blob);
// TODO check perf of caching functions
const {
renderSVG,
renderCanvas,
paramsSchema: rawParamsSchema,
} = await import(/* @vite-ignore */ url).finally(() =>
URL.revokeObjectURL(url)
);
} = await import(/* @vite-ignore */ url);
if (typeof renderCanvas !== "function" && typeof renderSVG !== "function") {
let type = "" as RenderType;
if (typeof renderSVG === "function") {
type = "svg";
}
if (typeof renderCanvas === "function") {
if (type) {
throw new Error("renderSVG and renderCanvas cannot both be exported");
}
type = "canvas";
}
if (!type) {
throw new Error("renderSVG or renderCanvas must be exported");
} else if (
typeof renderCanvas === "function" &&
typeof renderSVG === "function"
) {
throw new Error("renderSVG and renderCanvas cannot both be exported");
}
// TODO see impl, user set default and props might be wrong
const parsedParamsSchema = parseParamsSchema(rawParamsSchema);
return { renderSVG, renderCanvas, parsedParamsSchema };
return { type, url, parsedParamsSchema };
};
const trySetCode = async (code: string, changed: boolean) => {
try {
const { renderSVG, renderCanvas, parsedParamsSchema } = await importCode(
code
);
if (
typeof renderCanvas !== "function" &&
typeof renderSVG !== "function"
) {
throw new Error("renderSVG or renderCanvas must be exported");
} else if (
typeof renderCanvas === "function" &&
typeof renderSVG === "function"
) {
throw new Error("renderSVG and renderCanvas cannot both be exported");
}
// If import fails and code is unchanged, it should still load
// otherwise, changed code should only save if valid
if (!changed) setCode(code);
const { type, url, parsedParamsSchema } = await importCode(code);
setCompileError(null);
setCode(code);
if (changed) setCode(code);
// batched b/c trigger rendering effect
batch(() => {
@ -184,70 +175,87 @@ export function Editor(props: Props) {
setParams(defaultParams(parsedParamsSchema));
}
setParamsSchema(parsedParamsSchema); // always update in case different property order
setRenderSVG(() => renderSVG ?? null);
setRenderCanvas(() => renderCanvas ?? null);
setRender((prev) => {
// TODO check perf of caching
if (prev != null) {
URL.revokeObjectURL(prev.url);
}
return { type, url };
});
});
if (changed) {
localStorage.setItem(renderFuncKey(), code);
updateThumbnail(
renderFuncKey(),
renderSVG,
renderCanvas,
parsedParamsSchema
);
localStorage.setItem(renderKey(), code);
updateThumbnail(renderKey(), type, url, parsedParamsSchema);
}
} catch (e) {
console.log("e", e!.toString());
console.error("e", e!.toString());
setCompileError(e!.toString());
}
};
const updateThumbnail = async (
renderKey: string,
renderSVG: RenderSVG | undefined,
renderCanvas: RenderCanvas | undefined,
const updateThumbnail = (
key: string,
type: "svg" | "canvas",
url: string,
parsedParamsSchema: ParamsSchema
) => {
try {
const defaultParams: Params = {};
Object.entries(parsedParamsSchema).forEach(([label, props]) => {
defaultParams[label] = props.default;
});
if (thumbWorker == null) setupThumbWorker();
const timeoutId = setTimeout(() => {
console.error(
`Thumbnail render took longer than 5 seconds, timed out!`,
timeoutId
);
timeoutIdMap.delete(timeoutId);
if (thumbWorker != null) {
thumbWorker.terminate();
thumbWorker = null;
}
}, 5000);
timeoutIdMap.set(timeoutId, key);
thumbWorker!.postMessage({
type,
url,
params: defaultParams(parsedParamsSchema),
timeoutId,
});
};
const setupThumbWorker = () => {
thumbWorker = new Worker("thumbnailWorker.js", { type: "module" });
thumbWorker.onmessage = (e) => {
clearTimeout(e.data.timeoutId);
const key = timeoutIdMap.get(e.data.timeoutId)!;
timeoutIdMap.delete(e.data.timeoutId);
let thumbnail;
if (renderSVG != null) {
// https://www.phpied.com/truth-encoding-svg-data-uris/
// Only need to encode #
thumbnail =
"data:image/svg+xml," +
(await renderSVG(PREVIEW_OUTPUTQR, defaultParams).replaceAll(
"#",
"%23"
));
} else {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
await renderCanvas!(PREVIEW_OUTPUTQR, defaultParams, ctx);
switch (e.data.type) {
case "svg":
thumbnail = "data:image/svg+xml," + e.data.svg.replaceAll("#", "%23");
break;
case "canvas":
const size = 96;
const smallCanvas = document.createElement("canvas");
const smallCanvas = document.createElement("canvas");
const size = 96;
smallCanvas.width = size;
smallCanvas.height = size;
const smallCtx = smallCanvas.getContext("2d")!;
smallCtx.drawImage(canvas, 0, 0, size, size);
thumbnail = smallCanvas.toDataURL("image/jpeg", 0.5);
smallCanvas.width = size;
smallCanvas.height = size;
const smallCtx = smallCanvas.getContext("2d")!;
smallCtx.drawImage(e.data.bitmap, 0, 0, size, size);
e.data.bitmap.close();
thumbnail = smallCanvas.toDataURL("image/jpeg", 0.5);
break;
case "error":
console.error(e.data.error);
return;
}
localStorage.setItem(`${renderKey}_thumb`, thumbnail);
if (!presetKeys.includes(renderKey)) {
setThumbs(renderKey, thumbnail);
}
} catch (e) {
console.error(`${renderKey} thumbnail render:`, e);
}
localStorage.setItem(`${key}_thumb`, thumbnail!);
setThumbs(key, thumbnail!);
};
};
const createAndSelectFunc = (name: string, code: string) => {
@ -261,7 +269,7 @@ export function Editor(props: Props) {
// TODO double setting thumbs
setThumbs(key, FALLBACK_THUMB);
setRenderFuncKey(key);
setRenderKey(key);
trySetCode(code, true);
};
@ -282,16 +290,16 @@ export function Editor(props: Props) {
Render function
</div>
<div class="flex gap-2">
<div class="flex items-center font-bold">{renderFuncKey()}</div>
<Show when={!presetKeys.includes(renderFuncKey())}>
<div class="flex items-center font-bold">{renderKey()}</div>
<Show when={!presetKeys.includes(renderKey())}>
<IconButtonDialog
title={`Rename ${renderFuncKey()}`}
title={`Rename ${renderKey()}`}
triggerTitle="Rename"
triggerChildren={<Pencil class="w-5 h-5" />}
onOpenAutoFocus={(e) => e.preventDefault()}
>
{(close) => {
const [rename, setRename] = createSignal(renderFuncKey());
const [rename, setRename] = createSignal(renderKey());
const [duplicate, setDuplicate] = createSignal(false);
let ref: HTMLInputElement;
@ -304,7 +312,7 @@ export function Editor(props: Props) {
defaultValue={rename()}
onChange={setRename}
onInput={() => duplicate() && setDuplicate(false)}
placeholder={renderFuncKey()}
placeholder={renderKey()}
/>
<div class="absolute p-1 text-sm text-red-600">
<Show when={duplicate()}>
@ -315,30 +323,28 @@ export function Editor(props: Props) {
class="px-3 py-2 float-right mt-4"
// input onChange runs after focus lost, so onMouseDown is too early
onClick={() => {
if (rename() === renderFuncKey()) return close();
if (rename() === renderKey()) return close();
if (funcKeys.includes(rename())) {
setDuplicate(true);
return;
}
localStorage.removeItem(renderFuncKey());
localStorage.removeItem(
`${renderFuncKey()}_thumb`
);
localStorage.removeItem(renderKey());
localStorage.removeItem(`${renderKey()}_thumb`);
const thumb = thumbs[renderFuncKey()];
const thumb = thumbs[renderKey()];
localStorage.setItem(rename(), code());
localStorage.setItem(`${rename()}_thumb`, thumb);
setThumbs(rename(), thumb);
setThumbs(renderFuncKey(), undefined!);
setThumbs(renderKey(), undefined!);
setFuncKeys(
funcKeys.indexOf(renderFuncKey()),
funcKeys.indexOf(renderKey()),
rename()
);
setRenderFuncKey(rename());
setRenderKey(rename());
close();
}}
>
@ -349,7 +355,7 @@ export function Editor(props: Props) {
}}
</IconButtonDialog>
<IconButtonDialog
title={`Delete ${renderFuncKey()}`}
title={`Delete ${renderKey()}`}
triggerTitle="Delete"
triggerChildren={<Trash2 class="w-5 h-5" />}
>
@ -361,14 +367,12 @@ export function Editor(props: Props) {
<div class="flex justify-end gap-2">
<FillButton
onMouseDown={() => {
localStorage.removeItem(renderFuncKey());
localStorage.removeItem(
`${renderFuncKey()}_thumb`
);
setThumbs(renderFuncKey(), undefined!);
localStorage.removeItem(renderKey());
localStorage.removeItem(`${renderKey()}_thumb`);
setThumbs(renderKey(), undefined!);
setFuncKeys((keys) =>
keys.filter((key) => key !== renderFuncKey())
keys.filter((key) => key !== renderKey())
);
setExistingKey(funcKeys[0]);
@ -391,7 +395,7 @@ export function Editor(props: Props) {
<Preview
onClick={() => setExistingKey(key)}
label={key}
active={renderFuncKey() === key}
active={renderKey() === key}
>
<img class="rounded-sm" src={thumbs[key]} />
</Preview>
@ -437,8 +441,8 @@ export function Editor(props: Props) {
<CodeEditor
initialValue={code()}
onSave={(code) => {
if (presetKeys.includes(renderFuncKey())) {
createAndSelectFunc(renderFuncKey(), code);
if (presetKeys.includes(renderKey())) {
createAndSelectFunc(renderKey(), code);
} else {
trySetCode(code, true);
}

Wyświetl plik

@ -1,13 +1,6 @@
import { QrError } from "fuqr";
import Download from "lucide-solid/icons/download";
import {
Match,
Show,
Switch,
createEffect,
createSignal,
untrack,
} from "solid-js";
import { Match, Show, Switch, createEffect, createSignal } from "solid-js";
import { useQrContext, type OutputQr } from "~/lib/QrContext";
import {
ECL_LABELS,
@ -47,7 +40,7 @@ export default function QrPreview(props: Props) {
</Match>
<Match when={outputQr() === QrError.InvalidEncoding}>
{`Input cannot be encoded in ${
// @ts-expect-error props.mode not null b/c InvalidEncoding implies mode
// @ts-expect-error props.mode not null b/c InvalidEncoding requires mode
MODE_NAMES[inputQr.mode + 1]
} mode`}
</Match>
@ -71,9 +64,8 @@ export default function QrPreview(props: Props) {
function RenderedQrCode() {
const {
outputQr: _outputQr,
getRenderSVG,
getRenderCanvas,
renderFuncKey,
render,
renderKey,
params,
paramsSchema,
} = useQrContext();
@ -86,52 +78,77 @@ function RenderedQrCode() {
const [canvasDims, setCanvasDims] = createSignal({ width: 0, height: 0 });
let prevFuncKey = "";
let worker: Worker | null = null;
const timeoutIdSet = new Set<NodeJS.Timeout>();
let renderCount = 0;
createEffect(async () => {
const render = getRenderSVG();
const fallbackRender = getRenderCanvas();
const r = render();
// Track store without passing store into function and leaking extra params
// Track store without leaking extra params
const paramsCopy: Params = {};
Object.keys(paramsSchema()).forEach((key) => {
paramsCopy[key] = params[key];
});
if (render == null && fallbackRender == null) return; // only true on page load
const currentRender = ++renderCount;
// all reactive deps must be above early return!
// true on page load
if (r == null) return;
prevFuncKey = untrack(renderFuncKey);
try {
// users can arbitrarily manipulate function args
// outputQr (big) is frozen, and params (small) is copied
if (worker == null) setupWorker();
if (render != null) {
const svgString = await render(outputQr(), paramsCopy);
if (currentRender !== renderCount) {
// race condition
return;
}
svgParent.innerHTML = svgString;
} else {
const ctx = canvas.getContext("2d")!;
ctx.reset();
// race condition can't be solved without double buffering
await fallbackRender!(outputQr(), paramsCopy, ctx);
setCanvasDims({ width: ctx.canvas.width, height: ctx.canvas.height });
const timeoutId = setTimeout(() => {
console.error(`Render took longer than 5 seconds, timed out!`, timeoutId);
timeoutIdSet.delete(timeoutId);
if (worker != null) {
worker.terminate();
worker = null
}
}, 5000);
timeoutIdSet.add(timeoutId);
setRuntimeError(null);
} catch (e) {
setRuntimeError(e!.toString());
console.error(`${prevFuncKey} render:`, e);
}
worker!.postMessage({
type: r.type,
url: r.url,
qr: outputQr(),
params: paramsCopy,
timeoutId,
});
return () => {
worker?.terminate();
timeoutIdSet.forEach((timeout) => clearTimeout(timeout));
};
});
const setupWorker = () => {
console.log("new worker")
worker = new Worker("renderWorker.js", { type: "module" });
worker.onmessage = (e) => {
clearTimeout(e.data.timeoutId);
timeoutIdSet.delete(e.data.timeoutId);
switch (e.data.type) {
case "svg":
svgParent.innerHTML = e.data.svg;
setRuntimeError(null);
break;
case "canvas":
canvas
.getContext("bitmaprenderer")!
.transferFromImageBitmap(e.data.bitmap);
setCanvasDims({ width: canvas.width, height: canvas.height });
setRuntimeError(null);
break;
case "error":
console.error(e.data.error);
break;
// case "canceled":
// break;
}
};
};
const filename = () => {
const s = outputQr().text.slice(0, 32);
// : and / are not valid filename chars, so it looks bad
@ -141,22 +158,24 @@ function RenderedQrCode() {
return (
<>
<div class="checkboard aspect-[1/1] border rounded-md relative overflow-hidden">
<Show when={getRenderSVG() != null}>
<div ref={svgParent!}></div>
</Show>
<Show when={getRenderCanvas() != null}>
<canvas
class="w-full h-full image-render-pixel"
ref={canvas!}
></canvas>
</Show>
<Switch>
<Match when={render()?.type === "svg"}>
<div ref={svgParent!}></div>
</Match>
<Match when={render()?.type === "canvas"}>
<canvas
class="w-full h-full image-render-pixel"
ref={canvas!}
></canvas>
</Match>
</Switch>
</div>
<Show when={runtimeError() != null}>
<div class="text-red-100 bg-red-950 px-2 py-1 rounded-md">
{runtimeError()}
</div>
</Show>
<Show when={getRenderCanvas() != null}>
<Show when={render()?.type === "canvas"}>
<div class="text-center">
{canvasDims().width}x{canvasDims().height} px
</div>
@ -194,7 +213,7 @@ function RenderedQrCode() {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
// TODO allow adjust resolution/aspect ratio
const size = 300 //(outputQr().version * 4 + 17) * 10;
const size = 300; //(outputQr().version * 4 + 17) * 10;
canvas.width = size;
canvas.height = size;
@ -214,7 +233,7 @@ function RenderedQrCode() {
<Download size={20} />
Download PNG
</FlatButton>
<Show when={getRenderSVG() != null}>
<Show when={render()?.type === "svg"}>
<FlatButton
class="inline-flex justify-center items-center gap-1 flex-1 px-3 py-2"
onClick={async () => {

Wyświetl plik

@ -43,14 +43,10 @@ export const QrContext = createContext<{
inputQr: InputQr;
setInputQr: SetStoreFunction<InputQr>;
outputQr: Accessor<OutputQr | QrError>;
getRenderSVG: Accessor<RenderSVG | null>;
setRenderSVG: Setter<RenderSVG | null>;
getRenderCanvas: Accessor<RenderCanvas | null>;
setRenderCanvas: Setter<RenderCanvas | null>;
renderFuncKey: Accessor<string>;
setRenderFuncKey: Setter<string>;
render: Accessor<Render | null>;
setRender: Setter<Render | null>;
renderKey: Accessor<string>;
setRenderKey: Setter<string>;
params: Params;
setParams: SetStoreFunction<Params>;
paramsSchema: Accessor<ParamsSchema>;
@ -65,6 +61,14 @@ export type RenderCanvas = (
export type RenderSVG = (qr: OutputQr, params: Params) => string;
const renderTypes = ["svg", "canvas"] as const;
export type RenderType = (typeof renderTypes)[number];
type Render = {
type: RenderType;
url: string;
};
export function QrContextProvider(props: { children: JSX.Element }) {
const [inputQr, setInputQr] = createStore<InputQr>({
text: "https://qrframe.kylezhe.ng",
@ -76,11 +80,8 @@ export function QrContextProvider(props: { children: JSX.Element }) {
mask: null,
});
const [renderCanvas, setRenderCanvas] = createSignal<RenderCanvas | null>(
null
);
const [renderSVG, setRenderSVG] = createSignal<RenderSVG | null>(null);
const [renderFuncKey, setRenderFuncKey] = createSignal("");
const [renderKey, setRenderKey] = createSignal<string>("Square");
const [render, setRender] = createSignal<Render | null>(null);
const [paramsSchema, setParamsSchema] = createSignal<ParamsSchema>({});
const [params, setParams] = createStore({});
@ -122,12 +123,10 @@ export function QrContextProvider(props: { children: JSX.Element }) {
inputQr,
setInputQr,
outputQr,
getRenderSVG: renderSVG,
setRenderSVG,
getRenderCanvas: renderCanvas,
setRenderCanvas,
renderFuncKey,
setRenderFuncKey,
render,
setRender,
renderKey,
setRenderKey,
params,
setParams,
paramsSchema,
@ -147,13 +146,4 @@ export function useQrContext() {
return context;
}
// pre generated b/c needed often for thumbnails (generated on every save/load)
export const PREVIEW_OUTPUTQR = {
text: "https://qrfra.me",
// prettier-ignore
matrix: [3,3,3,3,3,3,3,12,8,0,0,0,0,12,3,3,3,3,3,3,3,3,2,2,2,2,2,3,12,9,0,0,1,1,12,3,2,2,2,2,2,3,3,2,3,3,3,2,3,12,8,1,0,1,1,12,3,2,3,3,3,2,3,3,2,3,3,3,2,3,12,9,1,0,1,0,12,3,2,3,3,3,2,3,3,2,3,3,3,2,3,12,8,0,1,0,0,12,3,2,3,3,3,2,3,3,2,2,2,2,2,3,12,9,1,1,1,1,12,3,2,2,2,2,2,3,3,3,3,3,3,3,3,12,7,6,7,6,7,12,3,3,3,3,3,3,3,12,12,12,12,12,12,12,12,8,0,1,0,0,12,12,12,12,12,12,12,12,9,9,9,9,9,8,7,9,9,1,0,1,0,9,8,9,8,9,8,9,8,0,0,0,1,0,0,6,1,0,0,1,0,0,1,1,1,1,1,1,1,1,1,1,0,0,1,0,7,1,0,0,1,0,1,1,1,1,0,0,1,1,0,0,1,1,0,0,0,6,0,0,1,1,1,1,1,0,0,1,1,1,0,0,1,0,0,1,0,0,7,1,1,0,0,0,1,1,1,0,1,1,0,0,1,12,12,12,12,12,12,12,12,9,1,0,0,0,0,0,1,1,1,1,0,1,3,3,3,3,3,3,3,12,9,1,1,1,1,1,0,1,0,0,1,1,0,3,2,2,2,2,2,3,12,8,1,0,0,0,1,0,1,1,1,1,0,0,3,2,3,3,3,2,3,12,9,0,0,0,1,1,1,1,1,1,0,0,0,3,2,3,3,3,2,3,12,9,0,1,1,0,0,0,0,1,0,1,1,0,3,2,3,3,3,2,3,12,9,1,1,1,1,0,1,1,0,0,1,0,0,3,2,2,2,2,2,3,12,9,0,0,0,0,1,0,0,1,1,1,0,0,3,3,3,3,3,3,3,12,9,1,0,1,1,0,1,1,0,1,0,1,0],
version: 1,
ecl: ECL.Low,
mode: Mode.Byte,
mask: Mask.M2,
};

Wyświetl plik

@ -58,7 +58,7 @@ const Module = {
SeparatorOFF: 12,
};
export async function renderCanvas(qr, params, ctx) {
export async function renderCanvas(qr, params, canvas) {
const unit = 3;
const pixel = 1;
@ -68,10 +68,17 @@ export async function renderCanvas(qr, params, ctx) {
const bg = params["Background"];
const alignment = params["Alignment pattern"];
const timing = params["Timing pattern"];
const file = params["Image"];
let file = params["Image"];
if (file == null) {
file = await fetch(
"https://upload.wikimedia.org/wikipedia/commons/1/14/The_Widow_%28Boston_Public_Library%29_%28cropped%29.jpg"
).then((res) => res.blob());
}
const image = await createImageBitmap(file)
const pixelWidth = matrixWidth + 2 * margin;
const canvasSize = pixelWidth * unit;
const ctx = canvas.getContext("2d");
ctx.canvas.width = canvasSize;
ctx.canvas.height = canvasSize;
@ -91,27 +98,10 @@ export async function renderCanvas(qr, params, ctx) {
}
}
const image = new Image();
if (file != null) {
image.src = URL.createObjectURL(file);
} else {
// if canvas tainted, need to reload
// https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
image.crossOrigin = "anonymous";
image.src =
"https://upload.wikimedia.org/wikipedia/commons/1/14/The_Widow_%28Boston_Public_Library%29_%28cropped%29.jpg";
}
await image.decode();
ctx.filter = \`brightness(\${params["Brightness"]}) contrast(\${params["Contrast"]})\`;
ctx.drawImage(image, 0, 0, canvasSize, canvasSize);
ctx.filter = "none";
if (file != null) {
URL.revokeObjectURL(image.src);
}
const imageData = ctx.getImageData(0, 0, canvasSize, canvasSize);
const data = imageData.data;

Wyświetl plik

@ -89,7 +89,7 @@ export function renderSVG(qr, params) {
svg += \`<circle cx="\${x + 3.5}" cy="\${y + 6.5}" r="0.5"/>\`;
switch (params["Finder pattern"]) {
case "Atom": {
case "Atom":
let r1 = 0.98;
let r2 = 1.5;
@ -101,11 +101,11 @@ export function renderSVG(qr, params) {
svg += \`M\${x + 3.5},\${y + 3.5 - 3 * r2}a\${r1},\${r2} 0,0,1 0,\${6 * r2}a\${r1},\${r2} 0,0,1 0,\${-6 * r2}\`;
break;
}
case "Planet": {
case "Planet":
svg += \`<path fill="none" stroke-width="0.1" stroke="\${fg}" stroke-dasharray="0.5 0.65" d="\`;
svg += \`M\${x + 3.5},\${y + 0.5}a3,3 0,0,1 0,6a3,3 0,0,1 0-6\`;
}
break;
}
svg += \`"/></g>\`;
}