main
Kyle Zheng 2024-08-15 23:45:55 -04:00
rodzic 1d2ba4b5b2
commit 81d704e034
10 zmienionych plików z 792 dodań i 54 usunięć

Wyświetl plik

@ -4,9 +4,59 @@ framework for making qr codes
Blatantly inspired by [QRBTF](https://qrbtf.com) and [Anthony Fu's QR Toolkit](https://qrcode.antfu.me).
## Examples
I'm working on more examples.
<table>
<tbody>
<tr>
<th colspan="3">Extending with noise</th>
</tr>
<tr>
<td>
<img src="./examples/circle.svg" width="300"/>
</td>
<td>
<img src="./examples/camo.svg" width="300"/>
</td>
<td>
<img src="./examples/neon.svg" width="300"/>
</td>
</tr>
<tr>
<th colspan="3">Using external libs like <a href="https://roughjs.com/">Rough.js</a></th>
</tr>
<td>
<img src="./examples/drawing1.png"/>
</td>
<td>
<img src="./examples/drawing2.png"/>
</td>
<td>
<img src="./examples/drawing3.png"/>
</td>
<tr>
<th colspan="3">Styles from <a href="https://qrbtf.com">QRBTF</a></th>
</tr>
<tr>
<td>
<img src="./examples/blocks.svg" width="300"/>
</td>
<td>
<img src="./examples/bubbles.svg" width="300"/>
</td>
<td>
<img src="./examples/alien.svg" width="300"/>
</td>
</tr>
</tbody>
</table>
## Features
- Customize data:
- encoding mode, version, error tolerance, mask pattern
- powered by [`fuqr`](https://github.com/zhengkyl/fuqr), my own Rust library imported as WASM. (i use windows, btw)
@ -14,7 +64,7 @@ Blatantly inspired by [QRBTF](https://qrbtf.com) and [Anthony Fu's QR Toolkit](h
- Choose any preset, customize or even create a new one from scratch via code editor.
- Define arbitrary ui parameters in code
- Supports SVG and PNG (canvas)
- All code runs *directly* in browser. There are no safeguards except that which browser vendors have bestowed upon us.
- All code runs _directly_ in browser. There are no safeguards except that which browser vendors have bestowed upon us.
## Use existing presets
@ -23,49 +73,3 @@ Blatantly inspired by [QRBTF](https://qrbtf.com) and [Anthony Fu's QR Toolkit](h
## Customizable parameters defined in code
![code and parameter editor ui](./examples/ui2.png)
## Examples
I'm working on more examples.
<table>
<thead>
<tr>
<th colspan="3">Extending with noise</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img src="./examples/circle.svg" width="300"/>
</td>
<td>
<img src="./examples/camo.svg" width="300"/>
</td>
<td>
<img src="./examples/neon.svg" width="300"/>
</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th colspan="3">Styles from <a href="https://qrbtf.com">QRBTF</a></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img src="./examples/blocks.svg" width="300"/>
</td>
<td>
<img src="./examples/bubbles.svg" width="300"/>
</td>
<td>
<img src="./examples/alien.svg" width="300"/>
</td>
</tr>
</tbody>
</table>

Plik binarny nie jest wyświetlany.

Po

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

Plik binarny nie jest wyświetlany.

Po

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

Plik binarny nie jest wyświetlany.

Po

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

369
presets/Drawing.ts 100644
Wyświetl plik

@ -0,0 +1,369 @@
import type { Params, RawParamsSchema } from "~/lib/params";
import type { OutputQr } from "~/lib/QrContext";
// @ts-expect-error not bundled
import rough from "https://esm.sh/roughjs";
export const paramsSchema = {
Margin: {
type: "number",
min: 0,
max: 10,
default: 2,
},
"Fill style": {
type: "Select",
options: [
"Hachure",
"Solid",
"Zigzag",
"Cross-hatch",
"Dots",
"Dashed",
"Zigzag-line",
],
default: "Zigzag",
},
Fill: {
type: "Color",
default: "#ffffff",
},
"Fill weight": {
type: "number",
min: 0,
max: 10,
default: 2,
},
"Fill gap": {
type: "number",
min: 1,
max: 10,
default: 4,
},
Stroke: {
type: "Color",
default: "#ffffff",
},
"Stroke width": {
type: "number",
min: 0,
max: 10,
default: 1,
},
Invert: {
type: "boolean",
default: true,
},
Roughness: {
type: "number",
min: 0,
max: 10,
default: 1,
},
Bowing: {
type: "number",
min: 0,
max: 10,
default: 1,
},
Background: {
type: "Color",
default: "#000000",
},
Seed: {
type: "number",
min: 1,
max: 100,
default: 1,
},
} satisfies RawParamsSchema;
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,
SeparatorOFF: 12,
};
function splitmix32(a: number) {
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 domMock = {
ownerDocument: {
createElementNS: (_ns: string, tagName: string) => {
const children: any[] = [];
const attributes: any = {};
return {
tagName,
attributes,
setAttribute: (key: string, value: string) => (attributes[key] = value),
appendChild: (node: any) => children.push(node),
children,
};
},
},
};
export function renderSVG(qr: OutputQr, params: Params<typeof paramsSchema>) {
const roughSVG = rough.svg(domMock, {
options: {
roughness: params["Roughness"],
bowing: params["Bowing"],
fillStyle: params["Fill style"].toLowerCase(),
fillWeight: params["Fill weight"],
fill: params["Fill weight"] === 0 ? "none" : params["Fill"],
strokeWidth: params["Stroke width"],
stroke: params["Stroke width"] === 0 ? "none" : params["Stroke"],
hachureGap: params["Fill gap"],
seed: params["Seed"],
fixedDecimalPlaceDigits: 2,
},
});
let matrix = qr.matrix as any;
let matrixWidth = qr.version * 4 + 17;
if (params["Invert"]) {
matrixWidth += 2;
matrix = [];
for (let y = 0; y < matrixWidth; y++) {
for (let x = 0; x < matrixWidth; x++) {
if (
x === 0 ||
y === 0 ||
x === matrixWidth - 1 ||
y === matrixWidth - 1
) {
matrix.push(Module.DataOFF);
} else {
matrix.push(qr.matrix[(y - 1) * (matrixWidth - 2) + x - 1]);
}
}
}
}
const visited = new Uint16Array(matrixWidth * matrixWidth);
const unit = 10;
const margin = params["Margin"] * unit;
const size = matrixWidth * unit + 2 * margin;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${-margin} ${-margin} ${size} ${size}">`;
svg += `<rect x="${-margin}" y="${-margin}" width="${size}" height="${size}" fill="${params["Background"]}"/>`;
const xMax = matrixWidth - 1;
const yMax = matrixWidth - 1;
let baseX: number;
let baseY: number;
const on = params["Invert"]
? (x: number, y: number) => (matrix[y * matrixWidth + x] & 1) === 0
: (x: number, y: number) => (matrix[y * matrixWidth + x] & 1) === 1;
function goRight(x: number, y: number, shape: number, cw: boolean) {
let sx = x;
let vert = false;
visited[y * matrixWidth + x] = shape;
while (x < xMax) {
const right = on(x + 1, y);
const vertRight = y > 0 && on(x + 1, y - 1);
if (!right || vertRight) {
vert = right && vertRight;
break;
}
x++;
visited[y * matrixWidth + x] = shape;
}
paths[shape] += `h${(x - sx + 1) * unit}`;
if (vert) {
goUp(x + 1, y - 1, shape, cw);
} else {
goDown(x, y, shape, cw);
}
}
function goLeft(x: number, y: number, shape: number, cw: boolean) {
let sx = x;
let vert = false;
visited[y * matrixWidth + x] = shape;
while (x > 0) {
const left = on(x - 1, y);
const vertLeft = y < yMax && on(x - 1, y + 1);
if (!left || vertLeft) {
vert = left && vertLeft;
break;
}
x--;
visited[y * matrixWidth + x] = shape;
}
if (!cw && x === baseX && y === baseY) {
paths[shape] += "z";
return;
}
paths[shape] += `h${(x - sx - 1) * unit}`;
if (vert) {
goDown(x - 1, y + 1, shape, cw);
} else {
goUp(x, y, shape, cw);
}
}
function goUp(x: number, y: number, shape: number, cw: boolean) {
let sy = y;
let horz = false;
visited[y * matrixWidth + x] = shape;
while (y > 0) {
const up = on(x, y - 1);
const horzUp = x > 0 && on(x - 1, y - 1);
if (!up || horzUp) {
horz = up && horzUp;
break;
}
y--;
visited[y * matrixWidth + x] = shape;
}
if (cw && x === baseX && y === baseY) {
paths[shape] += "z";
return;
}
paths[shape] += `v${(y - sy - 1) * unit}`;
if (horz) {
goLeft(x - 1, y - 1, shape, cw);
} else {
goRight(x, y, shape, cw);
}
}
function goDown(x: number, y: number, shape: number, cw: boolean) {
let sy = y;
let horz = false;
visited[y * matrixWidth + x] = shape;
while (y < yMax) {
const down = on(x, y + 1);
const horzDown = x < xMax && on(x + 1, y + 1);
if (!down || horzDown) {
horz = down && horzDown;
break;
}
y++;
visited[y * matrixWidth + x] = shape;
}
paths[shape] += `v${(y - sy + 1) * unit}`;
if (horz) {
goRight(x + 1, y + 1, shape, cw);
} else {
goLeft(x, y, shape, cw);
}
}
const stack: [number, number][] = [];
for (let x = 0; x < matrixWidth; x++) {
if (!on(x, 0)) stack.push([x, 0]);
}
for (let y = 1; y < yMax; y++) {
if (!on(0, y)) stack.push([0, y]);
if (!on(xMax, y)) stack.push([xMax, y]);
}
for (let x = 0; x < matrixWidth; x++) {
if (!on(x, yMax)) stack.push([x, yMax]);
}
// recursion dfs limited to ~4000
// visit all whitespace connected to edges
function dfsOff() {
while (stack.length > 0) {
const [x, y] = stack.pop()!;
if (visited[y * matrixWidth + x]) continue;
visited[y * matrixWidth + x] = 1;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dy === 0 && dx === 0) continue;
let nx = x + dx;
let ny = y + dy;
if (nx < 0 || nx > xMax || ny < 0 || ny > yMax) continue;
if (on(nx, ny)) continue;
stack.push([nx, ny]);
}
}
}
}
dfsOff();
const paths = [""];
for (let y = 0; y < matrixWidth; y++) {
for (let x = 0; x < matrixWidth; x++) {
if (visited[y * matrixWidth + x]) continue;
if (!on(x, y)) {
const shape = visited[y * matrixWidth + x - 1];
paths[shape] += `M${x * unit},${y * unit}`;
baseY = y - 1;
baseX = x;
goDown(x - 1, y, shape, false);
stack.push([x, y]);
dfsOff();
continue;
}
if (y > 0 && on(x, y - 1) && visited[(y - 1) * matrixWidth + x]) {
visited[y * matrixWidth + x] = visited[(y - 1) * matrixWidth + x];
continue;
}
if (x > 0 && on(x - 1, y) && visited[y * matrixWidth + x - 1]) {
visited[y * matrixWidth + x] = visited[y * matrixWidth + x - 1];
continue;
}
paths.push(`M${x * unit},${y * unit}`);
baseY = y;
baseX = x;
goRight(x, y, paths.length - 1, true);
}
}
function domToString(node: any) {
const attrs = Object.entries(node.attributes)
.map(([key, value]) => `${key}="${value}"`)
.join(" ");
svg += `<${node.tagName} ${attrs}>`;
node.children.forEach(domToString);
svg += `</${node.tagName}>`;
}
paths.forEach((path, i) => {
if (i === 0) return;
const g = roughSVG.path(path);
console.log(g);
domToString(g);
});
svg += `</svg>`;
return svg;
}

Wyświetl plik

@ -1,6 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M2 1v6c1 1 2 0 2-1l1-2c1 0 3 0 2-2H4L2 1ZM17 1l-3 1 2 2c2 0 0 1 1 2-1 1 1 2 2 1V3l-1-2h-1zM18 12c-2 0-2 2-2 3v2c-1-1-3 0-3 1l3 1h2l1-3v-3l-1-1zM3 12l-1 2v4h6c0-2-1-2-2-2l-2-1-1-3Z" style="fill:#000"/>
<path d="m15 5-4-1H6L4 7v4l1 3 3 2h7c2-1 2-3 2-5l-1-3-1-3z" style="fill:#fff"/>
<path d="M10 5 8 6 6 7v7h2l4 1 2-1 1-2V7l-2-2h-3ZM8 8h4l1 4H8V8zm4 4z" style="fill:#000"/>
<path d="M12 9H9l1 2h2V9z" style="fill:#000"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7 7"><path d="M0,0h7v7h-7zM2,1v1h-1v3h1v1h4v-4h-1v-1zM2,2h3v2h-1v1h-2z" style="fill:#000"/></svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 511 B

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 150 B

Wyświetl plik

@ -70,6 +70,7 @@ export function Editor(props: Props) {
Circle: "",
Camo: "",
Neon: "",
Drawing: "",
Blocks: "",
Bubbles: "",
Alien: "",

Wyświetl plik

@ -194,7 +194,7 @@ function RenderedQrCode() {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
// TODO allow adjust resolution/aspect ratio
const size = (outputQr().version * 4 + 17) * 10;
const size = 300 //(outputQr().version * 4 + 17) * 10;
canvas.width = size;
canvas.height = size;

Wyświetl plik

@ -3,6 +3,7 @@ import { Blocks } from "./presets/Blocks";
import { Bubbles } from "./presets/Bubbles";
import { Camo } from "./presets/Camo";
import { Circle } from "./presets/Circle";
import { Drawing } from "./presets/Drawing";
import { Halftone } from "./presets/Halftone";
import { Minimal } from "./presets/Minimal";
import { Neon } from "./presets/Neon";
@ -13,6 +14,7 @@ export const PRESET_CODE = {
Circle,
Camo,
Neon,
Drawing,
Halftone,
Minimal,
Blocks,

Wyświetl plik

@ -0,0 +1,367 @@
export const Drawing = `import rough from "https://esm.sh/roughjs";
export const paramsSchema = {
Margin: {
type: "number",
min: 0,
max: 10,
default: 2,
},
"Fill style": {
type: "Select",
options: [
"Hachure",
"Solid",
"Zigzag",
"Cross-hatch",
"Dots",
"Dashed",
"Zigzag-line",
],
default: "Zigzag",
},
Fill: {
type: "Color",
default: "#ffffff",
},
"Fill weight": {
type: "number",
min: 0,
max: 10,
default: 2,
},
"Fill gap": {
type: "number",
min: 1,
max: 10,
default: 4,
},
Stroke: {
type: "Color",
default: "#ffffff",
},
"Stroke width": {
type: "number",
min: 0,
max: 10,
default: 1,
},
Invert: {
type: "boolean",
default: true,
},
Roughness: {
type: "number",
min: 0,
max: 10,
default: 1,
},
Bowing: {
type: "number",
min: 0,
max: 10,
default: 1,
},
Background: {
type: "Color",
default: "#000000",
},
Seed: {
type: "number",
min: 1,
max: 100,
default: 1,
},
};
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,
SeparatorOFF: 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 domMock = {
ownerDocument: {
createElementNS: (_ns, tagName) => {
const children = [];
const attributes = {};
return {
tagName,
attributes,
setAttribute: (key, value) => (attributes[key] = value),
appendChild: (node) => children.push(node),
children,
};
},
},
};
export function renderSVG(qr, params) {
const roughSVG = rough.svg(domMock, {
options: {
roughness: params["Roughness"],
bowing: params["Bowing"],
fillStyle: params["Fill style"].toLowerCase(),
fillWeight: params["Fill weight"],
fill: params["Fill weight"] === 0 ? "none" : params["Fill"],
strokeWidth: params["Stroke width"],
stroke: params["Stroke width"] === 0 ? "none" : params["Stroke"],
hachureGap: params["Fill gap"],
seed: params["Seed"],
fixedDecimalPlaceDigits: 2,
},
});
let matrix = qr.matrix;
let matrixWidth = qr.version * 4 + 17;
if (params["Invert"]) {
matrixWidth += 2;
matrix = [];
for (let y = 0; y < matrixWidth; y++) {
for (let x = 0; x < matrixWidth; x++) {
if (
x === 0 ||
y === 0 ||
x === matrixWidth - 1 ||
y === matrixWidth - 1
) {
matrix.push(Module.DataOFF);
} else {
matrix.push(qr.matrix[(y - 1) * (matrixWidth - 2) + x - 1]);
}
}
}
}
const visited = new Uint16Array(matrixWidth * matrixWidth);
const unit = 10;
const margin = params["Margin"] * unit;
const size = matrixWidth * unit + 2 * margin;
let svg = \`<svg xmlns="http://www.w3.org/2000/svg" viewBox="\${-margin} \${-margin} \${size} \${size}">\`;
svg += \`<rect x="\${-margin}" y="\${-margin}" width="\${size}" height="\${size}" fill="\${params["Background"]}"/>\`;
const xMax = matrixWidth - 1;
const yMax = matrixWidth - 1;
let baseX;
let baseY;
const on = params["Invert"]
? (x, y) => (matrix[y * matrixWidth + x] & 1) === 0
: (x, y) => (matrix[y * matrixWidth + x] & 1) === 1;
function goRight(x, y, shape, cw) {
let sx = x;
let vert = false;
visited[y * matrixWidth + x] = shape;
while (x < xMax) {
const right = on(x + 1, y);
const vertRight = y > 0 && on(x + 1, y - 1);
if (!right || vertRight) {
vert = right && vertRight;
break;
}
x++;
visited[y * matrixWidth + x] = shape;
}
paths[shape] += \`h\${(x - sx + 1) * unit}\`;
if (vert) {
goUp(x + 1, y - 1, shape, cw);
} else {
goDown(x, y, shape, cw);
}
}
function goLeft(x, y, shape, cw) {
let sx = x;
let vert = false;
visited[y * matrixWidth + x] = shape;
while (x > 0) {
const left = on(x - 1, y);
const vertLeft = y < yMax && on(x - 1, y + 1);
if (!left || vertLeft) {
vert = left && vertLeft;
break;
}
x--;
visited[y * matrixWidth + x] = shape;
}
if (!cw && x === baseX && y === baseY) {
paths[shape] += "z";
return;
}
paths[shape] += \`h\${(x - sx - 1) * unit}\`;
if (vert) {
goDown(x - 1, y + 1, shape, cw);
} else {
goUp(x, y, shape, cw);
}
}
function goUp(x, y, shape, cw) {
let sy = y;
let horz = false;
visited[y * matrixWidth + x] = shape;
while (y > 0) {
const up = on(x, y - 1);
const horzUp = x > 0 && on(x - 1, y - 1);
if (!up || horzUp) {
horz = up && horzUp;
break;
}
y--;
visited[y * matrixWidth + x] = shape;
}
if (cw && x === baseX && y === baseY) {
paths[shape] += "z";
return;
}
paths[shape] += \`v\${(y - sy - 1) * unit}\`;
if (horz) {
goLeft(x - 1, y - 1, shape, cw);
} else {
goRight(x, y, shape, cw);
}
}
function goDown(x, y, shape, cw) {
let sy = y;
let horz = false;
visited[y * matrixWidth + x] = shape;
while (y < yMax) {
const down = on(x, y + 1);
const horzDown = x < xMax && on(x + 1, y + 1);
if (!down || horzDown) {
horz = down && horzDown;
break;
}
y++;
visited[y * matrixWidth + x] = shape;
}
paths[shape] += \`v\${(y - sy + 1) * unit}\`;
if (horz) {
goRight(x + 1, y + 1, shape, cw);
} else {
goLeft(x, y, shape, cw);
}
}
const stack = [];
for (let x = 0; x < matrixWidth; x++) {
if (!on(x, 0)) stack.push([x, 0]);
}
for (let y = 1; y < yMax; y++) {
if (!on(0, y)) stack.push([0, y]);
if (!on(xMax, y)) stack.push([xMax, y]);
}
for (let x = 0; x < matrixWidth; x++) {
if (!on(x, yMax)) stack.push([x, yMax]);
}
// recursion dfs limited to ~4000
// visit all whitespace connected to edges
function dfsOff() {
while (stack.length > 0) {
const [x, y] = stack.pop();
if (visited[y * matrixWidth + x]) continue;
visited[y * matrixWidth + x] = 1;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dy === 0 && dx === 0) continue;
let nx = x + dx;
let ny = y + dy;
if (nx < 0 || nx > xMax || ny < 0 || ny > yMax) continue;
if (on(nx, ny)) continue;
stack.push([nx, ny]);
}
}
}
}
dfsOff();
const paths = [""];
for (let y = 0; y < matrixWidth; y++) {
for (let x = 0; x < matrixWidth; x++) {
if (visited[y * matrixWidth + x]) continue;
if (!on(x, y)) {
const shape = visited[y * matrixWidth + x - 1];
paths[shape] += \`M\${x * unit},\${y * unit}\`;
baseY = y - 1;
baseX = x;
goDown(x - 1, y, shape, false);
stack.push([x, y]);
dfsOff();
continue;
}
if (y > 0 && on(x, y - 1) && visited[(y - 1) * matrixWidth + x]) {
visited[y * matrixWidth + x] = visited[(y - 1) * matrixWidth + x];
continue;
}
if (x > 0 && on(x - 1, y) && visited[y * matrixWidth + x - 1]) {
visited[y * matrixWidth + x] = visited[y * matrixWidth + x - 1];
continue;
}
paths.push(\`M\${x * unit},\${y * unit}\`);
baseY = y;
baseX = x;
goRight(x, y, paths.length - 1, true);
}
}
function domToString(node) {
const attrs = Object.entries(node.attributes)
.map(([key, value]) => \`\${key}="\${value}"\`)
.join(" ");
svg += \`<\${node.tagName} \${attrs}>\`;
node.children.forEach(domToString);
svg += \`</\${node.tagName}>\`;
}
paths.forEach((path, i) => {
if (i === 0) return;
const g = roughSVG.path(path);
console.log(g);
domToString(g);
});
svg += \`</svg>\`;
return svg;
}
`