kopia lustrzana https://github.com/zhengkyl/qrframe
support ui parameters by switching to import module
rodzic
60b4a76d60
commit
f9c6998d4f
|
@ -2989,8 +2989,8 @@ packages:
|
|||
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
zod@3.23.6:
|
||||
resolution: {integrity: sha512-RTHJlZhsRbuA8Hmp/iNL7jnfc4nZishjsanDAfEY1QpDQZCahUp3xDzl+zfweE9BklxMUcgBgS1b7Lvie/ZVwA==}
|
||||
zod@3.23.8:
|
||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||
|
||||
snapshots:
|
||||
|
||||
|
@ -5980,7 +5980,7 @@ snapshots:
|
|||
unenv: 1.9.0
|
||||
unstorage: 1.10.2(ioredis@5.4.1)
|
||||
vite: 5.2.11(@types/node@20.12.8)(terser@5.31.0)
|
||||
zod: 3.23.6
|
||||
zod: 3.23.8
|
||||
transitivePeerDependencies:
|
||||
- '@azure/app-configuration'
|
||||
- '@azure/cosmos'
|
||||
|
@ -6129,4 +6129,4 @@ snapshots:
|
|||
compress-commons: 6.0.2
|
||||
readable-stream: 4.5.2
|
||||
|
||||
zod@3.23.6: {}
|
||||
zod@3.23.8: {}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
type Props = {
|
||||
color: string;
|
||||
setColor: (c: string) => void;
|
||||
value: string;
|
||||
setValue: (c: string) => void;
|
||||
};
|
||||
export function ColorInput(props: Props) {
|
||||
return (
|
||||
<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}
|
||||
{props.value}
|
||||
<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)"
|
||||
type="color"
|
||||
value={props.color}
|
||||
onInput={(e) => props.setColor(e.target.value)}
|
||||
value={props.value}
|
||||
onInput={(e) => props.setValue(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
|
|
|
@ -13,8 +13,7 @@ export function ImageInput(props: Props) {
|
|||
<div class="inline-flex items-center gap-1">
|
||||
<input
|
||||
class="border rounded-md text-sm px-1 py-2 file:(bg-transparent border-none text-fore-base) hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)"
|
||||
// @ts-expect-error idk why this is angry
|
||||
ref={input}
|
||||
ref={input!}
|
||||
type="file"
|
||||
accept=".jpeg, .jpg, .png"
|
||||
onChange={(e) => {
|
||||
|
|
|
@ -14,7 +14,7 @@ import { vim } from "@replit/codemirror-vim";
|
|||
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { debounce } from "~/lib/util";
|
||||
import { Switch } from "./Switch";
|
||||
import { Switch } from "../Switch";
|
||||
|
||||
type Props = {
|
||||
onSave: (s: string) => void;
|
||||
|
@ -25,7 +25,7 @@ type Props = {
|
|||
|
||||
const INITIAL_VIM_MODE = false;
|
||||
|
||||
export function CodeInput(props: Props) {
|
||||
export function CodeEditor(props: Props) {
|
||||
let parent: HTMLDivElement;
|
||||
let view: EditorView;
|
||||
let modeComp = new Compartment();
|
|
@ -1,27 +1,24 @@
|
|||
import { For, Show, createSignal, onMount, 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 { ButtonGroup, ButtonGroupItem } from "../ButtonGroup";
|
||||
import { TextInput, TextareaInput } from "../TextInput";
|
||||
import { NumberInput } from "../NumberInput";
|
||||
import { GroupedSelect, Select } from "../Select";
|
||||
|
||||
import { createStore } from "solid-js/store";
|
||||
import { CodeInput } from "../CodeInput";
|
||||
import Trash2 from "lucide-solid/icons/trash-2";
|
||||
import Pencil from "lucide-solid/icons/pencil";
|
||||
import { IconButtonDialog } from "../Dialog";
|
||||
import Trash2 from "lucide-solid/icons/trash-2";
|
||||
import { For, Show, batch, createSignal, onMount } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import {
|
||||
PARAM_COMPONENTS,
|
||||
PARAM_DEFAULTS,
|
||||
PARAM_TYPES,
|
||||
type Params,
|
||||
type ParamsSchema,
|
||||
} from "~/lib/params";
|
||||
import { PRESET_FUNCS } from "~/lib/presetFuncs";
|
||||
import { useQrContext } from "~/lib/QrContext";
|
||||
import { FillButton, FlatButton } from "../Button";
|
||||
import { Collapsible } from "../Collapsible";
|
||||
import { IconButtonDialog } from "../Dialog";
|
||||
import { GroupedSelect } from "../Select";
|
||||
import { TextInput, TextareaInput } from "../TextInput";
|
||||
import { CodeEditor } from "./CodeEditor";
|
||||
import { Settings } from "./Settings";
|
||||
|
||||
type Props = {
|
||||
class?: string;
|
||||
|
@ -32,11 +29,14 @@ const USER_FUNC_KEYS_KEY = "userFuncKeys";
|
|||
|
||||
export function Editor(props: Props) {
|
||||
const {
|
||||
inputQr,
|
||||
setInputQr,
|
||||
setRenderFunc,
|
||||
renderFuncKey,
|
||||
setRenderFuncKey,
|
||||
paramsSchema,
|
||||
setParamsSchema,
|
||||
params,
|
||||
setParams,
|
||||
} = useQrContext();
|
||||
|
||||
const [code, setCode] = createSignal(PRESET_FUNCS.Square);
|
||||
|
@ -55,21 +55,88 @@ export function Editor(props: Props) {
|
|||
if (funcCode == null) continue;
|
||||
setUserFuncKeys(userFuncKeys.length, key);
|
||||
}
|
||||
|
||||
trySetCode(PRESET_FUNCS.Square)
|
||||
});
|
||||
|
||||
const trySetCode = (newCode: string) => {
|
||||
const trySetCode = async (newCode: string) => {
|
||||
let url;
|
||||
try {
|
||||
const render = new Function("qr", "ctx", newCode) as RenderFunc;
|
||||
const blob = new Blob([newCode], { type: "text/javascript" });
|
||||
url = URL.createObjectURL(blob);
|
||||
|
||||
const {
|
||||
renderCanvas,
|
||||
renderSVG,
|
||||
paramsSchema: rawParamsSchema,
|
||||
} = await import(/* @vite-ignore */ url);
|
||||
|
||||
if (renderCanvas == null && renderSVG == null) {
|
||||
throw new Error("One of renderCanvas and renderSVG must be exported");
|
||||
} else if (renderCanvas != null && renderSVG != null) {
|
||||
throw new Error("renderCanvas and renderSVG cannot both be exported");
|
||||
}
|
||||
|
||||
// TODO
|
||||
// refactor to parsing instead of validating...
|
||||
// maybe use zod?
|
||||
// for now this is easier and prevents obvious accidental crashing
|
||||
let parsedParamsSchema: ParamsSchema = {};
|
||||
if (typeof rawParamsSchema === "object") {
|
||||
for (const [key, value] of Object.entries(rawParamsSchema)) {
|
||||
if (
|
||||
value == null ||
|
||||
typeof value !== "object" ||
|
||||
!("type" in value) ||
|
||||
typeof value.type !== "string" ||
|
||||
!PARAM_TYPES.includes(value.type)
|
||||
) {
|
||||
continue;
|
||||
} else if (value.type === "Select") {
|
||||
if (
|
||||
!("options" in value) ||
|
||||
!Array.isArray(value.options) ||
|
||||
value.options.length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error prop types aren't validated yet, see above TODO
|
||||
parsedParamsSchema[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
setCode(newCode);
|
||||
setRenderFunc(() => render);
|
||||
setCompileError(null);
|
||||
setParamsSchema(parsedParamsSchema);
|
||||
|
||||
batch(() => {
|
||||
const defaultParams: Params = {};
|
||||
Object.entries(parsedParamsSchema).forEach(([label, props]) => {
|
||||
// null is a valid default value for ImageInput
|
||||
if (props.default !== undefined) {
|
||||
defaultParams[label] = props.default;
|
||||
} else if (props.type === "Select") {
|
||||
defaultParams[label] = props.options[0];
|
||||
} else {
|
||||
defaultParams[label] = PARAM_DEFAULTS[props.type];
|
||||
}
|
||||
});
|
||||
// todo we shouldn't override if paramSchema doesn't change
|
||||
setParams(defaultParams); // todo init with default values from schema
|
||||
setRenderFunc(() => renderCanvas);
|
||||
});
|
||||
|
||||
if (!PRESET_FUNCS.hasOwnProperty(renderFuncKey())) {
|
||||
localStorage.setItem(renderFuncKey(), newCode);
|
||||
}
|
||||
|
||||
setCompileError(null);
|
||||
} catch (e) {
|
||||
console.log("e", e!.toString());
|
||||
setCompileError(e!.toString());
|
||||
}
|
||||
URL.revokeObjectURL(url!);
|
||||
};
|
||||
|
||||
const createAndSelectFunc = (name: string, code: string) => {
|
||||
|
@ -93,55 +160,7 @@ export function Editor(props: Props) {
|
|||
setValue={(s) => setInputQr("text", s)}
|
||||
/>
|
||||
<Collapsible trigger="Settings">
|
||||
<div class="flex justify-between">
|
||||
<div class="text-sm py-2">Encoding mode</div>
|
||||
<Select
|
||||
options={MODE_NAMES}
|
||||
value={MODE_KEY[inputQr.mode!]}
|
||||
setValue={(name) => setInputQr("mode", MODE_VALUE[name])}
|
||||
/>
|
||||
</div>
|
||||
<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", { top: v, bottom: v, left: v, right: v })
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
<Settings />
|
||||
</Collapsible>
|
||||
<Collapsible trigger="Rendering" defaultOpen>
|
||||
<div class="mb-4">
|
||||
|
@ -278,7 +297,27 @@ export function Editor(props: Props) {
|
|||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<CodeInput
|
||||
<div class="flex flex-col gap-2 mb-4">
|
||||
<For each={Object.entries(paramsSchema())}>
|
||||
{([label, { type, ...props }]) => {
|
||||
return (
|
||||
<>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-sm py-2 w-48">{label}</div>
|
||||
{/* @ts-expect-error lose type b/c type and props destructured */}
|
||||
<Dynamic
|
||||
component={PARAM_COMPONENTS[type]}
|
||||
{...props}
|
||||
value={params[label]}
|
||||
setValue={(v: any) => setParams(label, v)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<CodeEditor
|
||||
initialValue={code()}
|
||||
onSave={(code) => {
|
||||
if (Object.keys(PRESET_FUNCS).includes(renderFuncKey())) {
|
||||
|
@ -294,368 +333,3 @@ export function Editor(props: Props) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row(props: {
|
||||
tooltip?: string;
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div class="text-sm py-2" title={props.tooltip}>
|
||||
{props.title}
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PRESET_FUNCS = {
|
||||
Square: `// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
Circle: `// qr, ctx are args
|
||||
const 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,
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const gradient = ctx.createRadialGradient(
|
||||
ctx.canvas.width / 2,
|
||||
ctx.canvas.height / 2,
|
||||
2 * pixelSize,
|
||||
ctx.canvas.width / 2,
|
||||
ctx.canvas.height / 2,
|
||||
20 * pixelSize,
|
||||
);
|
||||
|
||||
gradient.addColorStop(0, "red");
|
||||
gradient.addColorStop(1, "blue");
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
|
||||
const radius = pixelSize / 2;
|
||||
|
||||
const finderPos = [
|
||||
[qr.margin.left, qr.margin.top],
|
||||
[qr.matrixWidth - qr.margin.right - 7, qr.margin.top],
|
||||
[qr.margin.left, qr.matrixHeight - qr.margin.bottom - 7],
|
||||
];
|
||||
|
||||
for (const [x, y] of finderPos) {
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 3.5) * pixelSize, (y + 3.5) * pixelSize, 3.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 3.5) * pixelSize, (y + 3.5) * pixelSize, 2.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 3.5) * pixelSize, (y + 3.5) * pixelSize, 1.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
const xMid = qr.matrixWidth / 2;
|
||||
const yMid = qr.matrixHeight / 2;
|
||||
const maxDist = Math.sqrt(xMid * xMid + yMid + yMid);
|
||||
|
||||
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) {
|
||||
if (module === Module.FinderON) continue;
|
||||
if (module === Module.AlignmentON) {
|
||||
// Find top left corner of alignment square
|
||||
if (qr.matrix[(y - 1) * qr.matrixWidth + x] !== Module.AlignmentON &&
|
||||
qr.matrix[y * qr.matrixWidth + x - 1] !== Module.AlignmentON &&
|
||||
qr.matrix[y * qr.matrixWidth + x + 1] === Module.AlignmentON
|
||||
) {
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 2.5) * pixelSize, (y + 2.5) * pixelSize, 2.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 2.5) * pixelSize, (y + 2.5) * pixelSize, 1.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 2.5) * pixelSize, (y + 2.5) * pixelSize, 0.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
continue;
|
||||
};
|
||||
|
||||
const xCenter = x * pixelSize + radius;
|
||||
const yCenter = y * pixelSize + radius;
|
||||
|
||||
const xDist = Math.abs(xMid - x);
|
||||
const yDist = Math.abs(yMid - y);
|
||||
const scale = Math.sqrt(xDist * xDist + yDist * yDist) / maxDist * 0.7 + 0.5;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(xCenter, yCenter, radius * scale, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
"Camouflage": `// qr, ctx are args
|
||||
const 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,
|
||||
}
|
||||
|
||||
function splitmix32(a) {
|
||||
return function() {
|
||||
a |= 0;
|
||||
a = a + 0x9e3779b9 | 0;
|
||||
let t = a ^ a >>> 16;
|
||||
t = Math.imul(t, 0x21f0aaad);
|
||||
t = t ^ t >>> 15;
|
||||
t = Math.imul(t, 0x735a2d97);
|
||||
return ((t = t ^ t >>> 15) >>> 0) / 4294967296;
|
||||
}
|
||||
}
|
||||
|
||||
const seededRand = splitmix32(1 /* change seed to change pattern */);
|
||||
|
||||
// Randomly set pixels in margin
|
||||
for (let y = 0; y < qr.matrixHeight; y++) {
|
||||
for (let x = 0; x < qr.matrixWidth; x++) {
|
||||
if (y > qr.margin.top - 2 &&
|
||||
y < qr.matrixHeight - qr.margin.bottom + 1 &&
|
||||
x > qr.margin.left - 2 &&
|
||||
x < qr.matrixWidth - qr.margin.right + 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seededRand() > 0.5) qr.matrix[y * qr.matrixWidth + x] = Module.DataON;
|
||||
}
|
||||
}
|
||||
|
||||
const pixelSize = 20;
|
||||
const radius = pixelSize / 2;
|
||||
ctx.canvas.width = qr.matrixWidth * pixelSize;
|
||||
ctx.canvas.height = qr.matrixHeight * pixelSize;
|
||||
|
||||
const fg = "rgb(40, 70, 10)";
|
||||
const bg = "rgb(200, 200, 100)";
|
||||
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
const xMax = qr.matrixWidth - 1;
|
||||
const yMax = qr.matrixHeight - 1;
|
||||
|
||||
for (let y = 0; y < qr.matrixHeight; y++) {
|
||||
for (let x = 0; x < qr.matrixWidth; x++) {
|
||||
const module = qr.matrix[y * qr.matrixWidth + x];
|
||||
|
||||
const top = y > 0 && (qr.matrix[(y - 1) * qr.matrixWidth + x] & 1);
|
||||
const bottom = y < yMax && (qr.matrix[(y + 1) * qr.matrixWidth + x] & 1);
|
||||
const left = x > 0 && (qr.matrix[y * qr.matrixWidth + x - 1] & 1);
|
||||
const right = x < xMax && (qr.matrix[y * qr.matrixWidth + x + 1] & 1);
|
||||
|
||||
ctx.fillStyle = fg;
|
||||
|
||||
if (module & 1) {
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(
|
||||
x * pixelSize,
|
||||
y * pixelSize,
|
||||
pixelSize,
|
||||
pixelSize,
|
||||
[
|
||||
!left && !top && radius,
|
||||
!top && !right && radius,
|
||||
!right && !bottom && radius,
|
||||
!bottom && !left && radius,
|
||||
]
|
||||
);
|
||||
ctx.fill();
|
||||
} else {
|
||||
// Draw rounded concave corners
|
||||
const topLeft = y > 0 && x > 0 && (qr.matrix[(y - 1) * qr.matrixWidth + x - 1] & 1);
|
||||
const topRight = y > 0 && x < xMax && (qr.matrix[(y - 1) * qr.matrixWidth + x + 1] & 1);
|
||||
const bottomRight = y < yMax && x < xMax && (qr.matrix[(y + 1) * qr.matrixWidth + x + 1] & 1);
|
||||
const bottomLeft = y < yMax && x > 0 && (qr.matrix[(y + 1) * qr.matrixWidth + x - 1] & 1);
|
||||
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = bg;
|
||||
ctx.roundRect(
|
||||
x * pixelSize,
|
||||
y * pixelSize,
|
||||
pixelSize,
|
||||
pixelSize,
|
||||
[
|
||||
left && top && topLeft && radius,
|
||||
top && right && topRight && radius,
|
||||
right && bottom && bottomRight && radius,
|
||||
bottom && left && bottomLeft && radius,
|
||||
]
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
Minimal: `// qr, ctx are args
|
||||
const 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,
|
||||
}
|
||||
|
||||
const pixelSize = 12;
|
||||
ctx.canvas.width = qr.matrixWidth * pixelSize;
|
||||
ctx.canvas.height = qr.matrixHeight * pixelSize;
|
||||
|
||||
const finderPos = [
|
||||
[qr.margin.left, qr.margin.top],
|
||||
[qr.matrixWidth - qr.margin.right - 7, qr.margin.top],
|
||||
[qr.margin.left, qr.matrixHeight - qr.margin.bottom - 7],
|
||||
];
|
||||
|
||||
ctx.fillStyle = "rgb(0, 0, 0)";
|
||||
|
||||
for (const [x, y] of finderPos) {
|
||||
ctx.fillRect((x + 3) * pixelSize, y * pixelSize, pixelSize, pixelSize);
|
||||
ctx.fillRect((x + 3) * pixelSize, (y + 6) * pixelSize, pixelSize, pixelSize);
|
||||
ctx.fillRect(x * pixelSize, (y + 3) * pixelSize, pixelSize, pixelSize);
|
||||
ctx.fillRect((x + 6) * pixelSize, (y + 3) * pixelSize, pixelSize, pixelSize);
|
||||
|
||||
ctx.fillRect((x + 2) * pixelSize, (y + 2) * pixelSize, 3 * pixelSize, 3 * pixelSize);
|
||||
}
|
||||
|
||||
const minSize = pixelSize / 2;
|
||||
const offset = (pixelSize - minSize) / 2;
|
||||
|
||||
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) === Module.FinderON) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (module & 1) {
|
||||
ctx.fillRect(x * pixelSize + offset, y * pixelSize + offset, minSize, minSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
"Lover (Animated)": `// qr, ctx are args
|
||||
const pixelSize = 10;
|
||||
ctx.canvas.width = qr.matrixWidth * pixelSize;
|
||||
ctx.canvas.height = qr.matrixHeight * pixelSize;
|
||||
|
||||
const period = 3000; // ms
|
||||
const amplitude = 0.8; // maxSize - minSize
|
||||
const minSize = 0.6;
|
||||
|
||||
let counter = 0;
|
||||
let prevTimestamp;
|
||||
|
||||
let req;
|
||||
function frame(timestamp) {
|
||||
// performance.now() and requestAnimationFrame's timestamp are not consistent together
|
||||
if (prevTimestamp != null) {
|
||||
counter += timestamp - prevTimestamp;
|
||||
}
|
||||
|
||||
prevTimestamp = timestamp;
|
||||
|
||||
if (counter >= period) {
|
||||
counter -= period;
|
||||
}
|
||||
|
||||
ctx.fillStyle = "rgb(0, 0, 0)";
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
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) === 0) continue;
|
||||
|
||||
const xBias = Math.abs(5 - (x % 10));
|
||||
const biasCounter = counter + (x + y) * (period / 20) + xBias * (period / 10);
|
||||
|
||||
const ratio = Math.abs((period / 2) - (biasCounter % period)) / (period / 2);
|
||||
|
||||
const size = (ratio * amplitude + minSize) * pixelSize;
|
||||
|
||||
const offset = (pixelSize - size) / 2;
|
||||
|
||||
ctx.fillStyle = \`rgb(\${100 + ratio * 150}, \${200 + xBias * 10}, 255)\`;
|
||||
ctx.fillRect(x * pixelSize + offset, y * pixelSize + offset, size, size);
|
||||
}
|
||||
}
|
||||
req = requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
req = requestAnimationFrame(frame);
|
||||
|
||||
return () => cancelAnimationFrame(req);`,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { For, 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 { ButtonGroup, ButtonGroupItem } from "../ButtonGroup";
|
||||
import { NumberInput } from "../NumberInput";
|
||||
import { Select } from "../Select";
|
||||
|
||||
export function Settings() {
|
||||
const { inputQr, setInputQr } = useQrContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-sm py-2">Encoding mode</div>
|
||||
<Select
|
||||
options={MODE_NAMES}
|
||||
value={MODE_KEY[inputQr.mode!]}
|
||||
setValue={(name) => setInputQr("mode", MODE_VALUE[name])}
|
||||
/>
|
||||
</div>
|
||||
<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", { top: v, bottom: v, left: v, right: v })
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Row(props: {
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div class="text-sm py-2">
|
||||
{props.title}
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -33,7 +33,7 @@ type Props = {
|
|||
};
|
||||
|
||||
export default function QrPreview(props: Props) {
|
||||
const { inputQr, outputQr, renderFuncKey } = useQrContext();
|
||||
const { inputQr, outputQr } = useQrContext();
|
||||
|
||||
return (
|
||||
<div class={props.class}>
|
||||
|
@ -69,7 +69,7 @@ export default function QrPreview(props: Props) {
|
|||
* Running the effect in the ref function caused double rendering for future mounts.
|
||||
*/
|
||||
function RenderedQrCode() {
|
||||
const { outputQr: _outputQr, renderFunc, renderFuncKey } = useQrContext();
|
||||
const { outputQr: _outputQr, renderFunc, renderFuncKey, params } = useQrContext();
|
||||
const outputQr = _outputQr as () => OutputQr;
|
||||
|
||||
const fullWidth = () => {
|
||||
|
@ -110,11 +110,11 @@ function RenderedQrCode() {
|
|||
prevFuncKey = untrack(renderFuncKey);
|
||||
try {
|
||||
// matrix isn't cloned without this line... this disables some optimization i think
|
||||
const output = {...outputQr()};
|
||||
const output = { ...outputQr() };
|
||||
// Allow renderFunc to modify matrix with reset between renders
|
||||
output.matrix = [...output.matrix]
|
||||
output.matrix = [...output.matrix];
|
||||
|
||||
cleanupFunc = renderFunc()(output, ctx);
|
||||
cleanupFunc = renderFunc()(output, params, ctx);
|
||||
setRuntimeError(null);
|
||||
} catch (e) {
|
||||
setRuntimeError(e!.toString());
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
get_matrix,
|
||||
} from "fuqr";
|
||||
import { createStore, type SetStoreFunction } from "solid-js/store";
|
||||
import type { Params, ParamsSchema } from "./params";
|
||||
|
||||
type InputQr = {
|
||||
text: string;
|
||||
|
@ -63,10 +64,15 @@ export const QrContext = createContext<{
|
|||
setRenderFunc: Setter<RenderFunc>;
|
||||
renderFuncKey: Accessor<string>;
|
||||
setRenderFuncKey: Setter<string>;
|
||||
params: Params;
|
||||
setParams: SetStoreFunction<Params>;
|
||||
paramsSchema: Accessor<ParamsSchema>;
|
||||
setParamsSchema: Setter<ParamsSchema>;
|
||||
}>();
|
||||
|
||||
export type RenderFunc = (
|
||||
qr: OutputQr,
|
||||
params: {},
|
||||
ctx: CanvasRenderingContext2D
|
||||
) => void | (() => void);
|
||||
|
||||
|
@ -89,9 +95,12 @@ export function QrContextProvider(props: { children: JSX.Element }) {
|
|||
QrError.InvalidEncoding
|
||||
);
|
||||
|
||||
const [renderFunc, setRenderFunc] = createSignal<RenderFunc>(defaultRender);
|
||||
const [renderFunc, setRenderFunc] = createSignal<RenderFunc>(()=>{});
|
||||
const [renderFuncKey, setRenderFuncKey] = createSignal("Square");
|
||||
|
||||
const [paramsSchema, setParamsSchema] = createSignal<ParamsSchema>({});
|
||||
const [params, setParams] = createStore({});
|
||||
|
||||
createEffect(() => {
|
||||
try {
|
||||
// NOTE: Version and Margin cannot be reused, so must be created each time
|
||||
|
@ -141,7 +150,11 @@ export function QrContextProvider(props: { children: JSX.Element }) {
|
|||
renderFunc,
|
||||
setRenderFunc,
|
||||
renderFuncKey,
|
||||
setRenderFuncKey
|
||||
setRenderFuncKey,
|
||||
params,
|
||||
setParams,
|
||||
paramsSchema,
|
||||
setParamsSchema,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
@ -155,25 +168,4 @@ export function useQrContext() {
|
|||
throw new Error("useQrContext: used outside QrContextProvider");
|
||||
}
|
||||
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)";
|
||||
// ctx.imageSmoothingEnabled
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { ColorInput } from "~/components/ColorInput";
|
||||
import { ImageInput } from "~/components/ImageInput";
|
||||
import { NumberInput } from "~/components/NumberInput";
|
||||
import { Select } from "~/components/Select";
|
||||
import { Switch } from "~/components/Switch";
|
||||
|
||||
|
||||
export const PARAM_TYPES = ["boolean", "number", "Color", "Select", "File"]
|
||||
|
||||
export const PARAM_COMPONENTS = {
|
||||
boolean: Switch,
|
||||
number: NumberInput,
|
||||
Color: ColorInput,
|
||||
Select: Select,
|
||||
File: ImageInput,
|
||||
};
|
||||
|
||||
export const PARAM_DEFAULTS = {
|
||||
boolean: false,
|
||||
number: 0,
|
||||
Color: "rgb(0,0,0)",
|
||||
// Select: default is first option
|
||||
File: null,
|
||||
};
|
||||
|
||||
export type Params = {
|
||||
[label: string]: Exclude<ParamsSchema[string]["default"], undefined>;
|
||||
};
|
||||
|
||||
export type ParamsSchema = SchemaFromMapping<typeof PARAM_COMPONENTS>;
|
||||
|
||||
/**
|
||||
* Given object mapping keys to components, returns union of [key, props]
|
||||
*/
|
||||
type Step1<T extends { [key: keyof any]: (...args: any) => any }> = {
|
||||
[K in keyof T]: [K, Parameters<T[K]>[0]];
|
||||
}[keyof T];
|
||||
|
||||
/**
|
||||
* Given union of [key, props]:
|
||||
*
|
||||
* adds default to props based on type of value and removes value and setValue from props
|
||||
*/
|
||||
type Step2<T extends [keyof any, { [prop: string]: any }]> = T extends any
|
||||
? [T[0], Omit<T[1], "value" | "setValue"> & { default?: T[1]["value"] }]
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Converts each tuple to an object with a type corresponding to key
|
||||
* (discriminated union of component props) assignable to any label
|
||||
*/
|
||||
type Step3<T extends [keyof any, { [prop: string]: any }]> = {
|
||||
[label: string]: T extends any ? { type: T[0] } & T[1] : never;
|
||||
};
|
||||
|
||||
type SchemaFromMapping<T extends { [key: string]: (...args: any) => any }> =
|
||||
Step3<Step2<Step1<T>>>;
|
|
@ -0,0 +1,374 @@
|
|||
|
||||
export const PRESET_FUNCS = {
|
||||
Square: `export const paramsSchema = {
|
||||
"Pixel size": {
|
||||
type: "number",
|
||||
min: 1,
|
||||
max: 20,
|
||||
default: 10
|
||||
},
|
||||
"Foreground": {
|
||||
type: "Color",
|
||||
default: "#000000",
|
||||
},
|
||||
"Background": {
|
||||
type: "Color",
|
||||
default: "#ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
export function renderCanvas(qr, params, ctx) {
|
||||
const pixelSize = params["Pixel size"];
|
||||
ctx.canvas.width = qr.matrixWidth * pixelSize;
|
||||
ctx.canvas.height = qr.matrixHeight * pixelSize;
|
||||
|
||||
ctx.fillStyle = params["Background"];
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
ctx.fillStyle = params["Foreground"];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
Circle: `// qr, ctx are args
|
||||
const 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 function renderCanvas(qr, params, ctx) {
|
||||
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);
|
||||
|
||||
const gradient = ctx.createRadialGradient(
|
||||
ctx.canvas.width / 2,
|
||||
ctx.canvas.height / 2,
|
||||
2 * pixelSize,
|
||||
ctx.canvas.width / 2,
|
||||
ctx.canvas.height / 2,
|
||||
20 * pixelSize,
|
||||
);
|
||||
|
||||
gradient.addColorStop(0, "red");
|
||||
gradient.addColorStop(1, "blue");
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
|
||||
const radius = pixelSize / 2;
|
||||
|
||||
const finderPos = [
|
||||
[qr.margin.left, qr.margin.top],
|
||||
[qr.matrixWidth - qr.margin.right - 7, qr.margin.top],
|
||||
[qr.margin.left, qr.matrixHeight - qr.margin.bottom - 7],
|
||||
];
|
||||
|
||||
for (const [x, y] of finderPos) {
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 3.5) * pixelSize, (y + 3.5) * pixelSize, 3.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 3.5) * pixelSize, (y + 3.5) * pixelSize, 2.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 3.5) * pixelSize, (y + 3.5) * pixelSize, 1.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
const xMid = qr.matrixWidth / 2;
|
||||
const yMid = qr.matrixHeight / 2;
|
||||
const maxDist = Math.sqrt(xMid * xMid + yMid + yMid);
|
||||
|
||||
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) {
|
||||
if (module === Module.FinderON) continue;
|
||||
if (module === Module.AlignmentON) {
|
||||
// Find top left corner of alignment square
|
||||
if (qr.matrix[(y - 1) * qr.matrixWidth + x] !== Module.AlignmentON &&
|
||||
qr.matrix[y * qr.matrixWidth + x - 1] !== Module.AlignmentON &&
|
||||
qr.matrix[y * qr.matrixWidth + x + 1] === Module.AlignmentON
|
||||
) {
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 2.5) * pixelSize, (y + 2.5) * pixelSize, 2.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 2.5) * pixelSize, (y + 2.5) * pixelSize, 1.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc((x + 2.5) * pixelSize, (y + 2.5) * pixelSize, 0.5 * pixelSize, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
continue;
|
||||
};
|
||||
|
||||
const xCenter = x * pixelSize + radius;
|
||||
const yCenter = y * pixelSize + radius;
|
||||
|
||||
const xDist = Math.abs(xMid - x);
|
||||
const yDist = Math.abs(yMid - y);
|
||||
const scale = Math.sqrt(xDist * xDist + yDist * yDist) / maxDist * 0.7 + 0.5;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(xCenter, yCenter, radius * scale, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
Camouflage: `// qr, ctx are args
|
||||
const 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,
|
||||
}
|
||||
|
||||
function splitmix32(a) {
|
||||
return function() {
|
||||
a |= 0;
|
||||
a = a + 0x9e3779b9 | 0;
|
||||
let t = a ^ a >>> 16;
|
||||
t = Math.imul(t, 0x21f0aaad);
|
||||
t = t ^ t >>> 15;
|
||||
t = Math.imul(t, 0x735a2d97);
|
||||
return ((t = t ^ t >>> 15) >>> 0) / 4294967296;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderCanvas(qr, params, ctx) {
|
||||
const seededRand = splitmix32(1 /* change seed to change pattern */);
|
||||
|
||||
// Randomly set pixels in margin
|
||||
for (let y = 0; y < qr.matrixHeight; y++) {
|
||||
for (let x = 0; x < qr.matrixWidth; x++) {
|
||||
if (y > qr.margin.top - 2 &&
|
||||
y < qr.matrixHeight - qr.margin.bottom + 1 &&
|
||||
x > qr.margin.left - 2 &&
|
||||
x < qr.matrixWidth - qr.margin.right + 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seededRand() > 0.5) qr.matrix[y * qr.matrixWidth + x] = Module.DataON;
|
||||
}
|
||||
}
|
||||
|
||||
const pixelSize = 20;
|
||||
const radius = pixelSize / 2;
|
||||
ctx.canvas.width = qr.matrixWidth * pixelSize;
|
||||
ctx.canvas.height = qr.matrixHeight * pixelSize;
|
||||
|
||||
const fg = "rgb(40, 70, 10)";
|
||||
const bg = "rgb(200, 200, 100)";
|
||||
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
const xMax = qr.matrixWidth - 1;
|
||||
const yMax = qr.matrixHeight - 1;
|
||||
|
||||
for (let y = 0; y < qr.matrixHeight; y++) {
|
||||
for (let x = 0; x < qr.matrixWidth; x++) {
|
||||
const module = qr.matrix[y * qr.matrixWidth + x];
|
||||
|
||||
const top = y > 0 && (qr.matrix[(y - 1) * qr.matrixWidth + x] & 1);
|
||||
const bottom = y < yMax && (qr.matrix[(y + 1) * qr.matrixWidth + x] & 1);
|
||||
const left = x > 0 && (qr.matrix[y * qr.matrixWidth + x - 1] & 1);
|
||||
const right = x < xMax && (qr.matrix[y * qr.matrixWidth + x + 1] & 1);
|
||||
|
||||
ctx.fillStyle = fg;
|
||||
|
||||
if (module & 1) {
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(
|
||||
x * pixelSize,
|
||||
y * pixelSize,
|
||||
pixelSize,
|
||||
pixelSize,
|
||||
[
|
||||
!left && !top && radius,
|
||||
!top && !right && radius,
|
||||
!right && !bottom && radius,
|
||||
!bottom && !left && radius,
|
||||
]
|
||||
);
|
||||
ctx.fill();
|
||||
} else {
|
||||
// Draw rounded concave corners
|
||||
const topLeft = y > 0 && x > 0 && (qr.matrix[(y - 1) * qr.matrixWidth + x - 1] & 1);
|
||||
const topRight = y > 0 && x < xMax && (qr.matrix[(y - 1) * qr.matrixWidth + x + 1] & 1);
|
||||
const bottomRight = y < yMax && x < xMax && (qr.matrix[(y + 1) * qr.matrixWidth + x + 1] & 1);
|
||||
const bottomLeft = y < yMax && x > 0 && (qr.matrix[(y + 1) * qr.matrixWidth + x - 1] & 1);
|
||||
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = bg;
|
||||
ctx.roundRect(
|
||||
x * pixelSize,
|
||||
y * pixelSize,
|
||||
pixelSize,
|
||||
pixelSize,
|
||||
[
|
||||
left && top && topLeft && radius,
|
||||
top && right && topRight && radius,
|
||||
right && bottom && bottomRight && radius,
|
||||
bottom && left && bottomLeft && radius,
|
||||
]
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
Minimal: `export function renderCanvas(qr, params, ctx) {
|
||||
const 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,
|
||||
}
|
||||
|
||||
const pixelSize = 12;
|
||||
ctx.canvas.width = qr.matrixWidth * pixelSize;
|
||||
ctx.canvas.height = qr.matrixHeight * pixelSize;
|
||||
|
||||
const finderPos = [
|
||||
[qr.margin.left, qr.margin.top],
|
||||
[qr.matrixWidth - qr.margin.right - 7, qr.margin.top],
|
||||
[qr.margin.left, qr.matrixHeight - qr.margin.bottom - 7],
|
||||
];
|
||||
|
||||
ctx.fillStyle = "rgb(0, 0, 0)";
|
||||
|
||||
for (const [x, y] of finderPos) {
|
||||
ctx.fillRect((x + 3) * pixelSize, y * pixelSize, pixelSize, pixelSize);
|
||||
ctx.fillRect((x + 3) * pixelSize, (y + 6) * pixelSize, pixelSize, pixelSize);
|
||||
ctx.fillRect(x * pixelSize, (y + 3) * pixelSize, pixelSize, pixelSize);
|
||||
ctx.fillRect((x + 6) * pixelSize, (y + 3) * pixelSize, pixelSize, pixelSize);
|
||||
|
||||
ctx.fillRect((x + 2) * pixelSize, (y + 2) * pixelSize, 3 * pixelSize, 3 * pixelSize);
|
||||
}
|
||||
|
||||
const minSize = pixelSize / 2;
|
||||
const offset = (pixelSize - minSize) / 2;
|
||||
|
||||
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) === Module.FinderON) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (module & 1) {
|
||||
ctx.fillRect(x * pixelSize + offset, y * pixelSize + offset, minSize, minSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
"Lover (Animated)": `export function renderCanvas(qr, params, ctx) {
|
||||
const pixelSize = 10;
|
||||
ctx.canvas.width = qr.matrixWidth * pixelSize;
|
||||
ctx.canvas.height = qr.matrixHeight * pixelSize;
|
||||
|
||||
const period = 3000; // ms
|
||||
const amplitude = 0.8; // maxSize - minSize
|
||||
const minSize = 0.6;
|
||||
|
||||
let counter = 0;
|
||||
let prevTimestamp;
|
||||
|
||||
let req;
|
||||
function frame(timestamp) {
|
||||
// performance.now() and requestAnimationFrame's timestamp are not consistent together
|
||||
if (prevTimestamp != null) {
|
||||
counter += timestamp - prevTimestamp;
|
||||
}
|
||||
|
||||
prevTimestamp = timestamp;
|
||||
|
||||
if (counter >= period) {
|
||||
counter -= period;
|
||||
}
|
||||
|
||||
ctx.fillStyle = "rgb(0, 0, 0)";
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
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) === 0) continue;
|
||||
|
||||
const xBias = Math.abs(5 - (x % 10));
|
||||
const biasCounter = counter + (x + y) * (period / 20) + xBias * (period / 10);
|
||||
|
||||
const ratio = Math.abs((period / 2) - (biasCounter % period)) / (period / 2);
|
||||
|
||||
const size = (ratio * amplitude + minSize) * pixelSize;
|
||||
|
||||
const offset = (pixelSize - size) / 2;
|
||||
|
||||
ctx.fillStyle = \`rgb(\${100 + ratio * 150}, \${200 + xBias * 10}, 255)\`;
|
||||
ctx.fillRect(x * pixelSize + offset, y * pixelSize + offset, size, size);
|
||||
}
|
||||
}
|
||||
req = requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
req = requestAnimationFrame(frame);
|
||||
|
||||
return () => cancelAnimationFrame(req);
|
||||
}
|
||||
`,
|
||||
};
|
|
@ -1,7 +1,5 @@
|
|||
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";
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue