├── .gitignore ├── LICENSE ├── README.md ├── app.config.ts ├── examples ├── alien.svg ├── blocks.svg ├── boring1.png ├── boring2.png ├── boring3.png ├── bubbles.svg ├── camo.svg ├── childhood.svg ├── dots.svg ├── drawing1.png ├── drawing2.png ├── glass.svg ├── halftone.png ├── layers1.svg ├── layers2.svg ├── matrix.svg ├── mondrian.svg ├── neon.svg ├── quantum.svg ├── tile.png ├── ui2.png └── wood.svg ├── package.json ├── pnpm-lock.yaml ├── presets ├── Alien.js ├── Basic.js ├── Blocks.js ├── Bubbles.js ├── Camo.js ├── Circle.js ├── Dots.js ├── Drawing.js ├── Glass.js ├── Halftone.js ├── Layers.js ├── Minimal.js ├── Mondrian.js ├── Neon.js ├── Quantum.js ├── Tile.js └── Tutorial.js ├── public ├── favicon.svg ├── previewWorker.js ├── thumbnailWorker.js └── utils.js ├── src ├── app.css ├── app.tsx ├── coloris.css ├── components │ ├── Button.tsx │ ├── ButtonGroup.tsx │ ├── Collapsible.tsx │ ├── ColorInput.tsx │ ├── ContextMenu.tsx │ ├── Dialog.tsx │ ├── ErrorToasts.tsx │ ├── ImageInput.tsx │ ├── NumberInput.tsx │ ├── Select.tsx │ ├── SplitButton.tsx │ ├── Switch.tsx │ ├── TextInput.tsx │ ├── editor │ │ ├── AllowPasteDialog.tsx │ │ ├── CodeEditor.tsx │ │ ├── ParamsEditor.tsx │ │ ├── QrEditor.tsx │ │ └── Settings.tsx │ ├── preview │ │ └── QrPreview.tsx │ └── svg.tsx ├── entry-client.tsx ├── entry-server.tsx ├── global.d.ts ├── lib │ ├── QrContext.tsx │ ├── RenderContext.tsx │ ├── options.ts │ ├── params.ts │ ├── presets.ts │ └── util.ts └── routes │ ├── [...404].tsx │ ├── bugs.tsx │ └── index.tsx ├── tsconfig.json ├── uno.config.ts └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | dist 3 | .solid 4 | .output 5 | .vercel 6 | .netlify 7 | .vinxi 8 | 9 | # Environment 10 | .env 11 | .env*.local 12 | 13 | # dependencies 14 | /node_modules 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | *.launch 21 | .settings/ 22 | 23 | # Temp 24 | gitignore 25 | 26 | # System Files 27 | .DS_Store 28 | Thumbs.db 29 | 30 | # wrangler files 31 | .wrangler 32 | .dev.vars 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kyle Zheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qrframe 2 | 3 | code-based qr code generator 4 | 5 | Blatantly inspired by [QRBTF](https://qrbtf.com) and [Anthony Fu's QR Toolkit](https://qrcode.antfu.me). 6 | 7 | [Here's a post I wrote about crafting QR codes](https://kylezhe.ng/posts/crafting_qr_codes) that goes into deeper detail about how they work and ways to make them pretty. 8 | 9 | ## Examples 10 | 11 | > [!CAUTION] 12 | > These example QR codes may not be reliably scannable! Results may vary drastically based on device and scanner! 13 | 14 | This project is a tool to create designs! These are only examples! 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 28 | 31 | 32 | 33 | 36 | 39 | 42 | 43 | 44 | 47 | 50 | 53 | 54 | 55 | 58 | 61 | 64 | 65 | 66 | 67 | 68 | 71 | 74 | 77 | 78 | 79 | 80 | 81 | 84 | 87 | 90 | 91 | 92 | 93 | 94 | 97 | 100 | 103 | 104 | 105 | 106 |
Creative possibilities
23 | 24 | 26 | 27 | 29 | 30 |
34 | 35 | 37 | 38 | 40 | 41 |
45 | 46 | 48 | 49 | 51 | 52 |
56 | 57 | 59 | 60 | 62 | 63 |
Import external libs, fetch external files, etc
69 | 70 | 72 | 73 | 75 | 76 |
Styles copied from QRBTF
82 | 83 | 85 | 86 | 88 | 89 |
Boring options are available
95 | 96 | 98 | 99 | 101 | 102 |
107 | 108 | ## Create/modify designs with code 109 | 110 | ![code and parameter editor ui](./examples/ui2.png) 111 | 112 | ## Features 113 | 114 | - Customize data: 115 | 116 | - encoding mode, version, error tolerance, mask pattern 117 | - powered by [`fuqr`](https://github.com/zhengkyl/fuqr), my own Rust library imported as WASM. (i use windows, btw) 118 | 119 | - Customize appearance: 120 | - Choose any preset, customize or even create a new one from scratch via code editor. 121 | - Define arbitrary UI parameters in code 122 | - Supports SVG and PNG 123 | - All code runs _directly_ in browser in a web worker with no restrictions. 124 | - There is no sandbox, whitelist, blacklist, or anything besides a 5s timeout to stop infinite loops. 125 | - Generated SVGs are not sanitized. This is an impossible task and attempting it breaks perfectly fine SVGs, makes debugging harder, and adds latency to previewing changes. 126 | - These should be non-issues, but even if you copy-and-paste and run malware there's no secrets to leak. 127 | 128 | 129 | ## Creating a preset 130 | 131 | A preset must export `paramsSchema` and either `renderSVG` or `renderCanvas` 132 | 133 | ## `paramsSchema` 134 | 135 | This schema defines the UI components whose values are passed into `renderSVG` or `renderCanvas` via the `params` object. 136 | 137 | All properties besides `type` are optional, except 138 | 139 | - type `select` must have a nonempty options array 140 | - type `array` must have a valid `props` value. 141 | 142 | In this example, `default` is set explicitly to the implicit default value. 143 | 144 | ```js 145 | export const paramsSchema = { 146 | Example1: { 147 | type: "number", 148 | min: 0, 149 | max: 10, 150 | step: 0.1, 151 | default: 0, 152 | }, 153 | Example2: { 154 | type: "boolean", 155 | default: false, 156 | }, 157 | Example3: { 158 | type: "color", 159 | default: "#000000", // css color string (hex/rgba/hsla) 160 | }, 161 | Example4: { 162 | type: "select", 163 | options: ["I'm feeling", 22], 164 | default: "I'm feeling", // first option 165 | }, 166 | Example5: { 167 | type: "file", 168 | accept: ".jpeg, .jpg, .png", 169 | default: null, 170 | }, 171 | Example6: { 172 | type: "array", 173 | props: { 174 | type: "number", // any type except "array" 175 | // corresponding props 176 | }, 177 | resizable: true, 178 | defaultLength: 5, // overridden by default 179 | default: [], // overrides defaultLength 180 | }, 181 | }; 182 | ``` 183 | 184 | ## `renderSVG` and `renderCanvas` 185 | 186 | ```ts 187 | type renderSVG = (qr: Qr, params: Params) => string; 188 | 189 | type renderCanvas = (qr: Qr, params: Params, canvas: OffscreenCanvas) => void; 190 | ``` 191 | 192 | `params` is an object with all the keys of `paramsSchema` paired with the value from their respective input component. 193 | 194 | `qr` contains the final QR code in `matrix`. This represents a square where one side is `version * 4 + 17` wide, and modules (aka pixels) are stored from the left to right, top to bottom. 195 | 196 | ```ts 197 | type Qr = { 198 | matrix: Uint8Array; // see below 199 | version: number; // 1- 40 200 | mask: number; // 0 - 7, 201 | ecl: number; // 0 - 3, Low, Medium, Quartile, High 202 | mode: number; // 0 - 2, Numeric, Alphanumeric, Byte 203 | }; 204 | 205 | // bit flags for each u8 in matrix 206 | const Module = { 207 | ON: 1 << 0, 208 | DATA: 1 << 1, 209 | FINDER: 1 << 2, 210 | ALIGNMENT: 1 << 3, 211 | TIMING: 1 << 4, 212 | FORMAT: 1 << 5, 213 | VERSION: 1 << 6, 214 | MODIFIER: 1 << 7, 215 | }; 216 | ``` 217 | 218 | `MODIFIER` is set for Finder and Alignment centers, Format and Version copy. 219 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@solidjs/start/config"; 2 | import UnoCSS from "unocss/vite"; 3 | import wasmpack from "vite-plugin-wasm-pack"; 4 | 5 | export default defineConfig({ 6 | server: { 7 | preset: "cloudflare-pages-static", 8 | rollupConfig: { 9 | external: ["node:async_hooks"], 10 | }, 11 | }, 12 | ssr: true, 13 | vite: { 14 | plugins: [UnoCSS(), wasmpack([], ["fuqr"]), blobRewriter()], 15 | }, 16 | }); 17 | 18 | // Rewrites imports inside blobs in dev mode 19 | function blobRewriter() { 20 | const virtualModuleId = "virtual:blob-rewriter"; 21 | const resolvedVirtualModuleId = "\0" + virtualModuleId; 22 | 23 | return { 24 | name: "blob-rewriter", 25 | resolveId(id) { 26 | if (id === virtualModuleId) { 27 | return resolvedVirtualModuleId; 28 | } 29 | }, 30 | load(id) { 31 | if (id === resolvedVirtualModuleId) { 32 | if (process.env.NODE_ENV !== "development") { 33 | return "export {}"; 34 | } 35 | 36 | return ` 37 | if (!import.meta.env.SSR) { 38 | const originalBlob = window.Blob; 39 | window.Blob = function(array, options) { 40 | if (options.type === "text/javascript") { 41 | array = array.map(item => { 42 | return item.replace("https://qrframe.kylezhe.ng", "http://localhost:3000"); 43 | }); 44 | } 45 | return new originalBlob(array, options); 46 | } 47 | } 48 | `; 49 | } 50 | }, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /examples/boring1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengkyl/qrframe/d229ba221a6b070943e646cd7003f87367fa839c/examples/boring1.png -------------------------------------------------------------------------------- /examples/boring2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengkyl/qrframe/d229ba221a6b070943e646cd7003f87367fa839c/examples/boring2.png -------------------------------------------------------------------------------- /examples/boring3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengkyl/qrframe/d229ba221a6b070943e646cd7003f87367fa839c/examples/boring3.png -------------------------------------------------------------------------------- /examples/bubbles.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/drawing1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengkyl/qrframe/d229ba221a6b070943e646cd7003f87367fa839c/examples/drawing1.png -------------------------------------------------------------------------------- /examples/drawing2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengkyl/qrframe/d229ba221a6b070943e646cd7003f87367fa839c/examples/drawing2.png -------------------------------------------------------------------------------- /examples/halftone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengkyl/qrframe/d229ba221a6b070943e646cd7003f87367fa839c/examples/halftone.png -------------------------------------------------------------------------------- /examples/layers1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/layers2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/mondrian.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/neon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengkyl/qrframe/d229ba221a6b070943e646cd7003f87367fa839c/examples/tile.png -------------------------------------------------------------------------------- /examples/ui2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengkyl/qrframe/d229ba221a6b070943e646cd7003f87367fa839c/examples/ui2.png -------------------------------------------------------------------------------- /examples/wood.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrframe", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vinxi dev", 6 | "build": "vinxi build", 7 | "start": "vinxi start", 8 | "preview": "pnpm run build && npx wrangler pages dev", 9 | "deploy": "pnpm run build && wrangler pages deploy" 10 | }, 11 | "dependencies": { 12 | "@codemirror/commands": "^6.6.0", 13 | "@codemirror/lang-javascript": "^6.2.2", 14 | "@codemirror/language": "^6.10.2", 15 | "@codemirror/state": "^6.4.1", 16 | "@codemirror/theme-one-dark": "^6.1.2", 17 | "@codemirror/view": "^6.28.3", 18 | "@kobalte/core": "^0.13.4", 19 | "@melloware/coloris": "^0.24.0", 20 | "@replit/codemirror-vim": "^6.2.1", 21 | "@solidjs/router": "^0.13.3", 22 | "@solidjs/start": "^1.0.0", 23 | "@thisbeyond/solid-dnd": "^0.7.5", 24 | "@unocss/reset": "^0.59.4", 25 | "codemirror": "^6.0.1", 26 | "fuqr": "^1.0.0", 27 | "lucide-solid": "^0.474.0", 28 | "solid-js": "^1.9.2", 29 | "unocss": "^0.59.4", 30 | "vinxi": "^0.3.11" 31 | }, 32 | "devDependencies": { 33 | "@cloudflare/workers-types": "^4.20241011.0", 34 | "@unocss/transformer-variant-group": "^0.59.4", 35 | "prettier": "^3.3.3", 36 | "vite-plugin-wasm-pack": "^0.1.12", 37 | "wrangler": "^3.80.4" 38 | }, 39 | "pnpm": { 40 | "overrides": { 41 | "@internationalized/date": "^3.5.4", 42 | "@internationalized/number": "^3.5.3" 43 | } 44 | }, 45 | "engines": { 46 | "node": ">=18" 47 | } 48 | } -------------------------------------------------------------------------------- /presets/Alien.js: -------------------------------------------------------------------------------- 1 | // Based on QRBTF's Line style 2 | // https://github.com/CPunisher/react-qrbtf/blob/master/src/components/QRLine.tsx 3 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 4 | 5 | export const paramsSchema = { 6 | Margin: { 7 | type: "number", 8 | min: 0, 9 | max: 10, 10 | step: 0.1, 11 | default: 2, 12 | }, 13 | Background: { 14 | type: "color", 15 | default: "#ffffff", 16 | }, 17 | Dots: { 18 | type: "color", 19 | default: "#000000", 20 | }, 21 | Lines: { 22 | type: "color", 23 | default: "#000000", 24 | }, 25 | Seed: { 26 | type: "number", 27 | min: 1, 28 | max: 100, 29 | default: 1, 30 | }, 31 | }; 32 | 33 | export function renderSVG(qr, params) { 34 | const rand = getSeededRand(params["Seed"]); 35 | const rangeStr = (min, max) => (rand() * (max - min) + min).toFixed(2); 36 | 37 | const rowLen = qr.version * 4 + 17; 38 | const margin = params["Margin"]; 39 | const bg = params["Background"]; 40 | const dots = params["Dots"]; 41 | const lines = params["Lines"]; 42 | 43 | const size = rowLen + 2 * margin; 44 | let svg = ``; 45 | svg += ``; 46 | 47 | let linesLayer = ``; 48 | let dotsLayer = ``; 49 | 50 | function matrix(x, y) { 51 | return qr.matrix[y * rowLen + x]; 52 | } 53 | 54 | const rightVisited = Array(rowLen * rowLen).fill(false); 55 | const leftVisited = Array(rowLen * rowLen).fill(false); 56 | function visited1(x, y) { 57 | return rightVisited[y * rowLen + x]; 58 | } 59 | function visited2(x, y) { 60 | return leftVisited[y * rowLen + x]; 61 | } 62 | function setVisited1(x, y) { 63 | rightVisited[y * rowLen + x] = true; 64 | } 65 | function setVisited2(x, y) { 66 | leftVisited[y * rowLen + x] = true; 67 | } 68 | 69 | for (const [x, y] of [ 70 | [0, 0], 71 | [rowLen - 7, 0], 72 | [0, rowLen - 7], 73 | ]) { 74 | dotsLayer += ``; 75 | 76 | dotsLayer += ``; 77 | dotsLayer += ``; 78 | dotsLayer += ``; 79 | 80 | dotsLayer += ``; 81 | dotsLayer += ``; 82 | 83 | dotsLayer += ``; 84 | dotsLayer += ``; 85 | dotsLayer += ``; 86 | 87 | linesLayer += ``; 88 | linesLayer += ``; 89 | } 90 | 91 | for (let y = 0; y < rowLen; y++) { 92 | for (let x = 0; x < rowLen; x++) { 93 | const module = matrix(x, y); 94 | if (module & Module.FINDER) continue; 95 | 96 | if (!(module & Module.ON)) continue; 97 | dotsLayer += ``; 98 | 99 | if (!visited1(x, y)) { 100 | let nx = x + 1; 101 | let ny = y + 1; 102 | while ( 103 | nx < rowLen && 104 | ny < rowLen && 105 | matrix(nx, ny) & Module.ON && 106 | !visited1(nx, ny) 107 | ) { 108 | setVisited1(nx, ny); 109 | nx++; 110 | ny++; 111 | } 112 | if (ny - y > 1) { 113 | linesLayer += ``; 114 | } 115 | } 116 | 117 | if (!visited2(x, y)) { 118 | let nx = x - 1; 119 | let ny = y + 1; 120 | while ( 121 | nx >= 0 && 122 | ny < rowLen && 123 | matrix(nx, ny) & Module.ON && 124 | !visited2(nx, ny) 125 | ) { 126 | setVisited2(nx, ny); 127 | nx--; 128 | ny++; 129 | } 130 | if (ny - y > 1) { 131 | linesLayer += ``; 132 | } 133 | } 134 | } 135 | } 136 | 137 | linesLayer += ``; 138 | svg += linesLayer; 139 | dotsLayer += ``; 140 | svg += dotsLayer; 141 | svg += ``; 142 | 143 | return svg; 144 | } 145 | -------------------------------------------------------------------------------- /presets/Basic.js: -------------------------------------------------------------------------------- 1 | import { Module } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Margin: { 5 | type: "number", 6 | min: 0, 7 | max: 10, 8 | step: 0.1, 9 | default: 2, 10 | }, 11 | Foreground: { 12 | type: "color", 13 | default: "#000000", 14 | }, 15 | Background: { 16 | type: "color", 17 | default: "#ffffff", 18 | }, 19 | Shape: { 20 | type: "select", 21 | options: ["Square-Circle", "Diamond-Squircle"], 22 | }, 23 | Frame: { 24 | type: "select", 25 | options: ["None", "Corners"], 26 | }, 27 | Roundness: { 28 | type: "number", 29 | min: 0, 30 | max: 1, 31 | step: 0.01, 32 | default: 0, 33 | }, 34 | "Pixel size": { 35 | type: "number", 36 | min: 0.5, 37 | max: 1.5, 38 | step: 0.1, 39 | default: 1, 40 | }, 41 | Logo: { 42 | type: "file", 43 | accept: ".jpeg, .jpg, .png, .svg", 44 | }, 45 | "Logo size": { 46 | type: "number", 47 | min: 0, 48 | max: 1, 49 | step: 0.01, 50 | default: 0.25, 51 | }, 52 | "Show data behind logo": { 53 | type: "boolean", 54 | }, 55 | }; 56 | 57 | export async function renderSVG(qr, params) { 58 | const rowLen = qr.version * 4 + 17; 59 | const margin = params["Margin"]; 60 | const fg = params["Foreground"]; 61 | const bg = params["Background"]; 62 | const defaultShape = params["Shape"] === "Square-Circle"; 63 | const roundness = params["Roundness"]; 64 | const file = params["Logo"]; 65 | const logoRatio = params["Logo size"]; 66 | const showLogoData = params["Show data behind logo"]; 67 | 68 | const size = rowLen + 2 * margin; 69 | let svg = ``; 70 | svg += ``; 71 | 72 | svg += ``; 73 | 74 | if (params["Frame"] === "Corners") { 75 | const bracketRadius = 2.2 * roundness; 76 | const bracketStraight = 5 + margin / 2 - bracketRadius; 77 | svg += brackets( 78 | -margin / 2, 79 | -margin / 2, 80 | size - margin, 81 | bracketRadius, 82 | bracketStraight, 83 | fg 84 | ); 85 | } 86 | 87 | svg += ``; 108 | 109 | const dataSize = params["Pixel size"]; 110 | const dataRadius = (roundness * dataSize) / 2; 111 | const dataOffset = (1 - dataSize) / 2; 112 | 113 | if (!defaultShape || !roundness) svg += ``; 137 | } else { 138 | svg += `M${x + dataOffset},${y + dataOffset}h${dataSize}v${dataSize}h-${dataSize}z`; 139 | } 140 | } else { 141 | svg += squircle( 142 | x + dataOffset, 143 | y + dataOffset, 144 | dataSize, 145 | dataRadius, 146 | true 147 | ); 148 | } 149 | } 150 | } 151 | if (!defaultShape || !roundness) svg += `"/>`; 152 | svg += ``; 153 | 154 | if (file != null) { 155 | const bytes = new Uint8Array(await file.arrayBuffer()); 156 | const b64 = btoa( 157 | Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("") 158 | ); 159 | const logoSize = fmt(logoRatio * size); 160 | const logoOffset = fmt(((1 - logoRatio) * size) / 2 - margin); 161 | svg += ``; 162 | } 163 | 164 | svg += ``; 165 | return svg; 166 | } 167 | 168 | // reduce file bloat from floating point math 169 | const fmt = (n) => n.toFixed(2).replace(/.00$/, ""); 170 | 171 | function squircle(x, y, width, handle, cw) { 172 | const half = fmt(width / 2); 173 | 174 | if (handle === 0) { 175 | return cw 176 | ? `M${fmt(x + width / 2)},${fmt(y)}l${half},${half}l-${half},${half}l-${half},-${half}z` 177 | : `M${fmt(x + width / 2)},${fmt(y)}l-${half},${half}l${half},${half}l${half},-${half}z`; 178 | } 179 | 180 | const h = fmt(handle); 181 | const hInv1 = fmt(half - handle); 182 | const hInv2 = fmt(-(half - handle)); 183 | return cw 184 | ? `M${fmt(x + width / 2)},${fmt(y)}c${h},0 ${half},${hInv1} ${half},${half}s${hInv2},${half} -${half},${half}s-${half},${hInv2} -${half},-${half}s${hInv1},-${half} ${half},-${half}` 185 | : `M${fmt(x + width / 2)},${fmt(y)}c-${h},0 -${half},${hInv1} -${half},${half}s${hInv1},${half} ${half},${half}s${half},${hInv2} ${half},-${half}s${hInv2},-${half} -${half},-${half}`; 186 | } 187 | 188 | function roundedRect(x, y, width, radius, cw) { 189 | if (radius === 0) { 190 | return cw 191 | ? `M${fmt(x)},${fmt(y)}h${width}v${width}h-${width}z` 192 | : `M${fmt(x)},${fmt(y)}v${width}h${width}v-${width}z`; 193 | } 194 | 195 | if (radius === width / 2) { 196 | const r = fmt(radius); 197 | const cwFlag = cw ? "1" : "0"; 198 | return `M${fmt(x + radius)},${fmt(y)}a${r},${r} 0,0,${cwFlag} 0,${width}a${r},${r} 0,0,${cwFlag} ${0},-${width}`; 199 | } 200 | 201 | const r = fmt(radius); 202 | const side = fmt(width - 2 * radius); 203 | return cw 204 | ? `M${fmt(x + radius)},${fmt(y)}h${side}a${r},${r} 0,0,1 ${r},${r}v${side}a${r},${r} 0,0,1 -${r},${r}h-${side}a${r},${r} 0,0,1 -${r},-${r}v-${side}a${r},${r} 0,0,1 ${r},-${r}` 205 | : `M${fmt(x + radius)},${fmt(y)}a${r},${r} 0,0,0 -${r},${r}v${side}a${r},${r} 0,0,0 ${r},${r}h${side}a${r},${r} 0,0,0 ${r},-${r}v-${side}a${r},${r} 0,0,0 -${r},-${r}`; 206 | } 207 | 208 | function brackets(x, y, width, radius, straight, stroke) { 209 | const bracket = radius + straight; 210 | const side = fmt(width - 2 * bracket); 211 | const r = fmt(radius); 212 | 213 | const cap = radius === 0 ? "square" : "round"; 214 | 215 | let svg = ``; 222 | return svg; 223 | } 224 | -------------------------------------------------------------------------------- /presets/Blocks.js: -------------------------------------------------------------------------------- 1 | // Based on QRBTF's DSJ style 2 | // https://github.com/CPunisher/react-qrbtf/blob/master/src/components/QRDsj.tsx 3 | import { Module } from "https://qrframe.kylezhe.ng/utils.js"; 4 | 5 | export const paramsSchema = { 6 | Margin: { 7 | type: "number", 8 | min: 0, 9 | max: 10, 10 | step: 0.1, 11 | default: 2, 12 | }, 13 | Background: { 14 | type: "color", 15 | default: "#ffffff", 16 | }, 17 | Finder: { 18 | type: "color", 19 | default: "#131d87", 20 | }, 21 | Horizontal: { 22 | type: "color", 23 | default: "#dc9c07", 24 | }, 25 | Vertical: { 26 | type: "color", 27 | default: "#d21313", 28 | }, 29 | Cross: { 30 | type: "color", 31 | default: "#131d87", 32 | }, 33 | "Horizontal thickness": { 34 | type: "number", 35 | min: 0, 36 | max: 1, 37 | step: 0.1, 38 | default: 0.7, 39 | }, 40 | "Vertical thickness": { 41 | type: "number", 42 | min: 0, 43 | max: 1, 44 | step: 0.1, 45 | default: 0.7, 46 | }, 47 | "Cross thickness": { 48 | type: "number", 49 | min: 0, 50 | max: 1, 51 | step: 0.1, 52 | default: 0.7, 53 | }, 54 | }; 55 | 56 | export function renderSVG(qr, params) { 57 | const rowLen = qr.version * 4 + 17; 58 | const margin = params["Margin"]; 59 | const bg = params["Background"]; 60 | const fc = params["Finder"]; 61 | 62 | const hc = params["Horizontal"]; 63 | const ht = params["Horizontal thickness"]; 64 | const ho = (1 - ht) / 2; 65 | 66 | const vc = params["Vertical"]; 67 | const vt = params["Vertical thickness"]; 68 | const vo = (1 - vt) / 2; 69 | 70 | const cc = params["Cross"]; 71 | const ct = params["Cross thickness"]; 72 | const co = ct / Math.sqrt(8); // offset 73 | 74 | const size = rowLen + 2 * margin; 75 | let svg = ``; 76 | svg += ``; 77 | 78 | let crossLayer = ``; 79 | let vLayer = ``; 80 | let hLayer = ``; 81 | 82 | function matrix(x, y) { 83 | return qr.matrix[y * rowLen + x]; 84 | } 85 | 86 | const visitedMatrix = Array(rowLen * rowLen).fill(false); 87 | function visited(x, y) { 88 | return visitedMatrix[y * rowLen + x]; 89 | } 90 | function setVisited(x, y) { 91 | visitedMatrix[y * rowLen + x] = true; 92 | } 93 | 94 | svg += ``; 95 | for (const [x, y] of [ 96 | [0, 0], 97 | [rowLen - 7, 0], 98 | [0, rowLen - 7], 99 | ]) { 100 | svg += ``; 101 | svg += ``; 102 | svg += ``; 103 | svg += ``; 104 | svg += ``; 105 | } 106 | svg += ``; 107 | 108 | for (let y = 0; y < rowLen; y++) { 109 | for (let x = 0; x < rowLen; x++) { 110 | const module = matrix(x, y); 111 | if (module & Module.FINDER) continue; 112 | if (!(module & Module.ON)) continue; 113 | if (visited(x, y)) continue; 114 | setVisited(x, y); 115 | 116 | if ( 117 | y < rowLen - 2 && 118 | x < rowLen - 2 && 119 | matrix(x + 2, y) & 120 | matrix(x, y + 2) & 121 | matrix(x + 1, y + 1) & 122 | matrix(x + 2, y + 2) & 123 | 1 124 | ) { 125 | if ( 126 | !visited(x + 1, y) && 127 | !visited(x + 2, y) && 128 | !visited(x, y + 1) && 129 | !visited(x + 2, y + 1) 130 | ) { 131 | crossLayer += ``; 132 | crossLayer += ``; 133 | crossLayer += ``; 134 | crossLayer += ``; 135 | 136 | setVisited(x + 2, y); 137 | setVisited(x, y + 2); 138 | setVisited(x + 1, y + 1); 139 | setVisited(x + 2, y + 2); 140 | continue; 141 | } 142 | } 143 | if ( 144 | y < rowLen - 1 && 145 | x < rowLen - 1 && 146 | matrix(x + 1, y) & matrix(x, y + 1) & matrix(x + 1, y + 1) & Module.ON 147 | ) { 148 | if ( 149 | !visited(x + 1, y) && 150 | !visited(x + 1, y + 1) && 151 | !visited(x, y + 1) 152 | ) { 153 | crossLayer += ``; 154 | crossLayer += ``; 155 | crossLayer += ``; 156 | crossLayer += ``; 157 | 158 | setVisited(x + 1, y); 159 | setVisited(x, y + 1); 160 | setVisited(x + 1, y + 1); 161 | continue; 162 | } 163 | } 164 | 165 | let ny = y + 1; 166 | while (ny < rowLen && matrix(x, ny) & Module.ON && !visited(x, ny)) { 167 | ny++; 168 | } 169 | if (ny - y > 2) { 170 | vLayer += ``; 171 | vLayer += ``; 172 | for (let i = y + 1; i < ny; i++) { 173 | setVisited(x, i); 174 | } 175 | continue; 176 | } 177 | 178 | let nx = x + 1; 179 | while (nx < rowLen && matrix(nx, y) & Module.ON && !visited(nx, y)) { 180 | setVisited(nx, y); 181 | nx++; 182 | } 183 | hLayer += ``; 184 | } 185 | } 186 | 187 | vLayer += ``; 188 | svg += vLayer; 189 | hLayer += ``; 190 | svg += hLayer; 191 | crossLayer += ``; 192 | svg += crossLayer; 193 | 194 | svg += ``; 195 | 196 | return svg; 197 | } 198 | -------------------------------------------------------------------------------- /presets/Bubbles.js: -------------------------------------------------------------------------------- 1 | // Based on QRBTF's Bubble style 2 | // https://github.com/CPunisher/react-qrbtf/blob/master/src/components/QRBubble.tsx 3 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 4 | 5 | export const paramsSchema = { 6 | Margin: { 7 | type: "number", 8 | min: 0, 9 | max: 10, 10 | step: 0.1, 11 | default: 2, 12 | }, 13 | Background: { 14 | type: "color", 15 | default: "#ffffff", 16 | }, 17 | Finder: { 18 | type: "color", 19 | default: "#141e92", 20 | }, 21 | "Large circle": { 22 | type: "color", 23 | default: "#10a8e9", 24 | }, 25 | "Medium circle": { 26 | type: "color", 27 | default: "#1aa8cc", 28 | }, 29 | "Small circle": { 30 | type: "color", 31 | default: "#0f8bdd", 32 | }, 33 | "Tiny circle": { 34 | type: "color", 35 | default: "#012c8f", 36 | }, 37 | "Randomize circle size": { 38 | type: "boolean", 39 | }, 40 | Seed: { 41 | type: "number", 42 | min: 1, 43 | max: 100, 44 | default: 1, 45 | }, 46 | }; 47 | 48 | export function renderSVG(qr, params) { 49 | const rand = getSeededRand(params["Seed"]); 50 | 51 | const rangeStr = params["Randomize circle size"] 52 | ? (min, max) => (rand() * (max - min) + min).toFixed(2) 53 | : (min, max) => ((max - min) / 2 + min).toFixed(2); 54 | 55 | const rowLen = qr.version * 4 + 17; 56 | const margin = params["Margin"]; 57 | const bg = params["Background"]; 58 | 59 | const size = rowLen + 2 * margin; 60 | let svg = ``; 61 | svg += ``; 62 | 63 | let layer1 = ``; 64 | let layer2 = ``; 65 | let layer3 = ``; 66 | let layer4 = ``; 67 | 68 | function matrix(x, y) { 69 | return qr.matrix[y * rowLen + x]; 70 | } 71 | 72 | const visitedMatrix = Array(rowLen * rowLen).fill(false); 73 | function visited(x, y) { 74 | return visitedMatrix[y * rowLen + x]; 75 | } 76 | function setVisited(x, y) { 77 | visitedMatrix[y * rowLen + x] = true; 78 | } 79 | 80 | const fc = params["Finder"]; 81 | for (const [x, y] of [ 82 | [0, 0], 83 | [rowLen - 7, 0], 84 | [0, rowLen - 7], 85 | ]) { 86 | svg += ``; 87 | svg += ``; 88 | } 89 | 90 | for (let y = 0; y < rowLen; y++) { 91 | for (let x = 0; x < rowLen; x++) { 92 | const module = matrix(x, y); 93 | if (module & Module.FINDER) continue; 94 | if (visited(x, y)) continue; 95 | 96 | if ( 97 | y < rowLen - 2 && 98 | x < rowLen - 2 && 99 | matrix(x + 1, y) & 100 | matrix(x, y + 1) & 101 | matrix(x + 2, y + 1) & 102 | matrix(x + 1, y + 2) & 103 | 1 && 104 | !visited(x + 1, y) && 105 | !visited(x + 2, y) && 106 | !visited(x + 1, y + 1) && 107 | !visited(x + 2, y + 1) 108 | ) { 109 | layer1 += ``; 110 | 111 | setVisited(x + 1, y); 112 | setVisited(x, y + 1); 113 | setVisited(x + 2, y + 1); 114 | setVisited(x + 1, y + 2); 115 | continue; 116 | } 117 | if (!(module & Module.ON)) continue; 118 | setVisited(x, y); 119 | 120 | if ( 121 | y < rowLen - 1 && 122 | x < rowLen - 1 && 123 | matrix(x + 1, y) & 124 | matrix(x, y + 1) & 125 | matrix(x + 1, y + 1) & 126 | Module.ON && 127 | !visited(x + 1, y) && 128 | !visited(x + 1, y + 1) 129 | ) { 130 | layer2 += ``; 131 | setVisited(x + 1, y); 132 | setVisited(x, y + 1); 133 | setVisited(x + 1, y + 1); 134 | continue; 135 | } 136 | if ( 137 | x < rowLen - 1 && 138 | matrix(x + 1, y) & Module.ON && 139 | !visited(x + 1, y) 140 | ) { 141 | layer3 += ``; 142 | setVisited(x + 1, y); 143 | continue; 144 | } 145 | if ( 146 | y < rowLen - 1 && 147 | matrix(x, y + 1) & Module.ON && 148 | !visited(x, y + 1) 149 | ) { 150 | layer3 += ``; 151 | setVisited(x, y + 1); 152 | continue; 153 | } 154 | 155 | layer4 += ``; 156 | } 157 | } 158 | 159 | layer1 += ``; 160 | svg += layer1; 161 | layer2 += ``; 162 | svg += layer2; 163 | layer3 += ``; 164 | svg += layer3; 165 | layer4 += ``; 166 | svg += layer4; 167 | 168 | svg += ``; 169 | 170 | return svg; 171 | } 172 | -------------------------------------------------------------------------------- /presets/Camo.js: -------------------------------------------------------------------------------- 1 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Foreground: { 5 | type: "color", 6 | default: "#1c4a1a", 7 | }, 8 | Background: { 9 | type: "color", 10 | default: "#e3d68a", 11 | }, 12 | Margin: { 13 | type: "number", 14 | min: 0, 15 | max: 10, 16 | default: 3, 17 | }, 18 | "Quiet zone": { 19 | type: "number", 20 | min: 0, 21 | max: 10, 22 | default: 1, 23 | }, 24 | Invert: { 25 | type: "boolean", 26 | }, 27 | Seed: { 28 | type: "number", 29 | min: 1, 30 | max: 100, 31 | default: 1, 32 | }, 33 | }; 34 | 35 | export function renderSVG(qr, params) { 36 | const rand = getSeededRand(params["Seed"]); 37 | const margin = params["Margin"]; 38 | const quietZone = params["Quiet zone"]; 39 | const fg = params["Foreground"]; 40 | const bg = params["Background"]; 41 | 42 | const qrRowLen = qr.version * 4 + 17; 43 | const rowLen = qrRowLen + 2 * margin; 44 | 45 | const newMatrix = Array(rowLen * rowLen).fill(0); 46 | const visited = new Uint16Array(rowLen * rowLen); 47 | 48 | // Copy qr to matrix with margin and randomly set pixels in margin 49 | for (let y = 0; y < margin - quietZone; y++) { 50 | for (let x = 0; x < rowLen; x++) { 51 | if (rand() > 0.5) newMatrix[y * rowLen + x] = Module.ON; 52 | } 53 | } 54 | for (let y = margin - quietZone; y < margin + qrRowLen + quietZone; y++) { 55 | for (let x = 0; x < margin - quietZone; x++) { 56 | if (rand() > 0.5) newMatrix[y * rowLen + x] = Module.ON; 57 | } 58 | if (y >= margin && y < margin + qrRowLen) { 59 | for (let x = margin; x < rowLen - margin; x++) { 60 | newMatrix[y * rowLen + x] = 61 | qr.matrix[(y - margin) * qrRowLen + x - margin]; 62 | } 63 | } 64 | for (let x = margin + qrRowLen + quietZone; x < rowLen; x++) { 65 | if (rand() > 0.5) newMatrix[y * rowLen + x] = Module.ON; 66 | } 67 | } 68 | for (let y = margin + qrRowLen + quietZone; y < rowLen; y++) { 69 | for (let x = 0; x < rowLen; x++) { 70 | if (rand() > 0.5) newMatrix[y * rowLen + x] = Module.ON; 71 | } 72 | } 73 | if (quietZone === 0 && margin > 0) { 74 | for (let x = margin; x < margin + 7; x++) { 75 | newMatrix[(margin - 1) * rowLen + x] = 0; 76 | newMatrix[(margin - 1) * rowLen + x + qrRowLen - 7] = 0; 77 | } 78 | for (let y = margin; y < margin + 7; y++) { 79 | newMatrix[y * rowLen + margin - 1] = 0; 80 | newMatrix[y * rowLen + rowLen - margin] = 0; 81 | } 82 | for (let y = margin + qrRowLen - 7; y < margin + qrRowLen; y++) { 83 | newMatrix[y * rowLen + margin - 1] = 0; 84 | } 85 | for (let x = margin; x < margin + 7; x++) { 86 | newMatrix[(rowLen - margin) * rowLen + x] = 0; 87 | } 88 | } 89 | 90 | let svg = ``; 91 | svg += ``; 92 | svg += ``; 93 | 94 | const xMax = rowLen - 1; 95 | const yMax = rowLen - 1; 96 | 97 | let baseX; 98 | let baseY; 99 | 100 | const on = params["Invert"] 101 | ? (x, y) => (newMatrix[y * rowLen + x] & Module.ON) === 0 102 | : (x, y) => (newMatrix[y * rowLen + x] & Module.ON) !== 0; 103 | 104 | function go(x, y, dx, dy, path, cw) { 105 | visited[y * rowLen + x] = path; 106 | let concave = false; 107 | 108 | let nx = x + dx; 109 | let ny = y + dy; 110 | while (nx >= 0 && nx <= xMax && ny >= 0 && ny <= yMax) { 111 | const next = on(nx, ny); 112 | const cx = nx + dy; 113 | const cy = ny - dx; 114 | const diag = cx >= 0 && cx <= xMax && cy >= 0 && cy <= yMax && on(cx, cy); 115 | if (!next || diag) { 116 | concave = next && diag; 117 | break; 118 | } 119 | visited[ny * rowLen + nx] = path; 120 | nx += dx; 121 | ny += dy; 122 | } 123 | 124 | if (nx - dx === baseX && ny - dy === baseY) { 125 | if ((cw && dy === -1) || (!cw && dx === -1)) { 126 | paths[path] += "z"; 127 | return; 128 | } 129 | } 130 | 131 | if (dx !== 0) { 132 | const dist = nx - x - dx * 2 * 0.5; 133 | if (dist) paths[path] += `h${dist}`; 134 | } else { 135 | const dist = ny - y - dy * 2 * 0.5; 136 | if (dist) paths[path] += `v${dist}`; 137 | } 138 | 139 | if (concave) { 140 | paths[path] += `a.5.5 0,0,0 ${(dx + dy) * 0.5},${(dy - dx) * 0.5}`; 141 | go(nx + dy, ny - dx, dy, -dx, path, cw); 142 | } else { 143 | paths[path] += `a.5.5 0,0,1 ${(dx - dy) * 0.5},${(dy + dx) * 0.5}`; 144 | go(nx - dx, ny - dy, -dy, dx, path, cw); 145 | } 146 | } 147 | 148 | const stack = []; 149 | for (let x = 0; x < rowLen; x++) { 150 | if (!on(x, 0)) stack.push([x, 0]); 151 | } 152 | for (let y = 1; y < yMax; y++) { 153 | if (!on(0, y)) stack.push([0, y]); 154 | if (!on(xMax, y)) stack.push([xMax, y]); 155 | } 156 | for (let x = 0; x < rowLen; x++) { 157 | if (!on(x, yMax)) stack.push([x, yMax]); 158 | } 159 | 160 | // visit all whitespace connected to edges 161 | function dfsOff() { 162 | while (stack.length > 0) { 163 | const [x, y] = stack.pop(); 164 | if (visited[y * rowLen + x]) continue; 165 | visited[y * rowLen + x] = 1; 166 | for (let dy = -1; dy <= 1; dy++) { 167 | for (let dx = -1; dx <= 1; dx++) { 168 | if (dy === 0 && dx === 0) continue; 169 | let nx = x + dx; 170 | let ny = y + dy; 171 | if (nx < 0 || nx > xMax || ny < 0 || ny > yMax) continue; 172 | if (on(nx, ny)) continue; 173 | stack.push([nx, ny]); 174 | } 175 | } 176 | } 177 | } 178 | dfsOff(); 179 | 180 | const paths = [""]; 181 | for (let y = 0; y < rowLen; y++) { 182 | for (let x = 0; x < rowLen; x++) { 183 | if (visited[y * rowLen + x]) continue; 184 | 185 | if (!on(x, y)) { 186 | const path = visited[y * rowLen + x - 1]; 187 | paths[path] += `M${x + 0.5},${y}a.5.5 0,0,0 -.5.5`; 188 | 189 | baseY = y - 1; 190 | baseX = x; 191 | go(x - 1, y, 0, 1, path, false); 192 | stack.push([x, y]); 193 | dfsOff(); 194 | continue; 195 | } 196 | 197 | if (y > 0 && on(x, y - 1) && visited[(y - 1) * rowLen + x]) { 198 | visited[y * rowLen + x] = visited[(y - 1) * rowLen + x]; 199 | continue; 200 | } 201 | if (x > 0 && on(x - 1, y) && visited[y * rowLen + x - 1]) { 202 | visited[y * rowLen + x] = visited[y * rowLen + x - 1]; 203 | continue; 204 | } 205 | 206 | paths.push(``; 217 | }); 218 | 219 | svg += ``; 220 | return svg; 221 | } 222 | -------------------------------------------------------------------------------- /presets/Circle.js: -------------------------------------------------------------------------------- 1 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Margin: { 5 | type: "number", 6 | min: 0, 7 | max: 20, 8 | step: 0.1, 9 | default: 8, 10 | }, 11 | "Radius offset": { 12 | type: "number", 13 | min: -10, 14 | max: 10, 15 | default: 0, 16 | }, 17 | Foreground: { 18 | type: "color", 19 | default: "#000000", 20 | }, 21 | Background: { 22 | type: "color", 23 | default: "#ffffff", 24 | }, 25 | "Frame thickness": { 26 | type: "number", 27 | min: 0, 28 | max: 10, 29 | step: 0.1, 30 | }, 31 | "Finder pattern": { 32 | type: "select", 33 | options: ["Default", "Circle", "Square"], 34 | }, 35 | "Alignment pattern": { 36 | type: "select", 37 | options: ["Default", "Circle", "Square"], 38 | }, 39 | Logo: { 40 | type: "file", 41 | accept: ".jpeg, .jpg, .png, .svg", 42 | }, 43 | "Logo size": { 44 | type: "number", 45 | min: 0, 46 | max: 1, 47 | step: 0.01, 48 | default: 0.25, 49 | }, 50 | "Show data behind logo": { 51 | type: "boolean", 52 | }, 53 | "Pixel size": { 54 | type: "select", 55 | options: ["None", "Center", "Edge", "Random"], 56 | }, 57 | Seed: { 58 | type: "number", 59 | min: 1, 60 | max: 100, 61 | default: 1, 62 | }, 63 | }; 64 | 65 | const fmt = (n) => n.toFixed(2).replace(/.00$/, ""); 66 | 67 | export async function renderSVG(qr, params) { 68 | const rowLen = qr.version * 4 + 17; 69 | const margin = params["Margin"]; 70 | const fg = params["Foreground"]; 71 | const bg = params["Background"]; 72 | const rOffset = params["Radius offset"]; 73 | const file = params["Logo"]; 74 | const logoRatio = params["Logo size"]; 75 | const showLogoData = params["Show data behind logo"]; 76 | const rand = getSeededRand(params["Seed"]); 77 | const range = (min, max) => rand() * (max - min) + min; 78 | 79 | const size = rowLen + 2 * margin; 80 | 81 | let svg = ``; 82 | svg += ``; 83 | 84 | // nearest odd number 85 | let diameter = Math.round(Math.sqrt(2) * rowLen) + 2 * rOffset; 86 | if (!(diameter & 1)) diameter += 1; 87 | 88 | const frameThick = params["Frame thickness"]; 89 | if (frameThick) { 90 | const frameR = diameter / 2 + 1 + frameThick / 2; 91 | svg += ``; 92 | if (rOffset < -1) { 93 | const c = rowLen / 2; 94 | const offset = (frameR * Math.sqrt(2)) / 2; 95 | const r = (-rOffset + 1) * Math.max(frameThick / 2, 1); 96 | svg += ``; 97 | svg += ``; 98 | svg += ``; 99 | if (rOffset < -2) { 100 | svg += ``; 101 | } 102 | } 103 | } 104 | 105 | if (params["Finder pattern"] !== "Default") { 106 | for (const [x, y] of [ 107 | [0, 0], 108 | [rowLen - 7, 0], 109 | [0, rowLen - 7], 110 | ]) { 111 | if (params["Finder pattern"] === "Circle") { 112 | svg += ``; 113 | svg += ``; 114 | } else { 115 | svg += ``; 116 | } 117 | } 118 | } 119 | svg += ` diameter / 2) { 181 | continue; 182 | } else if (rand() > 0.5) { 183 | continue; 184 | } 185 | 186 | let ratio; 187 | switch (params["Pixel size"]) { 188 | case "Center": 189 | ratio = 1 - dist / maxDist + 0.8; 190 | break; 191 | case "Edge": 192 | ratio = dist / maxDist + 0.8; 193 | break; 194 | case "Random": 195 | ratio = range(0.8, 1.2); 196 | break; 197 | default: 198 | ratio = 1; 199 | } 200 | 201 | const radius = fmt(0.5 * ratio); 202 | 203 | svg += `M${x + 0.5},${y + 0.5 - radius}a${radius},${radius} 0,0,0 0,${2 * radius}a${radius},${radius} 0,0,0 0,${-2 * radius}`; 204 | } 205 | } 206 | svg += `"/>`; 207 | 208 | if (file != null) { 209 | const bytes = new Uint8Array(await file.arrayBuffer()); 210 | const b64 = btoa( 211 | Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("") 212 | ); 213 | const logoSize = fmt(logoRatio * size); 214 | const logoOffset = fmt(((1 - logoRatio) * size) / 2 - margin); 215 | svg += ``; 216 | } 217 | svg += ``; 218 | 219 | return svg; 220 | } 221 | -------------------------------------------------------------------------------- /presets/Dots.js: -------------------------------------------------------------------------------- 1 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Margin: { 5 | type: "number", 6 | min: 0, 7 | max: 10, 8 | step: 0.1, 9 | default: 2, 10 | }, 11 | Density: { 12 | type: "number", 13 | min: 2, 14 | max: 10, 15 | default: 4, 16 | }, 17 | "Finder clarity": { 18 | type: "number", 19 | min: 1, 20 | max: 1.5, 21 | step: 0.1, 22 | default: 1.3, 23 | }, 24 | Foreground: { 25 | type: "array", 26 | resizable: true, 27 | props: { 28 | type: "color", 29 | }, 30 | default: ["#f7158b", "#02d1fd", "#1f014b"], 31 | }, 32 | Background: { 33 | type: "color", 34 | default: "#ffffff", 35 | }, 36 | // See browser compatibility issues here 37 | // https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode 38 | "Mix blend mode": { 39 | type: "select", 40 | options: [ 41 | "normal", 42 | "multiply", 43 | "screen", 44 | "overlay", 45 | "darken", 46 | "lighten", 47 | "color-dodge", 48 | "color-burn", 49 | "hard-light", 50 | "soft-light", 51 | "difference", 52 | "exclusion", 53 | "hue", 54 | "saturation", 55 | "color", 56 | "luminosity", 57 | "plus-darker", 58 | "plus-lighter", 59 | ], 60 | }, 61 | Seed: { 62 | type: "number", 63 | min: 1, 64 | max: 100, 65 | default: 1, 66 | }, 67 | }; 68 | 69 | export function renderSVG(qr, params) { 70 | const unit = params["Density"]; 71 | 72 | const rand = getSeededRand(params["Seed"]); 73 | const rangeStr = (min, max) => (rand() * (max - min) + min).toFixed(2); 74 | const rowLen = qr.version * 4 + 17; 75 | const margin = params["Margin"] * unit; 76 | const colors = params["Foreground"]; 77 | const bg = params["Background"]; 78 | 79 | const size = rowLen * unit + 2 * margin; 80 | let svg = ``; 81 | 82 | const center = (rowLen * unit) / 2; 83 | svg += ``; 84 | 85 | const dotRadius = 1; 86 | const dotSpace = 2.2; 87 | const maxRadius = Math.sqrt((unit * unit * rowLen * rowLen) / 2); 88 | for (let r = 0.1; r < maxRadius; r += dotSpace) { 89 | const angleInc = dotSpace / r; 90 | for (let theta = 0; theta < 2 * Math.PI - angleInc / 2; theta += angleInc) { 91 | const x = r * Math.cos(theta); 92 | const y = r * Math.sin(theta); 93 | const qx = Math.floor((x + center) / unit); 94 | const qy = Math.floor((y + center) / unit); 95 | if (qx >= 0 && qx < rowLen && qy >= 0 && qy < rowLen) { 96 | if (qr.matrix[qy * rowLen + qx] & Module.ON) { 97 | const rad = 98 | qr.matrix[qy * rowLen + qx] & Module.FINDER 99 | ? params["Finder clarity"] 100 | : dotRadius; 101 | svg += ``; 102 | } 103 | } 104 | } 105 | } 106 | svg += ``; 107 | svg += ``; 108 | 109 | svg += ``; 110 | colors.forEach( 111 | (color) => 112 | (svg += ``) 113 | ); 114 | svg += ``; 115 | 116 | svg += ``; 117 | return svg; 118 | } 119 | -------------------------------------------------------------------------------- /presets/Drawing.js: -------------------------------------------------------------------------------- 1 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 2 | import rough from "https://esm.sh/roughjs"; 3 | 4 | export const paramsSchema = { 5 | Margin: { 6 | type: "number", 7 | min: 0, 8 | max: 10, 9 | default: 2, 10 | }, 11 | "Fill style": { 12 | type: "select", 13 | options: [ 14 | "Hachure", 15 | "Solid", 16 | "Zigzag", 17 | "Cross-hatch", 18 | "Dots", 19 | "Dashed", 20 | "Zigzag-line", 21 | ], 22 | default: "Zigzag", 23 | }, 24 | Fill: { 25 | type: "color", 26 | default: "#ffffff", 27 | }, 28 | "Fill weight": { 29 | type: "number", 30 | min: 0, 31 | max: 10, 32 | default: 2, 33 | }, 34 | "Fill gap": { 35 | type: "number", 36 | min: 1, 37 | max: 10, 38 | default: 4, 39 | }, 40 | Stroke: { 41 | type: "color", 42 | default: "#ffffff", 43 | }, 44 | "Stroke width": { 45 | type: "number", 46 | min: 0, 47 | max: 10, 48 | default: 1, 49 | }, 50 | Invert: { 51 | type: "boolean", 52 | default: true, 53 | }, 54 | Roughness: { 55 | type: "number", 56 | min: 0, 57 | max: 10, 58 | default: 1, 59 | }, 60 | Bowing: { 61 | type: "number", 62 | min: 0, 63 | max: 10, 64 | default: 1, 65 | }, 66 | Background: { 67 | type: "color", 68 | default: "#222222", 69 | }, 70 | Seed: { 71 | type: "number", 72 | min: 1, 73 | max: 100, 74 | default: 1, 75 | }, 76 | }; 77 | 78 | const domMock = { 79 | ownerDocument: { 80 | createElementNS: (_ns, tagName) => { 81 | const children = []; 82 | const attributes = {}; 83 | return { 84 | tagName, 85 | attributes, 86 | setAttribute: (key, value) => (attributes[key] = value), 87 | appendChild: (node) => children.push(node), 88 | children, 89 | }; 90 | }, 91 | }, 92 | }; 93 | 94 | export function renderSVG(qr, params) { 95 | const roughSVG = rough.svg(domMock, { 96 | options: { 97 | roughness: params["Roughness"], 98 | bowing: params["Bowing"], 99 | fillStyle: params["Fill style"].toLowerCase(), 100 | fillWeight: params["Fill weight"], 101 | fill: params["Fill weight"] === 0 ? "none" : params["Fill"], 102 | strokeWidth: params["Stroke width"], 103 | stroke: params["Stroke width"] === 0 ? "none" : params["Stroke"], 104 | hachureGap: params["Fill gap"], 105 | seed: params["Seed"], 106 | fixedDecimalPlaceDigits: 2, 107 | }, 108 | }); 109 | 110 | let matrix = qr.matrix; 111 | let rowLen = qr.version * 4 + 17; 112 | 113 | if (params["Invert"]) { 114 | rowLen += 2; 115 | matrix = []; 116 | for (let y = 0; y < rowLen; y++) { 117 | for (let x = 0; x < rowLen; x++) { 118 | if (x === 0 || y === 0 || x === rowLen - 1 || y === rowLen - 1) { 119 | matrix.push(0); 120 | } else { 121 | matrix.push(qr.matrix[(y - 1) * (rowLen - 2) + x - 1]); 122 | } 123 | } 124 | } 125 | } 126 | 127 | const visited = new Uint16Array(rowLen * rowLen); 128 | const unit = 10; 129 | const margin = params["Margin"] * unit; 130 | const size = rowLen * unit + 2 * margin; 131 | 132 | let svg = ``; 133 | svg += ``; 134 | 135 | const xMax = rowLen - 1; 136 | const yMax = rowLen - 1; 137 | 138 | let baseX; 139 | let baseY; 140 | 141 | const on = params["Invert"] 142 | ? (x, y) => (matrix[y * rowLen + x] & Module.ON) === 0 143 | : (x, y) => (matrix[y * rowLen + x] & Module.ON) !== 0; 144 | 145 | function go(x, y, dx, dy, path, cw) { 146 | visited[y * rowLen + x] = path; 147 | let concave = false; 148 | 149 | let nx = x + dx; 150 | let ny = y + dy; 151 | while (nx >= 0 && nx <= xMax && ny >= 0 && ny <= yMax) { 152 | const next = on(nx, ny); 153 | const cx = nx + dy; 154 | const cy = ny - dx; 155 | const diag = cx >= 0 && cx <= xMax && cy >= 0 && cy <= yMax && on(cx, cy); 156 | if (!next || diag) { 157 | concave = next && diag; 158 | break; 159 | } 160 | visited[ny * rowLen + nx] = path; 161 | nx += dx; 162 | ny += dy; 163 | } 164 | 165 | if (nx - dx === baseX && ny - dy === baseY) { 166 | if ((cw && dy === -1) || (!cw && dx === -1)) { 167 | paths[path] += "z"; 168 | return; 169 | } 170 | } 171 | 172 | if (dx !== 0) { 173 | paths[path] += `h${(nx - x) * unit}`; 174 | } else { 175 | paths[path] += `v${(ny - y) * unit}`; 176 | } 177 | 178 | if (concave) { 179 | go(nx + dy, ny - dx, dy, -dx, path, cw); 180 | } else { 181 | go(nx - dx, ny - dy, -dy, dx, path, cw); 182 | } 183 | } 184 | 185 | const stack = []; 186 | for (let x = 0; x < rowLen; x++) { 187 | if (!on(x, 0)) stack.push([x, 0]); 188 | } 189 | for (let y = 1; y < yMax; y++) { 190 | if (!on(0, y)) stack.push([0, y]); 191 | if (!on(xMax, y)) stack.push([xMax, y]); 192 | } 193 | for (let x = 0; x < rowLen; x++) { 194 | if (!on(x, yMax)) stack.push([x, yMax]); 195 | } 196 | 197 | // visit all whitespace connected to edges 198 | function dfsOff() { 199 | while (stack.length > 0) { 200 | const [x, y] = stack.pop(); 201 | if (visited[y * rowLen + x]) continue; 202 | visited[y * rowLen + x] = 1; 203 | for (let dy = -1; dy <= 1; dy++) { 204 | for (let dx = -1; dx <= 1; dx++) { 205 | if (dy === 0 && dx === 0) continue; 206 | let nx = x + dx; 207 | let ny = y + dy; 208 | if (nx < 0 || nx > xMax || ny < 0 || ny > yMax) continue; 209 | if (on(nx, ny)) continue; 210 | stack.push([nx, ny]); 211 | } 212 | } 213 | } 214 | } 215 | dfsOff(); 216 | 217 | const paths = [""]; 218 | for (let y = 0; y < rowLen; y++) { 219 | for (let x = 0; x < rowLen; x++) { 220 | if (visited[y * rowLen + x]) continue; 221 | 222 | if (!on(x, y)) { 223 | const path = visited[y * rowLen + x - 1]; 224 | paths[path] += `M${x * unit},${y * unit}`; 225 | 226 | baseY = y - 1; 227 | baseX = x; 228 | go(x - 1, y, 0, 1, path, false); 229 | stack.push([x, y]); 230 | dfsOff(); 231 | continue; 232 | } 233 | 234 | if (y > 0 && on(x, y - 1) && visited[(y - 1) * rowLen + x]) { 235 | visited[y * rowLen + x] = visited[(y - 1) * rowLen + x]; 236 | continue; 237 | } 238 | if (x > 0 && on(x - 1, y) && visited[y * rowLen + x - 1]) { 239 | visited[y * rowLen + x] = visited[y * rowLen + x - 1]; 240 | continue; 241 | } 242 | 243 | paths.push(`M${x * unit},${y * unit}`); 244 | baseY = y; 245 | baseX = x; 246 | go(x, y, 1, 0, paths.length - 1, true); 247 | } 248 | } 249 | 250 | function domToString(node) { 251 | const attrs = Object.entries(node.attributes) 252 | .map(([key, value]) => `${key}="${value}"`) 253 | .join(" "); 254 | svg += `<${node.tagName} ${attrs}>`; 255 | node.children.forEach(domToString); 256 | svg += ``; 257 | } 258 | 259 | paths.forEach((path, i) => { 260 | if (i === 0) return; 261 | const g = roughSVG.path(path); 262 | domToString(g); 263 | }); 264 | 265 | svg += ``; 266 | return svg; 267 | } 268 | -------------------------------------------------------------------------------- /presets/Glass.js: -------------------------------------------------------------------------------- 1 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Margin: { 5 | type: "number", 6 | min: 0, 7 | max: 10, 8 | default: 2, 9 | }, 10 | Foreground: { 11 | type: "color", 12 | default: "#000000", 13 | }, 14 | Background: { 15 | type: "color", 16 | default: "#fcb9ff", 17 | }, 18 | Shapes: { 19 | type: "number", 20 | min: 1, 21 | max: 400, 22 | default: 100, 23 | }, 24 | "Stroke width": { 25 | type: "number", 26 | min: 0, 27 | max: 4, 28 | step: 0.1, 29 | default: 1.5, 30 | }, 31 | "Shape gap": { 32 | type: "number", 33 | min: -4, 34 | max: 4, 35 | default: 0, 36 | }, 37 | "Shape opacity": { 38 | type: "number", 39 | min: 0, 40 | max: 1, 41 | step: 0.1, 42 | default: 0.3, 43 | }, 44 | "QR layer": { 45 | type: "select", 46 | options: ["Above", "Below"], 47 | }, 48 | Seed: { 49 | type: "number", 50 | min: 1, 51 | max: 100, 52 | default: 1, 53 | }, 54 | }; 55 | 56 | export function renderSVG(qr, params) { 57 | const rand = getSeededRand(params["Seed"]); 58 | const margin = params["Margin"]; 59 | 60 | const unit = 4; 61 | const offset = params["Shape gap"] / 2; 62 | const thin = unit - params["Shape gap"]; 63 | const rowLen = qr.version * 4 + 17 + 2 * margin; 64 | const size = rowLen * unit; 65 | 66 | let svg = ``; 67 | svg += ``; 68 | 69 | function getRGB() { 70 | const r = Math.floor(rand() * 255); 71 | const g = Math.floor(rand() * 255); 72 | const b = Math.floor(rand() * 255); 73 | return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; 74 | } 75 | 76 | const groups = params["Shapes"]; 77 | 78 | let groupLayer = ""; 79 | 80 | const group = Array.from({ length: rowLen * rowLen }).fill(0); 81 | const visited = Array.from({ length: rowLen * rowLen }).fill(false); 82 | 83 | const queue = []; 84 | while (queue.length < groups) { 85 | const x = Math.floor(rand() * rowLen); 86 | const y = Math.floor(rand() * rowLen); 87 | if (queue.some((seed) => seed.x === x && seed.y === y)) { 88 | continue; 89 | } 90 | queue.push([x, y]); 91 | group[y * rowLen + x] = queue.length; 92 | } 93 | while (queue.length) { 94 | const [x, y] = queue.shift(); 95 | const id = group[y * rowLen + x]; 96 | if (x > 0 && !group[y * rowLen + x - 1]) { 97 | queue.push([x - 1, y]); 98 | group[y * rowLen + x - 1] = id; 99 | } 100 | if (y > 0 && !group[(y - 1) * rowLen + x]) { 101 | queue.push([x, y - 1]); 102 | group[(y - 1) * rowLen + x] = id; 103 | } 104 | if (x < rowLen - 1 && !group[y * rowLen + x + 1]) { 105 | queue.push([x + 1, y]); 106 | group[y * rowLen + x + 1] = id; 107 | } 108 | if (y < rowLen - 1 && !group[(y + 1) * rowLen + x]) { 109 | queue.push([x, y + 1]); 110 | group[(y + 1) * rowLen + x] = id; 111 | } 112 | } 113 | 114 | const xMax = rowLen - 1; 115 | const yMax = rowLen - 1; 116 | 117 | let baseX; 118 | let baseY; 119 | 120 | const on = (x, y, id) => group[y * rowLen + x] === id; 121 | 122 | function go(x, y, dx, dy, id) { 123 | visited[y * rowLen + x] = true; 124 | let concave = false; 125 | 126 | let nx = x + dx; 127 | let ny = y + dy; 128 | while (nx >= 0 && nx <= xMax && ny >= 0 && ny <= yMax) { 129 | const next = on(nx, ny, id); 130 | const cx = nx + dy; 131 | const cy = ny - dx; 132 | const diag = 133 | cx >= 0 && cx <= xMax && cy >= 0 && cy <= yMax && on(cx, cy, id); 134 | if (!next || diag) { 135 | concave = next && diag; 136 | break; 137 | } 138 | visited[ny * rowLen + nx] = true; 139 | nx += dx; 140 | ny += dy; 141 | } 142 | 143 | if (nx - dx === baseX && ny - dy === baseY) { 144 | if (dy === -1) { 145 | groupLayer += "z"; 146 | return; 147 | } 148 | } 149 | 150 | if (concave) { 151 | if (dx) { 152 | groupLayer += `h${(nx - x) * unit}v${-dx * 2 * offset}`; 153 | } else { 154 | groupLayer += `v${(ny - y) * unit}h${dy * 2 * offset}`; 155 | } 156 | go(nx + dy, ny - dx, dy, -dx, id); 157 | } else { 158 | if (dx) { 159 | groupLayer += `h${(nx - x - dx) * unit + dx * thin}`; 160 | } else { 161 | groupLayer += `v${(ny - y - dy) * unit + dy * thin}`; 162 | } 163 | go(nx - dx, ny - dy, -dy, dx, id); 164 | } 165 | } 166 | 167 | const matrix = Array.from({ length: rowLen * rowLen }).fill(0); 168 | const qrWidth = qr.version * 4 + 17; 169 | 170 | for (let y = 0; y < rowLen; y++) { 171 | for (let x = 0; x < rowLen; x++) { 172 | if ( 173 | y >= margin && 174 | y < rowLen - margin && 175 | x >= margin && 176 | x < rowLen - margin 177 | ) { 178 | matrix[y * rowLen + x] = 179 | qr.matrix[(y - margin) * qrWidth + (x - margin)]; 180 | } 181 | } 182 | } 183 | 184 | let qrLayer = ``; 219 | } 220 | } 221 | if (params["QR layer"] === "Below") svg += qrLayer + `"/>`; 222 | svg += groupLayer; 223 | if (params["QR layer"] === "Above") svg += qrLayer + `"/>`; 224 | svg += ``; 225 | 226 | return svg; 227 | } 228 | -------------------------------------------------------------------------------- /presets/Halftone.js: -------------------------------------------------------------------------------- 1 | import { Module } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Image: { 5 | type: "file", 6 | }, 7 | "Image scale": { 8 | type: "number", 9 | min: 0, 10 | max: 1, 11 | step: 0.01, 12 | default: 1, 13 | }, 14 | Contrast: { 15 | type: "number", 16 | min: 0, 17 | max: 10, 18 | step: 0.1, 19 | default: 1, 20 | }, 21 | Brightness: { 22 | type: "number", 23 | min: 0, 24 | max: 5, 25 | step: 0.1, 26 | default: 1.8, 27 | }, 28 | "QR background": { 29 | type: "boolean", 30 | }, 31 | "Alignment pattern": { 32 | type: "boolean", 33 | default: true, 34 | }, 35 | "Timing pattern": { 36 | type: "boolean", 37 | }, 38 | Margin: { 39 | type: "number", 40 | min: 0, 41 | max: 10, 42 | default: 2, 43 | }, 44 | Foreground: { 45 | type: "color", 46 | default: "#000000", 47 | }, 48 | Background: { 49 | type: "color", 50 | default: "#ffffff", 51 | }, 52 | }; 53 | 54 | export async function renderCanvas(qr, params, canvas) { 55 | const unit = 3; 56 | const pixel = 1; 57 | 58 | const rowLen = qr.version * 4 + 17; 59 | const margin = params["Margin"]; 60 | const fg = params["Foreground"]; 61 | const bg = params["Background"]; 62 | const alignment = params["Alignment pattern"]; 63 | const timing = params["Timing pattern"]; 64 | let file = params["Image"]; 65 | if (file == null) { 66 | file = await fetch( 67 | "https://upload.wikimedia.org/wikipedia/commons/1/14/The_Widow_%28Boston_Public_Library%29_%28cropped%29.jpg" 68 | ).then((res) => res.blob()); 69 | } 70 | const image = await createImageBitmap(file); 71 | 72 | const pixelWidth = rowLen + 2 * margin; 73 | const canvasSize = pixelWidth * unit; 74 | const ctx = canvas.getContext("2d"); 75 | ctx.canvas.width = canvasSize; 76 | ctx.canvas.height = canvasSize; 77 | 78 | ctx.fillStyle = bg; 79 | ctx.fillRect(0, 0, canvasSize, canvasSize); 80 | if (params["QR background"]) { 81 | ctx.fillStyle = fg; 82 | for (let y = 0; y < rowLen; y++) { 83 | for (let x = 0; x < rowLen; x++) { 84 | const module = qr.matrix[y * rowLen + x]; 85 | if (module & Module.ON) { 86 | const px = x + margin; 87 | const py = y + margin; 88 | ctx.fillRect(px * unit, py * unit, unit, unit); 89 | } 90 | } 91 | } 92 | } 93 | 94 | ctx.filter = `brightness(${params["Brightness"]}) contrast(${params["Contrast"]})`; 95 | const imgScale = params["Image scale"]; 96 | const imgSize = Math.floor(imgScale * canvasSize); 97 | const imgOffset = Math.floor((canvasSize - imgSize) / 2); 98 | ctx.drawImage(image, imgOffset, imgOffset, imgSize, imgSize); 99 | ctx.filter = "none"; 100 | 101 | const imageData = ctx.getImageData(0, 0, canvasSize, canvasSize); 102 | const data = imageData.data; 103 | 104 | for (let y = imgOffset; y < imgOffset + imgSize; y++) { 105 | for (let x = imgOffset; x < imgOffset + imgSize; x++) { 106 | const i = (y * canvasSize + x) * 4; 107 | 108 | if (data[i + 3] === 0) continue; 109 | // Convert to grayscale and normalize to 0-255 110 | const oldPixel = 111 | (data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114) | 0; 112 | 113 | let newPixel; 114 | if (oldPixel < 128) { 115 | newPixel = 0; 116 | ctx.fillStyle = fg; 117 | } else { 118 | newPixel = 255; 119 | ctx.fillStyle = bg; 120 | } 121 | ctx.fillRect(x * pixel, y * pixel, pixel, pixel); 122 | 123 | data[i] = data[i + 1] = data[i + 2] = newPixel; 124 | const error = oldPixel - newPixel; 125 | 126 | // Distribute error to neighboring pixels 127 | if (x < canvasSize - 1) { 128 | data[i + 4] += (error * 7) / 16; 129 | } 130 | if (y < canvasSize - 1) { 131 | if (x > 0) { 132 | data[i + canvasSize * 4 - 4] += (error * 3) / 16; 133 | } 134 | data[i + canvasSize * 4] += (error * 5) / 16; 135 | if (x < canvasSize - 1) { 136 | data[i + canvasSize * 4 + 4] += (error * 1) / 16; 137 | } 138 | } 139 | } 140 | } 141 | 142 | const dataOffset = (unit - pixel) / 2; 143 | 144 | for (let y = 0; y < rowLen; y++) { 145 | for (let x = 0; x < rowLen; x++) { 146 | const module = qr.matrix[y * rowLen + x]; 147 | if (module & Module.ON) { 148 | ctx.fillStyle = fg; 149 | } else { 150 | ctx.fillStyle = bg; 151 | } 152 | 153 | const px = x + margin; 154 | const py = y + margin; 155 | 156 | if ( 157 | module & Module.FINDER || 158 | (alignment && module & Module.ALIGNMENT) || 159 | (timing && module & Module.TIMING) 160 | ) { 161 | ctx.fillRect(px * unit, py * unit, unit, unit); 162 | } else { 163 | ctx.fillRect( 164 | px * unit + dataOffset, 165 | py * unit + dataOffset, 166 | pixel, 167 | pixel 168 | ); 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /presets/Layers.js: -------------------------------------------------------------------------------- 1 | import { Module } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Margin: { 5 | type: "number", 6 | min: 0, 7 | max: 10, 8 | step: 0.1, 9 | default: 2, 10 | }, 11 | Background: { 12 | type: "color", 13 | default: "#163157", 14 | }, 15 | // See browser compatibility issues here 16 | // https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode 17 | "Mix blend mode": { 18 | type: "select", 19 | options: [ 20 | "normal", 21 | "multiply", 22 | "screen", 23 | "overlay", 24 | "darken", 25 | "lighten", 26 | "color-dodge", 27 | "color-burn", 28 | "hard-light", 29 | "soft-light", 30 | "difference", 31 | "exclusion", 32 | "hue", 33 | "saturation", 34 | "color", 35 | "luminosity", 36 | "plus-darker", 37 | "plus-lighter", 38 | ], 39 | default: "difference", 40 | }, 41 | Foreground: { 42 | type: "array", 43 | resizable: true, 44 | props: { 45 | type: "color", 46 | }, 47 | default: ["#e80004", "#000000", "#ca70cf", "#000000", "#ffffff"], 48 | }, 49 | "Offset x": { 50 | type: "array", 51 | resizable: true, 52 | props: { 53 | type: "number", 54 | min: -1, 55 | step: 0.1, 56 | max: 1, 57 | }, 58 | default: [0.6, 0.4, 0.2, 0, -0.2], 59 | }, 60 | "Offset y": { 61 | type: "array", 62 | resizable: true, 63 | props: { 64 | type: "number", 65 | min: -1, 66 | step: 0.1, 67 | max: 1, 68 | }, 69 | default: [0.6, 0.4, 0.2, 0, -0.2], 70 | }, 71 | }; 72 | 73 | export function renderSVG(qr, params) { 74 | const rowLen = qr.version * 4 + 17; 75 | const margin = params["Margin"]; 76 | const colors = params["Foreground"]; 77 | const offsetX = params["Offset x"]; 78 | const offsetY = params["Offset y"]; 79 | const bg = params["Background"]; 80 | 81 | const size = rowLen + 2 * margin; 82 | let svg = ``; 83 | svg += ``; 84 | 85 | svg += ``; 86 | svg += ``; 96 | svg += ``; 97 | 98 | svg += ``; 99 | colors.forEach((color, i) => { 100 | svg += ``; 101 | }); 102 | svg += ``; 103 | svg += ``; 104 | return svg; 105 | } 106 | -------------------------------------------------------------------------------- /presets/Minimal.js: -------------------------------------------------------------------------------- 1 | import { Module } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Margin: { 5 | type: "number", 6 | min: 0, 7 | max: 10, 8 | default: 2, 9 | }, 10 | "Data pixel size": { 11 | type: "number", 12 | min: 1, 13 | max: 20, 14 | default: 3, 15 | }, 16 | Background: { 17 | type: "boolean", 18 | }, 19 | }; 20 | 21 | export function renderSVG(qr, params) { 22 | const rowLen = qr.version * 4 + 17; 23 | const unit = 10; 24 | const dataSize = params["Data pixel size"]; 25 | const margin = params["Margin"] * unit; 26 | 27 | const fg = "#000"; 28 | const bg = "#fff"; 29 | 30 | const size = rowLen * unit + 2 * margin; 31 | let svg = ``; 32 | if (params["Background"]) { 33 | svg += ``; 34 | } 35 | svg += ``; 67 | 68 | return svg; 69 | } 70 | -------------------------------------------------------------------------------- /presets/Mondrian.js: -------------------------------------------------------------------------------- 1 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Margin: { 5 | type: "number", 6 | min: 0, 7 | max: 10, 8 | default: 2, 9 | }, 10 | Foreground: { 11 | type: "array", 12 | props: { 13 | type: "color", 14 | }, 15 | resizable: true, 16 | default: ["#860909", "#0e21a0", "#95800f"], 17 | }, 18 | Background: { 19 | type: "color", 20 | default: "#ffffff", 21 | }, 22 | Lines: { 23 | type: "color", 24 | default: "#000000", 25 | }, 26 | "Line thickness": { 27 | type: "number", 28 | min: -10, 29 | max: 10, 30 | default: 2, 31 | }, 32 | Seed: { 33 | type: "number", 34 | min: 1, 35 | max: 100, 36 | default: 1, 37 | }, 38 | }; 39 | 40 | export function renderSVG(qr, params) { 41 | const rand = getSeededRand(params["Seed"]); 42 | const margin = params["Margin"]; 43 | 44 | const unit = 20; 45 | const rowLen = qr.version * 4 + 17 + 2 * margin; 46 | const size = rowLen * unit; 47 | 48 | const gap = params["Line thickness"]; 49 | const offset = gap / 2; 50 | 51 | let svg = ``; 52 | svg += ``; 53 | 54 | let lightLayer = ` ` (svg += layer + `"/>`)); 123 | svg += lightLayer + `"/>`; 124 | svg += ``; 125 | 126 | return svg; 127 | } 128 | -------------------------------------------------------------------------------- /presets/Neon.js: -------------------------------------------------------------------------------- 1 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Foreground: { 5 | type: "array", 6 | props: { 7 | type: "color", 8 | }, 9 | resizable: true, 10 | default: ["#fb51dd", "#f2cffa", "#aefdfd", "#54a9fe"], 11 | }, 12 | Background: { 13 | type: "color", 14 | default: "#101529", 15 | }, 16 | Margin: { 17 | type: "number", 18 | min: 0, 19 | max: 10, 20 | default: 4, 21 | }, 22 | "Quiet zone": { 23 | type: "select", 24 | options: ["Minimal", "Full"], 25 | }, 26 | Invert: { 27 | type: "boolean", 28 | }, 29 | "Line thickness": { 30 | type: "number", 31 | min: 1, 32 | max: 4, 33 | default: 2, 34 | }, 35 | "Finder thickness": { 36 | type: "number", 37 | min: 1, 38 | max: 4, 39 | default: 4, 40 | }, 41 | "Glow strength": { 42 | type: "number", 43 | min: 0, 44 | max: 4, 45 | step: 0.1, 46 | default: 2, 47 | }, 48 | Seed: { 49 | type: "number", 50 | min: 1, 51 | max: 100, 52 | default: 1, 53 | }, 54 | }; 55 | 56 | export function renderSVG(qr, params) { 57 | const rand = getSeededRand(params["Seed"]); 58 | const margin = params["Margin"]; 59 | const colors = params["Foreground"]; 60 | const bg = params["Background"]; 61 | 62 | const qrRowLen = qr.version * 4 + 17; 63 | const rowLen = qrRowLen + 2 * margin; 64 | 65 | const newMatrix = Array(rowLen * rowLen).fill(0); 66 | const visited = new Uint16Array(rowLen * rowLen); 67 | 68 | // Copy qr to matrix with margin and randomly set pixels in margin 69 | for (let y = 0; y < margin - 1; y++) { 70 | for (let x = 0; x < rowLen; x++) { 71 | if (rand() > 0.5) newMatrix[y * rowLen + x] = Module.ON; 72 | } 73 | } 74 | for (let y = margin - 1; y < margin + qrRowLen + 1; y++) { 75 | for (let x = 0; x < margin - 1; x++) { 76 | if (rand() > 0.5) newMatrix[y * rowLen + x] = Module.ON; 77 | } 78 | if (y >= margin && y < margin + qrRowLen) { 79 | for (let x = margin; x < rowLen - margin; x++) { 80 | newMatrix[y * rowLen + x] = 81 | qr.matrix[(y - margin) * qrRowLen + x - margin]; 82 | } 83 | } 84 | for (let x = margin + qrRowLen + 1; x < rowLen; x++) { 85 | if (rand() > 0.5) newMatrix[y * rowLen + x] = Module.ON; 86 | } 87 | } 88 | for (let y = margin + qrRowLen + 1; y < rowLen; y++) { 89 | for (let x = 0; x < rowLen; x++) { 90 | if (rand() > 0.5) newMatrix[y * rowLen + x] = Module.ON; 91 | } 92 | } 93 | if (params["Quiet zone"] === "Minimal") { 94 | for (let x = margin + 8; x < rowLen - margin - 8; x++) { 95 | if (rand() > 0.5) newMatrix[(margin - 1) * rowLen + x] = Module.ON; 96 | } 97 | for (let y = margin + 8; y < rowLen - margin; y++) { 98 | if (y < rowLen - margin - 8) { 99 | if (rand() > 0.5) newMatrix[y * rowLen + margin - 1] = Module.ON; 100 | } 101 | if (rand() > 0.5) newMatrix[y * rowLen + rowLen - margin] = Module.ON; 102 | } 103 | for (let x = margin + 8; x < rowLen - margin + 1; x++) { 104 | if (rand() > 0.5) newMatrix[(rowLen - margin) * rowLen + x] = Module.ON; 105 | } 106 | } 107 | 108 | const unit = 4; 109 | let thin = params["Line thickness"]; 110 | let offset = (unit - thin) / 2; 111 | const size = rowLen * unit - 2 * offset; 112 | 113 | let svg = ``; 114 | 115 | svg += ` 116 | 117 | 118 | `; 119 | 120 | svg += ``; 121 | 122 | const xMax = rowLen - 1; 123 | const yMax = rowLen - 1; 124 | 125 | let baseX; 126 | let baseY; 127 | 128 | const on = params["Invert"] 129 | ? (x, y) => (newMatrix[y * rowLen + x] & Module.ON) === 0 130 | : (x, y) => (newMatrix[y * rowLen + x] & Module.ON) !== 0; 131 | 132 | function go(x, y, dx, dy, path, cw) { 133 | visited[y * rowLen + x] = path; 134 | let concave = false; 135 | 136 | let nx = x + dx; 137 | let ny = y + dy; 138 | while (nx >= 0 && nx <= xMax && ny >= 0 && ny <= yMax) { 139 | const next = on(nx, ny); 140 | const cx = nx + dy; 141 | const cy = ny - dx; 142 | const diag = cx >= 0 && cx <= xMax && cy >= 0 && cy <= yMax && on(cx, cy); 143 | if (!next || diag) { 144 | concave = next && diag; 145 | break; 146 | } 147 | visited[ny * rowLen + nx] = path; 148 | nx += dx; 149 | ny += dy; 150 | } 151 | 152 | if (nx - dx === baseX && ny - dy === baseY) { 153 | if ((cw && dy === -1) || (!cw && dx === -1)) { 154 | paths[path] += "z"; 155 | return; 156 | } 157 | } 158 | 159 | if (concave) { 160 | if (dx) { 161 | paths[path] += `h${(nx - x) * unit}v${-dx * 2 * offset}`; 162 | } else { 163 | paths[path] += `v${(ny - y) * unit}h${dy * 2 * offset}`; 164 | } 165 | go(nx + dy, ny - dx, dy, -dx, path, cw); 166 | } else { 167 | if (dx) { 168 | paths[path] += `h${(nx - x - dx) * unit + dx * thin}`; 169 | } else { 170 | paths[path] += `v${(ny - y - dy) * unit + dy * thin}`; 171 | } 172 | go(nx - dx, ny - dy, -dy, dx, path, cw); 173 | } 174 | } 175 | 176 | const stack = []; 177 | for (let x = 0; x < rowLen; x++) { 178 | if (!on(x, 0)) stack.push([x, 0]); 179 | } 180 | for (let y = 1; y < yMax; y++) { 181 | if (!on(0, y)) stack.push([0, y]); 182 | if (!on(xMax, y)) stack.push([xMax, y]); 183 | } 184 | for (let x = 0; x < rowLen; x++) { 185 | if (!on(x, yMax)) stack.push([x, yMax]); 186 | } 187 | 188 | // visit all whitespace connected to edges 189 | function dfsOff() { 190 | while (stack.length > 0) { 191 | const [x, y] = stack.pop(); 192 | if (visited[y * rowLen + x]) continue; 193 | visited[y * rowLen + x] = 1; 194 | for (let dy = -1; dy <= 1; dy++) { 195 | for (let dx = -1; dx <= 1; dx++) { 196 | if (dy === 0 && dx === 0) continue; 197 | let nx = x + dx; 198 | let ny = y + dy; 199 | if (nx < 0 || nx > xMax || ny < 0 || ny > yMax) continue; 200 | if (on(nx, ny)) continue; 201 | stack.push([nx, ny]); 202 | } 203 | } 204 | } 205 | } 206 | dfsOff(); 207 | 208 | const paths = [""]; 209 | for (let y = 0; y < rowLen; y++) { 210 | for (let x = 0; x < rowLen; x++) { 211 | if (visited[y * rowLen + x]) continue; 212 | 213 | if (newMatrix[y * rowLen + x] & Module.FINDER) { 214 | thin = params["Finder thickness"]; 215 | offset = (unit - thin) / 2; 216 | } else { 217 | thin = params["Line thickness"]; 218 | offset = (unit - thin) / 2; 219 | } 220 | 221 | if (!on(x, y)) { 222 | const path = visited[y * rowLen + x - 1]; 223 | paths[path] += 224 | `M${x * unit - offset},${y * unit - offset}v${2 * offset}`; 225 | 226 | baseY = y - 1; 227 | baseX = x; 228 | go(x - 1, y, 0, 1, path, false); 229 | stack.push([x, y]); 230 | dfsOff(); 231 | continue; 232 | } 233 | 234 | if (y > 0 && on(x, y - 1) && visited[(y - 1) * rowLen + x]) { 235 | visited[y * rowLen + x] = visited[(y - 1) * rowLen + x]; 236 | continue; 237 | } 238 | if (x > 0 && on(x - 1, y) && visited[y * rowLen + x - 1]) { 239 | visited[y * rowLen + x] = visited[y * rowLen + x - 1]; 240 | continue; 241 | } 242 | 243 | const color = colors[Math.floor(rand() * colors.length)]; 244 | paths.push( 245 | ``; 257 | }); 258 | svg += ``; 259 | return svg; 260 | } 261 | -------------------------------------------------------------------------------- /presets/Quantum.js: -------------------------------------------------------------------------------- 1 | // Based on QRBTF's A1P style 2 | // https://github.com/CPunisher/react-qrbtf/blob/master/src/components/QRNormal.tsx 3 | import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js"; 4 | 5 | export const paramsSchema = { 6 | Margin: { 7 | type: "number", 8 | min: 0, 9 | max: 10, 10 | step: 0.1, 11 | default: 2, 12 | }, 13 | Background: { 14 | type: "color", 15 | default: "#ffffff", 16 | }, 17 | Foreground: { 18 | type: "color", 19 | default: "#000000", 20 | }, 21 | "Finder pattern": { 22 | type: "select", 23 | options: ["Atom", "Planet"], 24 | }, 25 | Particles: { 26 | type: "boolean", 27 | default: true, 28 | }, 29 | Seed: { 30 | type: "number", 31 | min: 1, 32 | max: 100, 33 | default: 1, 34 | }, 35 | }; 36 | 37 | export function renderSVG(qr, params) { 38 | const rand = getSeededRand(params["Seed"]); 39 | const range = (min, max) => 40 | Math.trunc(100 * (rand() * (max - min) + min)) / 100; 41 | 42 | const rowLen = qr.version * 4 + 17; 43 | const margin = params["Margin"]; 44 | const bg = params["Background"]; 45 | const fg = params["Foreground"]; 46 | 47 | const size = rowLen + 2 * margin; 48 | let svg = ``; 49 | svg += ``; 50 | 51 | for (const [x, y] of [ 52 | [0, 0], 53 | [rowLen - 7, 0], 54 | [0, rowLen - 7], 55 | ]) { 56 | svg += ``; 57 | svg += ``; 58 | svg += ``; 59 | svg += ``; 60 | svg += ``; 61 | svg += ``; 62 | 63 | switch (params["Finder pattern"]) { 64 | case "Atom": 65 | let r1 = 0.98; 66 | let r2 = 1.5; 67 | 68 | const a = 0.87 * r2; 69 | const b = 0.5 * r2; 70 | svg += ``; 83 | } 84 | 85 | let linesLayer = ``; 87 | 88 | function on(x, y) { 89 | return (qr.matrix[y * rowLen + x] & Module.ON) !== 0; 90 | } 91 | 92 | const visitArray = Array.from({ length: rowLen * rowLen }).fill(false); 93 | 94 | function visited(x, y) { 95 | return visitArray[y * rowLen + x]; 96 | } 97 | function visitCenter(x, y) { 98 | visitArray[y * rowLen + x] = true; 99 | dotsLayer += ``; 100 | } 101 | function visit(x, y) { 102 | visitArray[y * rowLen + x] = true; 103 | dotsLayer += ``; 104 | } 105 | 106 | for (let y = 0; y < rowLen; y++) { 107 | for (let x = 0; x < rowLen; x++) { 108 | const module = qr.matrix[y * rowLen + x]; 109 | if (module & Module.FINDER) continue; 110 | 111 | if (params["Particles"] && y < rowLen - 2 && x < rowLen - 2) { 112 | let xCross = false; 113 | let tCross = false; 114 | 115 | let a = range(-10, 10); 116 | if ( 117 | on(x, y) && 118 | !visited(x, y) && 119 | on(x + 2, y) && 120 | !visited(x + 2, y) && 121 | on(x + 1, y + 1) && 122 | !visited(x + 1, y + 1) && 123 | on(x, y + 2) && 124 | !visited(x, y + 2) && 125 | on(x + 2, y + 2) && 126 | !visited(x + 2, y + 2) 127 | ) { 128 | linesLayer += `M${x + 0.5},${y + 0.5}a1.4,.35 ${45 + a},0,1 2,2a1.4,.35 ${45 + a},0,1 -2,-2`; 129 | linesLayer += `M${x + 2.5},${y + 0.5}a.35,1.4 ${45 + a},0,1 -2,2a.35,1.4 ${45 + a},0,1 2,-2`; 130 | xCross = true; 131 | } 132 | if ( 133 | on(x + 1, y) && 134 | !visited(x + 1, y) && 135 | on(x, y + 1) && 136 | !visited(x, y + 1) && 137 | on(x + 1, y + 1) && 138 | !visited(x + 1, y + 1) && 139 | on(x + 2, y + 1) && 140 | !visited(x + 2, y + 1) && 141 | on(x + 1, y + 2) && 142 | !visited(x + 1, y + 2) 143 | ) { 144 | linesLayer += `M${x},${y + 1.55}a1,.35 ${a},0,1 3,0a1,.35 ${a},0,1 -3,0`; 145 | linesLayer += `M${x + 1.5},${y}a.35,1 ${a},0,1 0,3a.35,1 ${a},0,1 0,-3`; 146 | tCross = true; 147 | } 148 | if (xCross) { 149 | visit(x, y); 150 | visit(x + 2, y); 151 | visitCenter(x + 1, y + 1); 152 | visit(x, y + 2); 153 | visit(x + 2, y + 2); 154 | } 155 | if (tCross) { 156 | visit(x + 1, y); 157 | visit(x, y + 1); 158 | visitCenter(x + 1, y + 1); 159 | visit(x + 2, y + 1); 160 | visit(x + 1, y + 2); 161 | } 162 | } 163 | 164 | if (!visited(x, y) && on(x, y)) { 165 | dotsLayer += ``; 166 | } 167 | } 168 | } 169 | 170 | linesLayer += `"/>`; 171 | svg += linesLayer; 172 | dotsLayer += ``; 173 | svg += dotsLayer; 174 | svg += ``; 175 | 176 | return svg; 177 | } 178 | -------------------------------------------------------------------------------- /presets/Tile.js: -------------------------------------------------------------------------------- 1 | import { Module } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Margin: { 5 | type: "number", 6 | min: 0, 7 | max: 20, 8 | default: 10, 9 | }, 10 | "Inner square": { 11 | type: "number", 12 | min: 0, 13 | max: 10, 14 | default: 2, 15 | }, 16 | "Outer square": { 17 | type: "number", 18 | min: 0, 19 | max: 10, 20 | default: 6, 21 | }, 22 | Foreground: { 23 | type: "color", 24 | default: "#000000", 25 | }, 26 | Background: { 27 | type: "color", 28 | default: "#ffffff", 29 | }, 30 | Grout: { 31 | type: "color", 32 | default: "#b3b8fd", 33 | }, 34 | }; 35 | 36 | export function renderSVG(qr, params) { 37 | const unit = 16; 38 | const gap = 2; 39 | const offset = gap / 2; 40 | 41 | const margin = params["Margin"]; 42 | const qrRowLen = qr.version * 4 + 17; 43 | const rowLen = qrRowLen + 2 * margin; 44 | 45 | const fg = params["Foreground"]; 46 | const bg = params["Background"]; 47 | const grout = params["Grout"]; 48 | 49 | const newMatrix = Array.from({ length: rowLen * rowLen }).fill(0); 50 | 51 | const start = margin; 52 | const end = rowLen - 1 - margin; 53 | const inner = params["Inner square"]; 54 | const outer = params["Outer square"]; 55 | for (let y = 0; y < rowLen; y++) { 56 | for (let x = 0; x < rowLen; x++) { 57 | // outer square 58 | if (y === start - outer && x >= start - outer && x <= end + outer) { 59 | newMatrix[y * rowLen + x] = Module.ON; 60 | } else if ( 61 | (x === start - outer || x === end + outer) && 62 | y >= start - outer + 1 && 63 | y <= end + outer - 1 64 | ) { 65 | newMatrix[y * rowLen + x] = Module.ON; 66 | newMatrix[y * rowLen + x] = Module.ON; 67 | } else if (y === end + outer && x >= start - outer && x <= end + outer) { 68 | newMatrix[y * rowLen + x] = Module.ON; 69 | } 70 | // inner square 71 | else if (y === start - inner && x >= start - inner && x <= end + inner) { 72 | newMatrix[y * rowLen + x] = Module.ON; 73 | } else if ( 74 | (x === start - inner || x === end + inner) && 75 | y >= start - inner + 1 && 76 | y <= end + inner - 1 77 | ) { 78 | newMatrix[y * rowLen + x] = Module.ON; 79 | newMatrix[y * rowLen + x] = Module.ON; 80 | } else if (y === end + inner && x >= start - inner && x <= end + inner) { 81 | newMatrix[y * rowLen + x] = Module.ON; 82 | } 83 | // qr code w/ quiet zone 84 | else if ( 85 | y >= margin - inner && 86 | y < rowLen - margin + inner && 87 | x >= margin - inner && 88 | x < rowLen - margin + inner 89 | ) { 90 | if ( 91 | y >= margin && 92 | y < rowLen - margin && 93 | x >= margin && 94 | x < rowLen - margin 95 | ) { 96 | newMatrix[y * rowLen + x] = 97 | qr.matrix[(y - margin) * qrRowLen + x - margin]; 98 | } 99 | } 100 | // between squares 101 | else if ( 102 | y > start - outer && 103 | y < end + outer && 104 | x > start - outer && 105 | x < end + outer 106 | ) { 107 | if ((x + y) & 1) { 108 | newMatrix[y * rowLen + x] = Module.ON; 109 | } 110 | // outside squares 111 | } else { 112 | if (x % 4 && y % 4) { 113 | if ((x % 8 < 4 && y % 8 < 4) || (x % 8 > 4 && y % 8 > 4)) { 114 | newMatrix[y * rowLen + x] = Module.ON; 115 | } 116 | } else { 117 | newMatrix[y * rowLen + x] = Module.ON; 118 | } 119 | } 120 | } 121 | } 122 | 123 | const size = rowLen * unit; 124 | let svg = ``; 125 | svg += ``; 126 | svg += ``; 158 | svg += layer + `"/>`; 159 | svg += ``; 160 | 161 | return svg; 162 | } 163 | -------------------------------------------------------------------------------- /presets/Tutorial.js: -------------------------------------------------------------------------------- 1 | import { Module } from "https://qrframe.kylezhe.ng/utils.js"; 2 | 3 | export const paramsSchema = { 4 | Margin: { 5 | type: "number", 6 | min: 0, 7 | max: 10, 8 | step: 0.1, 9 | default: 2, 10 | }, 11 | Foreground: { 12 | type: "color", 13 | default: "#000000", 14 | }, 15 | Background: { 16 | type: "color", 17 | default: "#ffffff", 18 | }, 19 | }; 20 | 21 | export function renderSVG(qr, params) { 22 | const rowLen = qr.version * 4 + 17; 23 | const margin = params["Margin"]; 24 | const fg = params["Foreground"]; 25 | const bg = params["Background"]; 26 | 27 | const size = rowLen + 2 * margin; 28 | let svg = ``; 29 | svg += ``; 30 | svg += ``; 41 | 42 | return svg; 43 | } 44 | 45 | // export function renderCanvas(qr, params, canvas) { 46 | // const rowLen = qr.version * 4 + 17; 47 | // const margin = params["Margin"]; 48 | // const fg = params["Foreground"]; 49 | // const bg = params["Background"]; 50 | // const unit = 10; 51 | // const size = (rowLen + 2 * margin) * unit; 52 | // canvas.width = size; 53 | // canvas.height = size; 54 | 55 | // const ctx = canvas.getContext("2d"); 56 | // ctx.fillStyle = bg; 57 | // ctx.fillRect(0, 0, size, size); 58 | 59 | // ctx.fillStyle = fg; 60 | // for (let y = 0; y < rowLen; y++) { 61 | // for (let x = 0; x < rowLen; x++) { 62 | // const module = qr.matrix[y * rowLen + x]; 63 | // if (module & Module.ON) { 64 | // ctx.fillRect((x + margin) * unit, (y + margin) * unit, unit, unit); 65 | // } 66 | // } 67 | // } 68 | // } 69 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/previewWorker.js: -------------------------------------------------------------------------------- 1 | let prevToken = { canceled: true }; 2 | 3 | onmessage = async ({ data: { type, url, qr, params, timeoutId } }) => { 4 | prevToken.canceled = true; 5 | const token = { canceled: false }; 6 | prevToken = token; 7 | 8 | try { 9 | switch (type) { 10 | case "svg": { 11 | const { renderSVG } = await import(url); 12 | const svg = await renderSVG(qr, params); 13 | if (token.canceled) { 14 | return postMessage({ type: "canceled", timeoutId }); 15 | } 16 | 17 | postMessage({ type, svg, timeoutId }); 18 | break; 19 | } 20 | case "canvas": { 21 | const { renderCanvas } = await import(url); 22 | const canvas = new OffscreenCanvas(0, 0); 23 | await renderCanvas(qr, params, canvas); 24 | if (token.canceled) { 25 | return postMessage({ type: "canceled", timeoutId }); 26 | } 27 | 28 | const bitmap = canvas.transferToImageBitmap(); 29 | postMessage({ type, bitmap, timeoutId }, [bitmap]); 30 | break; 31 | } 32 | } 33 | } catch (error) { 34 | postMessage({ 35 | type: "error", 36 | error, 37 | timeoutId, 38 | }); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /public/thumbnailWorker.js: -------------------------------------------------------------------------------- 1 | // pre-generated unscannable thumbnail qrcode 2 | const PREVIEW_OUTPUTQR = { 3 | text: "thumbnail", 4 | // prettier-ignore 5 | matrix: [ 5,5,5,5,5,5,5,0,33,2,3,2,3,0,5,5,5,5,5,5,5,5,4,4,4,4,4,5,0,33,3,2,2,3,0,5,4,4,4,4,4,5,5,4,133,133,133,4,5,0,33,2,3,2,2,0,5,4,133,133,133,4,5,5,4,133,133,133,4,5,0,33,3,2,3,2,0,5,4,133,133,133,4,5,5,4,133,133,133,4,5,0,33,2,2,2,2,0,5,4,133,133,133,4,5,5,4,4,4,4,4,5,0,32,2,2,3,2,0,5,4,4,4,4,4,5,5,5,5,5,5,5,5,0,17,16,17,16,17,0,5,5,5,5,5,5,5,0,0,0,0,0,0,0,0,33,3,3,3,2,0,0,0,0,0,0,0,0,32,33,33,32,33,32,17,33,32,2,3,3,3,160,161,160,161,161,161,161,161,2,3,2,2,2,2,16,2,3,2,3,3,2,2,3,2,3,2,3,2,3,3,3,2,3,3,2,17,3,3,2,3,3,2,3,2,3,2,2,2,3,2,3,3,3,2,2,2,16,2,2,3,2,2,3,3,2,3,2,3,2,2,2,2,2,3,3,2,2,17,2,3,3,3,3,3,2,3,2,2,3,3,3,2,0,0,0,0,0,0,0,0,161,2,3,3,3,3,3,2,3,3,2,3,3,5,5,5,5,5,5,5,0,161,3,2,3,2,2,2,3,2,3,2,3,2,5,4,4,4,4,4,5,0,160,2,3,2,2,2,2,2,3,3,2,3,2,5,4,133,133,133,4,5,0,161,2,2,2,3,2,2,3,3,2,3,2,3,5,4,133,133,133,4,5,0,160,3,2,3,2,3,3,2,3,3,3,2,2,5,4,133,133,133,4,5,0,161,2,2,2,2,2,3,3,2,3,2,2,2,5,4,4,4,4,4,5,0,161,3,2,3,2,3,3,2,3,3,2,2,2,5,5,5,5,5,5,5,0,160,3,2,2,2,2,2,2,2,2,3,3,2], 6 | version: 1, 7 | ecl: 2, //ECL.Quartile 8 | mode: 2, // Mode.Byte 9 | mask: 4, // Mask.M4 10 | }; 11 | 12 | onmessage = async ({ data: { type,url,params,timeoutId } }) => { 13 | try { 14 | switch (type) { 15 | case "svg": { 16 | const { renderSVG } = await import(url); 17 | const svg = await renderSVG(PREVIEW_OUTPUTQR,params); 18 | 19 | postMessage({ type,svg,timeoutId }); 20 | break; 21 | } 22 | case "canvas": { 23 | const { renderCanvas } = await import(url); 24 | const canvas = new OffscreenCanvas(0,0); 25 | await renderCanvas(PREVIEW_OUTPUTQR,params,canvas); 26 | 27 | const bitmap = canvas.transferToImageBitmap(); 28 | postMessage({ type,bitmap,timeoutId },[bitmap]); 29 | break; 30 | } 31 | } 32 | } catch (error) { 33 | postMessage({ 34 | type: "error", 35 | error, 36 | timeoutId, 37 | }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /public/utils.js: -------------------------------------------------------------------------------- 1 | export const Module = Object.freeze({ 2 | ON: 1 << 0, 3 | DATA: 1 << 1, 4 | FINDER: 1 << 2, 5 | ALIGNMENT: 1 << 3, 6 | TIMING: 1 << 4, 7 | FORMAT: 1 << 5, 8 | VERSION: 1 << 6, 9 | MODIFIER: 1 << 7, 10 | }); 11 | 12 | function splitmix32(a) { 13 | return function () { 14 | a |= 0; 15 | a = (a + 0x9e3779b9) | 0; 16 | let t = a ^ (a >>> 16); 17 | t = Math.imul(t, 0x21f0aaad); 18 | t = t ^ (t >>> 15); 19 | t = Math.imul(t, 0x735a2d97); 20 | return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296; 21 | }; 22 | } 23 | export { splitmix32 as getSeededRand } 24 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | .checkerboard { 2 | background-image: repeating-conic-gradient(#ddd 0% 25%, #aaa 25% 50%); 3 | background-position: 50%; 4 | background-size: 10% 10%; 5 | } 6 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import "@unocss/reset/tailwind.css"; 2 | import "virtual:uno.css"; 3 | import "./app.css"; 4 | 5 | import { Router } from "@solidjs/router"; 6 | import { FileRoutes } from "@solidjs/start/router"; 7 | import { Suspense } from "solid-js"; 8 | 9 | export default function App() { 10 | return ( 11 | <> 12 | {props.children}}> 13 | 14 | 15 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/coloris.css: -------------------------------------------------------------------------------- 1 | .clr-picker { 2 | display: none; 3 | flex-wrap: wrap; 4 | position: absolute; 5 | width: 200px; 6 | z-index: 1000; 7 | border-radius: 10px; 8 | background-color: #fff; 9 | justify-content: flex-end; 10 | direction: ltr; 11 | box-shadow: 0 0 5px rgba(0,0,0,.05), 0 5px 20px rgba(0,0,0,.1); 12 | -moz-user-select: none; 13 | -webkit-user-select: none; 14 | user-select: none; 15 | } 16 | 17 | .clr-picker.clr-open, 18 | .clr-picker[data-inline="true"] { 19 | display: flex; 20 | } 21 | 22 | .clr-picker[data-inline="true"] { 23 | position: relative; 24 | } 25 | 26 | .clr-gradient { 27 | position: relative; 28 | width: 100%; 29 | height: 100px; 30 | margin-bottom: 15px; 31 | border-radius: 3px 3px 0 0; 32 | background-image: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentColor); 33 | cursor: pointer; 34 | } 35 | 36 | .clr-marker { 37 | position: absolute; 38 | width: 12px; 39 | height: 12px; 40 | margin: -6px 0 0 -6px; 41 | border: 1px solid #fff; 42 | border-radius: 50%; 43 | background-color: currentColor; 44 | cursor: pointer; 45 | } 46 | 47 | .clr-picker input[type="range"]::-webkit-slider-runnable-track { 48 | width: 100%; 49 | height: 16px; 50 | } 51 | 52 | .clr-picker input[type="range"]::-webkit-slider-thumb { 53 | width: 16px; 54 | height: 16px; 55 | -webkit-appearance: none; 56 | } 57 | 58 | .clr-picker input[type="range"]::-moz-range-track { 59 | width: 100%; 60 | height: 16px; 61 | border: 0; 62 | } 63 | 64 | .clr-picker input[type="range"]::-moz-range-thumb { 65 | width: 16px; 66 | height: 16px; 67 | border: 0; 68 | } 69 | 70 | .clr-hue { 71 | background-image: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); 72 | } 73 | 74 | .clr-hue, 75 | .clr-alpha { 76 | position: relative; 77 | width: calc(100% - 40px); 78 | height: 8px; 79 | margin: 5px 20px; 80 | border-radius: 4px; 81 | } 82 | 83 | .clr-alpha span { 84 | display: block; 85 | height: 100%; 86 | width: 100%; 87 | border-radius: inherit; 88 | background-image: linear-gradient(90deg, rgba(0,0,0,0), currentColor); 89 | } 90 | 91 | .clr-hue input[type="range"], 92 | .clr-alpha input[type="range"] { 93 | position: absolute; 94 | width: calc(100% + 32px); 95 | height: 16px; 96 | left: -16px; 97 | top: -4px; 98 | margin: 0; 99 | background-color: transparent; 100 | opacity: 0; 101 | cursor: pointer; 102 | appearance: none; 103 | -webkit-appearance: none; 104 | } 105 | 106 | .clr-hue div, 107 | .clr-alpha div { 108 | position: absolute; 109 | width: 16px; 110 | height: 16px; 111 | left: 0; 112 | top: 50%; 113 | margin-left: -8px; 114 | transform: translateY(-50%); 115 | border: 2px solid #fff; 116 | border-radius: 50%; 117 | background-color: currentColor; 118 | box-shadow: 0 0 1px #888; 119 | pointer-events: none; 120 | } 121 | 122 | .clr-alpha div:before { 123 | content: ''; 124 | position: absolute; 125 | height: 100%; 126 | width: 100%; 127 | left: 0; 128 | top: 0; 129 | border-radius: 50%; 130 | background-color: currentColor; 131 | } 132 | 133 | .clr-format { 134 | display: none; 135 | order: 1; 136 | width: calc(100% - 40px); 137 | margin: 0 20px 20px; 138 | } 139 | 140 | .clr-segmented { 141 | display: flex; 142 | position: relative; 143 | width: 100%; 144 | margin: 0; 145 | padding: 0; 146 | border: 1px solid #ddd; 147 | border-radius: 15px; 148 | box-sizing: border-box; 149 | color: #999; 150 | font-size: 12px; 151 | } 152 | 153 | .clr-segmented input, 154 | .clr-segmented legend { 155 | position: absolute; 156 | width: 100%; 157 | height: 100%; 158 | margin: 0; 159 | padding: 0; 160 | border: 0; 161 | left: 0; 162 | top: 0; 163 | opacity: 0; 164 | pointer-events: none; 165 | } 166 | 167 | .clr-segmented label { 168 | flex-grow: 1; 169 | margin: 0; 170 | padding: 4px 0; 171 | font-size: inherit; 172 | font-weight: normal; 173 | line-height: initial; 174 | text-align: center; 175 | cursor: pointer; 176 | } 177 | 178 | .clr-segmented label:first-of-type { 179 | border-radius: 10px 0 0 10px; 180 | } 181 | 182 | .clr-segmented label:last-of-type { 183 | border-radius: 0 10px 10px 0; 184 | } 185 | 186 | .clr-segmented input:checked + label { 187 | color: #fff; 188 | background-color: #666; 189 | } 190 | 191 | input.clr-color { 192 | order: 1; 193 | width: calc(100% - 80px); 194 | height: 32px; 195 | margin: 15px 20px 20px auto; 196 | padding: 0 10px; 197 | border: 1px solid #ddd; 198 | border-radius: 16px; 199 | color: #444; 200 | background-color: #fff; 201 | font-family: sans-serif; 202 | font-size: 14px; 203 | text-align: center; 204 | box-shadow: none; 205 | } 206 | 207 | input.clr-color:focus { 208 | outline: none; 209 | border: 1px solid #1e90ff; 210 | } 211 | 212 | .clr-close, 213 | .clr-clear { 214 | display: none; 215 | order: 2; 216 | height: 24px; 217 | margin: 0 20px 20px; 218 | padding: 0 20px; 219 | border: 0; 220 | border-radius: 12px; 221 | color: #fff; 222 | background-color: #666; 223 | font-family: inherit; 224 | font-size: 12px; 225 | font-weight: 400; 226 | cursor: pointer; 227 | } 228 | 229 | .clr-close { 230 | display: block; 231 | margin: 0 20px 20px auto; 232 | } 233 | 234 | .clr-preview { 235 | position: relative; 236 | width: 32px; 237 | height: 32px; 238 | margin: 15px 0 20px 20px; 239 | border-radius: 50%; 240 | overflow: hidden; 241 | } 242 | 243 | .clr-preview:before, 244 | .clr-preview:after { 245 | content: ''; 246 | position: absolute; 247 | height: 100%; 248 | width: 100%; 249 | left: 0; 250 | top: 0; 251 | border: 1px solid #fff; 252 | border-radius: 50%; 253 | } 254 | 255 | .clr-preview:after { 256 | border: 0; 257 | background-color: currentColor; 258 | box-shadow: inset 0 0 0 1px rgba(0,0,0,.1); 259 | } 260 | 261 | .clr-preview button { 262 | position: absolute; 263 | width: 100%; 264 | height: 100%; 265 | z-index: 1; 266 | margin: 0; 267 | padding: 0; 268 | border: 0; 269 | border-radius: 50%; 270 | outline-offset: -2px; 271 | background-color: transparent; 272 | text-indent: -9999px; 273 | cursor: pointer; 274 | overflow: hidden; 275 | } 276 | 277 | .clr-marker, 278 | .clr-hue div, 279 | .clr-alpha div, 280 | .clr-color { 281 | box-sizing: border-box; 282 | } 283 | 284 | .clr-field { 285 | display: inline-block; 286 | position: relative; 287 | color: transparent; 288 | width: 100%; 289 | } 290 | 291 | .clr-field button { 292 | position: absolute; 293 | width: 24px; 294 | height: 24px; 295 | left: 6px; 296 | border-radius: 4px; 297 | top: 50%; 298 | transform: translateY(-50%); 299 | margin: 0; 300 | padding: 0; 301 | border: 0; 302 | color: inherit; 303 | text-indent: -1000px; 304 | white-space: nowrap; 305 | overflow: hidden; 306 | pointer-events: none; 307 | background: none; 308 | } 309 | 310 | .clr-field button:after { 311 | content: ''; 312 | display: block; 313 | position: absolute; 314 | width: 100%; 315 | height: 100%; 316 | left: 0; 317 | top: 0; 318 | border-radius: inherit; 319 | background-color: currentColor; 320 | box-shadow: inset 0 0 1px rgba(0,0,0,.5); 321 | } 322 | 323 | .clr-alpha, 324 | .clr-alpha div, 325 | .clr-preview:before, 326 | .clr-field button { 327 | background-image: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); 328 | background-position: 0 0, 4px 4px; 329 | background-size: 8px 8px; 330 | } 331 | 332 | .clr-marker:focus { 333 | outline: none; 334 | } 335 | 336 | .clr-keyboard-nav .clr-marker:focus, 337 | .clr-keyboard-nav .clr-hue input:focus + div, 338 | .clr-keyboard-nav .clr-alpha input:focus + div, 339 | .clr-keyboard-nav .clr-segmented input:focus + label { 340 | outline: none; 341 | box-shadow: 0 0 0 2px #1e90ff, 0 0 2px 2px #fff; 342 | } 343 | 344 | .clr-picker[data-minimal="true"] { 345 | padding-top: 16px; 346 | } 347 | 348 | .clr-picker[data-minimal="true"] .clr-gradient, 349 | .clr-picker[data-minimal="true"] .clr-hue, 350 | .clr-picker[data-minimal="true"] .clr-alpha, 351 | .clr-picker[data-minimal="true"] .clr-color, 352 | .clr-picker[data-minimal="true"] .clr-preview { 353 | display: none; 354 | } 355 | 356 | /** Dark theme **/ 357 | 358 | .clr-dark { 359 | background-color: #444; 360 | } 361 | 362 | .clr-dark .clr-segmented { 363 | border-color: #777; 364 | } 365 | 366 | .clr-dark input.clr-color { 367 | color: #fff; 368 | border-color: #777; 369 | background-color: #555; 370 | } 371 | 372 | .clr-dark input.clr-color:focus { 373 | border-color: #1e90ff; 374 | } 375 | 376 | .clr-dark .clr-preview:after { 377 | box-shadow: inset 0 0 0 1px rgba(255,255,255,.5); 378 | } 379 | 380 | .clr-dark .clr-alpha, 381 | .clr-dark .clr-alpha div, 382 | .clr-dark .clr-preview:before { 383 | background-image: repeating-linear-gradient(45deg, #666 25%, transparent 25%, transparent 75%, #888 75%, #888), repeating-linear-gradient(45deg, #888 25%, #444 25%, #444 75%, #888 75%, #888); 384 | } 385 | 386 | /** Large theme **/ 387 | 388 | .clr-picker.clr-large { 389 | width: 275px; 390 | } 391 | 392 | .clr-large .clr-gradient { 393 | height: 150px; 394 | } 395 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@kobalte/core/button"; 2 | import { type JSX, type Ref } from "solid-js"; 3 | 4 | type Props = { 5 | class?: string; 6 | onClick?: () => void; 7 | onMouseDown?: () => void; 8 | children: JSX.Element; 9 | title?: string; 10 | disabled?: boolean; 11 | ref?: Ref; 12 | }; 13 | export function FlatButton(props: Props) { 14 | return ( 15 | 29 | ); 30 | } 31 | 32 | export function FillButton(props: Props) { 33 | return ( 34 | 47 | ); 48 | } 49 | 50 | type ToggleProps = { 51 | value: boolean; 52 | onClick?: () => void; 53 | onMouseDown?: () => void; 54 | children: JSX.Element; 55 | }; 56 | 57 | export function ToggleButton(props: ToggleProps) { 58 | return ( 59 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import { ToggleGroup } from "@kobalte/core/toggle-group"; 2 | import { type JSX } from "solid-js"; 3 | 4 | type Props = { 5 | value?: T; 6 | setValue: (v: T) => void; 7 | children: JSX.Element; 8 | }; 9 | 10 | export function ButtonGroup(props: Props) { 11 | return ( 12 | v && props.setValue(v as T)} 16 | > 17 | {props.children} 18 | 19 | ); 20 | } 21 | 22 | type ItemProps = { 23 | value: string; 24 | ariaLabel?: string; 25 | title?: boolean; 26 | children: JSX.Element; 27 | }; 28 | 29 | export function ButtonGroupItem(props: ItemProps) { 30 | return ( 31 | 37 | {props.children} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsible as KCollapsible } from "@kobalte/core/collapsible"; 2 | import ChevronDown from "lucide-solid/icons/chevron-down"; 3 | import type { JSX } from "solid-js"; 4 | 5 | type Props = { 6 | trigger: string; 7 | children: JSX.Element; 8 | defaultOpen?: boolean 9 | }; 10 | export function Collapsible(props: Props) { 11 | return ( 12 | 13 | 14 | {props.trigger} 15 | 16 | 17 | {/* Content cannot have y padding b/c it breaks animation */} 18 | 19 | {props.children} 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ColorInput.tsx: -------------------------------------------------------------------------------- 1 | import { createEffect } from "solid-js"; 2 | import Coloris from "@melloware/coloris"; 3 | import "../coloris.css"; 4 | 5 | Coloris.init(); 6 | 7 | type Props = { 8 | value: string; 9 | setValue: (c: string) => void; 10 | }; 11 | 12 | export default function ColorInput(props: Props) { 13 | let input: HTMLInputElement; 14 | createEffect(() => { 15 | if (props.value !== input.value) { 16 | input.value = props.value; 17 | input.dispatchEvent(new Event("input", { bubbles: true })); 18 | } 19 | }); 20 | 21 | return ( 22 |
23 | { 27 | props.setValue(e.target.value); 28 | }} 29 | ref={(ref) => { 30 | ref.value = props.value; 31 | Coloris({ 32 | el: ref, 33 | alpha: true, 34 | formatToggle: true, 35 | focusInput: false, 36 | theme: "large", 37 | themeMode: "auto", 38 | }); 39 | input = ref; 40 | }} 41 | /> 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import { ContextMenu } from "@kobalte/core/context-menu"; 2 | import { type JSX } from "solid-js"; 3 | 4 | type Props = { 5 | children: JSX.Element; 6 | onRename: () => void; 7 | onDelete: () => void; 8 | disabled?: boolean; 9 | }; 10 | 11 | export function ContextMenuProvider(props: Props) { 12 | return ( 13 | 14 | {props.children} 15 | 16 | 22 | 31 | Rename 32 | 33 | 42 | Delete 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | 50 | type TriggerProps = { 51 | children: JSX.Element; 52 | }; 53 | 54 | export function ContentMenuTrigger(props: TriggerProps) { 55 | return {props.children}; 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog } from "@kobalte/core/dialog"; 2 | import X from "lucide-solid/icons/x"; 3 | import { type JSX } from "solid-js"; 4 | import { FlatButton } from "./Button"; 5 | 6 | type Props = { 7 | title: string; 8 | children: (close: () => void) => JSX.Element; 9 | onOpenAutoFocus?: (event: Event) => void; 10 | open: boolean; 11 | setOpen: (b: boolean) => void; 12 | }; 13 | export function ControlledDialog(props: Props) { 14 | return ( 15 | 16 | 17 | 18 |
19 | 23 |
24 | 25 | {props.title} 26 | 27 | 28 | 29 | 30 |
31 | {props.children(() => props.setOpen(false))} 32 |
33 |
34 |
35 |
36 | ); 37 | } 38 | 39 | type ButtonProps = { 40 | title: string; 41 | children: JSX.Element; 42 | onClick?: () => void; 43 | }; 44 | // Dialog.Trigger toggles the open state, so 45 | // it cannot be used with onClick that modifies the open state 46 | export function DialogButton(props: ButtonProps) { 47 | return ( 48 | 49 | {props.children} 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/ErrorToasts.tsx: -------------------------------------------------------------------------------- 1 | import { Toast, toaster } from "@kobalte/core/toast"; 2 | import X from "lucide-solid/icons/x"; 3 | import { type JSX } from "solid-js"; 4 | 5 | export const clearToasts = () => toaster.clear(); 6 | 7 | export const toastError = (title: JSX.Element, description: JSX.Element) => { 8 | toaster.clear(); 9 | toaster.show((props) => ( 10 | 15 |
16 | {title} 17 | 18 | 19 | 20 |
21 | 22 | {description} 23 | 24 |
25 | )); 26 | }; 27 | 28 | export function ErrorToasts() { 29 | return ( 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ImageInput.tsx: -------------------------------------------------------------------------------- 1 | import X from "lucide-solid/icons/x"; 2 | import { Show } from "solid-js"; 3 | import { FlatButton } from "./Button"; 4 | 5 | type Props = { 6 | value: File | null; 7 | setValue: (f: File | null) => void; 8 | accept?: string; 9 | }; 10 | 11 | export function FileInput(props: Props) { 12 | let input: HTMLInputElement; 13 | return ( 14 |
15 | { 21 | // @ts-expect-error onChange is called so files exists 22 | props.setValue(e.target.files[0]); 23 | }} 24 | /> 25 | 26 | { 29 | input.value = ""; 30 | props.setValue(null); 31 | }} 32 | > 33 | 34 | 35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/NumberInput.tsx: -------------------------------------------------------------------------------- 1 | import { NumberField } from "@kobalte/core/number-field"; 2 | import { Slider } from "@kobalte/core/slider"; 3 | import ChevronUp from "lucide-solid/icons/chevron-up"; 4 | import ChevronDown from "lucide-solid/icons/chevron-down"; 5 | import { createSignal } from "solid-js"; 6 | 7 | type Props = { 8 | min: number; 9 | max: number; 10 | step?: number; 11 | value: number; 12 | setValue: (v: number) => void; 13 | }; 14 | 15 | export function NumberInput(props: Props) { 16 | const [rawValue, setRawValue] = createSignal(props.value); 17 | 18 | const safeSetValue = (value: number) => { 19 | setRawValue(value); 20 | if (value < props.min || value > props.max || isNaN(value)) { 21 | return; 22 | } 23 | 24 | if (value !== props.value) { 25 | props.setValue(value); 26 | } 27 | }; 28 | 29 | const [focused, setFocused] = createSignal(false); 30 | 31 | return ( 32 |
33 | props.setValue(values[0])} 40 | > 41 | 42 |
43 |
44 | 45 | 46 | 47 | 48 |
49 |
50 |
51 | 59 | setFocused(true)} 62 | onBlur={() => setFocused(false)} 63 | /> 64 | 68 | 69 | 70 | 74 | 75 | 76 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Select.tsx: -------------------------------------------------------------------------------- 1 | import { Select as KSelect } from "@kobalte/core/select"; 2 | import ChevronsUpDown from "lucide-solid/icons/chevrons-up-down"; 3 | import { createSignal } from "solid-js"; 4 | import { FilledDot } from "./svg"; 5 | 6 | type Props = { 7 | options: string[]; 8 | value: string; 9 | setValue: (v: string) => void; 10 | }; 11 | 12 | export function Select(props: Props) { 13 | // props.value changes on focus/highlight for quick preview 14 | // but the old value should be restored on esc/unfocus 15 | const [retainedValue, setRetainedValue] = createSignal(props.value); 16 | return ( 17 | { 20 | if (v != null) { 21 | props.setValue(v); 22 | setRetainedValue(v); 23 | } 24 | }} 25 | onOpenChange={(isOpen) => { 26 | if (!isOpen && props.value !== retainedValue()) { 27 | props.setValue(retainedValue()); 28 | } 29 | }} 30 | onKeyDown={(e) => { 31 | const index = props.options.indexOf(props.value); 32 | switch (e.key) { 33 | case "ArrowDown": 34 | props.setValue( 35 | props.options[Math.min(index + 1, props.options.length - 1)] 36 | ); 37 | break; 38 | case "ArrowUp": 39 | props.setValue(props.options[Math.max(index - 1, 0)]); 40 | break; 41 | case "Home": 42 | props.setValue(props.options[0]); 43 | break; 44 | case "End": 45 | props.setValue(props.options[props.options.length - 1]); 46 | break; 47 | } 48 | }} 49 | class="min-w-40" 50 | options={props.options} 51 | gutter={4} 52 | itemComponent={(itemProps) => ( 53 | { 57 | props.setValue(itemProps.item.key); 58 | }} 59 | > 60 | {itemProps.item.rawValue} 61 | 62 | 63 | 64 | 65 | )} 66 | > 67 | 68 | 69 | {(state) => state.selectedOption() as string} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ); 82 | } -------------------------------------------------------------------------------- /src/components/SplitButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@kobalte/core/button"; 2 | import { NumberField } from "@kobalte/core/number-field"; 3 | import { Popover } from "@kobalte/core/popover"; 4 | import ChevronDown from "lucide-solid/icons/chevron-down"; 5 | import Download from "lucide-solid/icons/download"; 6 | import { createSignal } from "solid-js"; 7 | import { FillButton } from "./Button"; 8 | 9 | type Props = { 10 | onPng: (resizeWidth, resizeHeight) => void; 11 | onSvg: () => void; 12 | disabled: boolean; 13 | }; 14 | export function SplitButton(props: Props) { 15 | const [customWidth, setCustomWidth] = createSignal(2000); 16 | const [customHeight, setCustomHeight] = createSignal(2000); 17 | 18 | const onPng = (resizeWidth, resizeHeight) => { 19 | props.onPng(resizeWidth, resizeHeight); 20 | setOpen(false); 21 | }; 22 | const onSvg = () => { 23 | props.onSvg(); 24 | setOpen(false); 25 | }; 26 | const [open, setOpen] = createSignal(false); 27 | return ( 28 |
29 | 38 | 39 | 44 | 48 | 49 | 50 | 51 |
52 | 64 |
65 |
Alternate file type
66 | 67 | SVG 68 | 69 |
70 |
71 |
Custom size
72 |
73 | 79 | 85 |
86 | onPng(customWidth(), customHeight())} 89 | > 90 | Download custom 91 | 92 |
93 |
94 |
95 |
96 |
97 | ); 98 | } 99 | 100 | type NumberProps = { 101 | min: number; 102 | max: number; 103 | value: number; 104 | setValue: (v: number) => void; 105 | }; 106 | 107 | function MenuNumberInput(props: NumberProps) { 108 | const [rawValue, setRawValue] = createSignal(props.value); 109 | 110 | const safeSetValue = (value) => { 111 | setRawValue(value); 112 | if ( 113 | value < props.min || 114 | value > props.max || 115 | isNaN(value) || 116 | !Number.isInteger(value) 117 | ) { 118 | return; 119 | } 120 | 121 | if (value !== props.value) { 122 | props.setValue(value); 123 | } 124 | }; 125 | 126 | const [focused, setFocused] = createSignal(false); 127 | return ( 128 | 136 | 141 | 142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /src/components/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { Switch as KSwitch } from "@kobalte/core/switch"; 2 | 3 | type Props = { 4 | value: boolean; 5 | setValue: (b: boolean) => void; 6 | label?: string; 7 | }; 8 | 9 | export function Switch(props: Props) { 10 | return ( 11 | 16 | {props.label && ( 17 | {props.label} 18 | )} 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import { debounce } from "~/lib/util"; 2 | 3 | type TextareaProps = { 4 | setValue: (i: string) => void; 5 | placeholder?: string; 6 | onFocus: () => void; 7 | onBlur: () => void; 8 | ref: HTMLTextAreaElement | ((el: HTMLTextAreaElement) => void); 9 | }; 10 | 11 | /** No `value` prop b/c textarea cannot be controlled */ 12 | export function TextareaInput(props: TextareaProps) { 13 | const onInput = debounce(props.setValue, 300); 14 | return ( 15 | 24 | ); 25 | } 26 | 27 | type InputProps = { 28 | placeholder?: string; 29 | defaultValue: string; 30 | onInput: (s: string) => void; 31 | ref?: HTMLInputElement; 32 | class?: string; 33 | onKeyDown?: (e: KeyboardEvent) => void; 34 | }; 35 | 36 | /** UNCONTROLLED */ 37 | export function TextInput(props: InputProps) { 38 | return ( 39 | props.onInput(e.target.value)} 48 | onKeyDown={props.onKeyDown} 49 | /> 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/editor/AllowPasteDialog.tsx: -------------------------------------------------------------------------------- 1 | import { AlertDialog } from "@kobalte/core/alert-dialog"; 2 | import X from "lucide-solid/icons/x"; 3 | import { FillButton, FlatButton } from "../Button"; 4 | 5 | type Props = { 6 | open: boolean; 7 | setClosed: () => void; 8 | onAllow: () => void; 9 | }; 10 | 11 | export function AllowPasteDialog(props: Props) { 12 | return ( 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | Allow pasting code? 21 | 22 | 23 | 24 | 25 |
26 |
27 |

Using code you don't understand could be dangerous.

28 |

29 | There are no secrets or passwords that can be leaked from this 30 | website, but any number of things could happen. The page may 31 | break, you could be redirected to another URL, or get absolutely 32 | memed on. 33 |

34 |

35 | In case you need to delete a preset without running its code, you can right click on it. 36 |

37 |

Do you accept these risks?

38 |
39 |
40 | { 42 | props.onAllow(); 43 | props.setClosed(); 44 | }} 45 | > 46 | Yes 47 | 48 | 49 | No, I'm sorry I wasted your time 50 | 51 |
52 |
53 |
54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/editor/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import { createEffect, createSignal, onMount, untrack } from "solid-js"; 2 | 3 | import { basicSetup } from "codemirror"; 4 | import { historyKeymap, indentWithTab } from "@codemirror/commands"; 5 | import { javascript } from "@codemirror/lang-javascript"; 6 | import { syntaxHighlighting } from "@codemirror/language"; 7 | import { Compartment, EditorState } from "@codemirror/state"; 8 | import { EditorView, keymap, type ViewUpdate } from "@codemirror/view"; 9 | import { 10 | oneDarkHighlightStyle, 11 | oneDarkTheme, 12 | } from "@codemirror/theme-one-dark"; 13 | import { vim } from "@replit/codemirror-vim"; 14 | 15 | import { Button } from "@kobalte/core/button"; 16 | import { debounce } from "~/lib/util"; 17 | import { AllowPasteDialog } from "./AllowPasteDialog"; 18 | 19 | type Props = { 20 | onSave: (s: string, thumbnail: boolean) => void; 21 | initialValue: string; 22 | }; 23 | 24 | const VIM_MODE_KEY = "vimMode"; 25 | const ALLOW_PASTE_KEY = "allowPaste"; 26 | 27 | export function CodeEditor(props: Props) { 28 | let parent!: HTMLDivElement; 29 | let view: EditorView; 30 | let modeComp = new Compartment(); 31 | let allowPaste; 32 | 33 | const [vimMode, _setVimMode] = createSignal(false); 34 | const setVimMode = (v: boolean) => { 35 | _setVimMode(v); 36 | view.dispatch({ 37 | effects: modeComp.reconfigure(v ? vim() : []), 38 | }); 39 | localStorage.setItem(VIM_MODE_KEY, v ? "true" : "false"); 40 | }; 41 | 42 | const [updateThumbnail, setUpdateThumbnail] = createSignal(true); 43 | 44 | const [dirty, setDirty] = createSignal(false); 45 | 46 | const extensions = [ 47 | modeComp.of(vimMode() ? vim() : []), 48 | basicSetup, 49 | EditorView.lineWrapping, 50 | keymap.of([ 51 | indentWithTab, 52 | { 53 | win: "Mod-Shift-z", 54 | // Dirty hack, but undo/redo commands are not exposed 55 | run: historyKeymap[1].run, 56 | }, 57 | { 58 | key: "Mod-s", 59 | linux: "Ctrl-s", // untested, but might be necessary 60 | run: (view) => { 61 | props.onSave(view.state.doc.toString(), updateThumbnail()); 62 | return true; 63 | }, 64 | }, 65 | { 66 | key: "Mod-v", 67 | run: () => { 68 | if (allowPaste) return false; 69 | setShowDialog(true); 70 | return true; 71 | }, 72 | }, 73 | ]), 74 | EditorView.domEventHandlers({ 75 | paste() { 76 | if (allowPaste) return false; 77 | setShowDialog(true); 78 | return true; 79 | }, 80 | }), 81 | javascript(), 82 | oneDarkTheme, 83 | syntaxHighlighting(oneDarkHighlightStyle), 84 | EditorView.updateListener.of( 85 | debounce((u: ViewUpdate) => { 86 | // docChanged (aka changes.empty) doesn't work when debounced 87 | // if (!u.docChanged) return; 88 | const newDirty = u.state.doc.toString() !== props.initialValue; 89 | setDirty(newDirty); 90 | }, 300) 91 | ), 92 | ]; 93 | 94 | onMount(() => { 95 | view = new EditorView({ 96 | extensions, 97 | parent, 98 | }); 99 | 100 | const saved = localStorage.getItem(VIM_MODE_KEY); 101 | if (saved === "true") { 102 | _setVimMode(true); 103 | view.dispatch({ 104 | effects: modeComp.reconfigure(vim()), 105 | }); 106 | } 107 | 108 | allowPaste = localStorage.getItem(ALLOW_PASTE_KEY) === "true"; 109 | }); 110 | 111 | // Track props.initialValue 112 | createEffect(() => { 113 | setDirty(false); 114 | 115 | // Saving should not reset editor state (cursor pos etc) 116 | if (view.state.doc.toString() === props.initialValue) return; 117 | 118 | view.setState( 119 | EditorState.create({ 120 | doc: props.initialValue, 121 | extensions, 122 | selection: { 123 | head: 0, 124 | anchor: 0, 125 | }, 126 | }) 127 | ); 128 | 129 | const currVimMode = untrack(vimMode); 130 | if (currVimMode) { 131 | view.dispatch({ 132 | effects: modeComp.reconfigure(vim()), 133 | }); 134 | } 135 | 136 | // These 2 lines partially fix auto-scroll to cursor issue (focusing, then switching code, then scrolling down) 137 | // But causes blurring then scrolling to be weird 138 | view.contentDOM.focus({ preventScroll: true }); 139 | view.contentDOM.blur(); 140 | }); 141 | 142 | const [showDialog, setShowDialog] = createSignal(false); 143 | 144 | return ( 145 | <> 146 | { 149 | setShowDialog(false); 150 | }} 151 | onAllow={() => { 152 | allowPaste = true; 153 | localStorage.setItem(ALLOW_PASTE_KEY, "true"); 154 | }} 155 | /> 156 |
157 | 166 | 175 | 184 |
185 |
186 | 187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /src/components/editor/ParamsEditor.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | closestCenter, 3 | createSortable, 4 | DragDropProvider, 5 | DragDropSensors, 6 | DragOverlay, 7 | SortableProvider, 8 | transformStyle, 9 | useDragDropContext, 10 | } from "@thisbeyond/solid-dnd"; 11 | import GripVertical from "lucide-solid/icons/grip-vertical"; 12 | import Minus from "lucide-solid/icons/minus"; 13 | import Plus from "lucide-solid/icons/plus"; 14 | import { createSignal, For, Index, Show } from "solid-js"; 15 | import { Dynamic } from "solid-js/web"; 16 | import { PARAM_COMPONENTS } from "~/lib/params"; 17 | import { useRenderContext } from "~/lib/RenderContext"; 18 | import { FlatButton } from "../Button"; 19 | 20 | export function ParamsEditor() { 21 | const { paramsSchema, params, setParams } = useRenderContext(); 22 | return ( 23 |
24 | 25 | {([label, { type, ...other }]) => { 26 | if (type === "array") { 27 | return ; 28 | } 29 | return ( 30 | <> 31 |
32 |
{label}
33 | setParams(label, v)} 38 | /> 39 |
40 | 41 | ); 42 | }} 43 |
44 |
45 | ); 46 | } 47 | 48 | function ArrayParam({ label, other }) { 49 | const { params, setParams } = useRenderContext(); 50 | 51 | // 0 is falsey and not a valid key 52 | const idFromIndex = (i) => i + 1; 53 | const indexFromId = (k) => k - 1; 54 | const [activeId, setActiveId] = createSignal(null); 55 | const [dragging, setDragging] = createSignal(false); 56 | 57 | const onDragStart = ({ draggable }) => { 58 | setActiveId(draggable.id); 59 | setDragging(true); 60 | }; 61 | const onDragEnd = ({ draggable, droppable }) => { 62 | const fromIndex = indexFromId(draggable.id); 63 | const toIndex = indexFromId(droppable.id); 64 | if (fromIndex !== toIndex) { 65 | setParams(label, (prev: any[]) => { 66 | const updatedItems = prev.slice(); 67 | updatedItems.splice(toIndex, 0, ...updatedItems.splice(fromIndex, 1)); 68 | return updatedItems; 69 | }); 70 | } 71 | setDragging(false); 72 | }; 73 | return ( 74 |
75 |
{label}
76 |
77 | 78 | setParams(label, (prev: any[]) => prev.slice(0, -1))} 81 | > 82 | 83 | 84 | 87 | setParams(label, (prev: any[]) => [...prev, other.props.default]) 88 | } 89 | > 90 | 91 | 92 | 93 |
94 | 100 | 101 | 103 | idFromIndex(i) 104 | )} 105 | > 106 | 107 | {(v, i) => { 108 | const sortable = createSortable(idFromIndex(i)); 109 | const [state] = useDragDropContext()!; 110 | return ( 111 | <> 112 |
{i}
113 |
122 | setParams(label, i, v)} 131 | /> 132 |
136 | 137 |
138 |
139 | 140 | ); 141 | }} 142 |
143 |
144 | 145 |
146 | 147 | 157 | 158 |
159 | 160 |
161 |
162 |
163 |
164 |
165 | ); 166 | } 167 | -------------------------------------------------------------------------------- /src/components/editor/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { For } from "solid-js"; 2 | import { useQrContext } from "~/lib/QrContext"; 3 | import { 4 | ECL_NAMES, 5 | ECL_VALUE, 6 | MASK_KEY, 7 | MASK_NAMES, 8 | MASK_VALUE, 9 | MODE_KEY, 10 | MODE_NAMES, 11 | MODE_VALUE, 12 | } from "~/lib/options"; 13 | import { ButtonGroup, ButtonGroupItem } from "../ButtonGroup"; 14 | import { NumberInput } from "../NumberInput"; 15 | import { Select } from "../Select"; 16 | import { Switch } from "../Switch"; 17 | 18 | export function Settings() { 19 | const { inputQr, setInputQr } = useQrContext(); 20 | 21 | return ( 22 |
23 |
24 |
Encoding mode
25 |