├── .browserslistrc ├── .eslintignore ├── .husky └── pre-push ├── src ├── util │ ├── consts.ts │ ├── index.ts │ ├── helpers.ts │ └── effects.ts ├── index.ts ├── qrcanvas │ ├── index.ts │ └── renderer.ts └── types.ts ├── demo ├── style.css ├── examples │ ├── data │ │ ├── unicode.js │ │ ├── simple.js │ │ ├── padding.js │ │ ├── pure-color.js │ │ ├── text.js │ │ ├── logo.js │ │ ├── gradient.js │ │ └── colorful.js │ ├── style.css │ ├── app.js │ └── index.html ├── index.js └── index.html ├── .gitignore ├── .babelrc.js ├── .editorconfig ├── .eslintrc.js ├── tsconfig.json ├── LICENSE ├── package.json ├── rollup.conf.js └── README.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/src 3 | !/test 4 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /src/util/consts.ts: -------------------------------------------------------------------------------- 1 | export const COLOR_BLACK = '#000'; 2 | export const COLOR_WHITE = '#fff'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './qrcanvas'; 3 | export * from './util'; 4 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | .form-horizontal { 2 | width: 480px; 3 | } 4 | 5 | canvas { 6 | border: 1px solid #999; 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | *.lock 4 | /.idea 5 | /lib 6 | /dist 7 | /.nyc_output 8 | /coverage 9 | /types 10 | /demo/docs 11 | -------------------------------------------------------------------------------- /demo/examples/data/unicode.js: -------------------------------------------------------------------------------- 1 | const canvas = qrcanvas.qrcanvas({ 2 | cellSize: 8, 3 | data: '你好', 4 | }); 5 | container.appendChild(canvas); 6 | -------------------------------------------------------------------------------- /demo/examples/data/simple.js: -------------------------------------------------------------------------------- 1 | const canvas = qrcanvas.qrcanvas({ 2 | cellSize: 8, 3 | data: 'hello, world', 4 | }); 5 | container.appendChild(canvas); 6 | -------------------------------------------------------------------------------- /demo/examples/data/padding.js: -------------------------------------------------------------------------------- 1 | const canvas = qrcanvas.qrcanvas({ 2 | cellSize: 8, 3 | data: 'hello, world', 4 | padding: 8, 5 | }); 6 | container.appendChild(canvas); 7 | -------------------------------------------------------------------------------- /demo/examples/data/pure-color.js: -------------------------------------------------------------------------------- 1 | const canvas = qrcanvas.qrcanvas({ 2 | cellSize: 8, 3 | correctLevel: 'H', 4 | data: 'hello, world', 5 | foreground: 'blue', 6 | }); 7 | container.appendChild(canvas); 8 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: require.resolve('@gera2ld/plaid/config/babelrc-base'), 3 | presets: [ 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | ].filter(Boolean), 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /demo/examples/data/text.js: -------------------------------------------------------------------------------- 1 | const canvas = qrcanvas.qrcanvas({ 2 | cellSize: 8, 3 | correctLevel: 'H', 4 | data: 'hello, world', 5 | logo: { 6 | text: 'QRCanvas', 7 | options: { 8 | color: 'dodgerblue', 9 | }, 10 | }, 11 | }); 12 | container.appendChild(canvas); 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | require.resolve('@gera2ld/plaid-common-ts/eslint'), 5 | ], 6 | parserOptions: { 7 | project: './tsconfig.json', 8 | }, 9 | rules: { 10 | 'no-bitwise': 'off', 11 | 'semi-style': 'off', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "outDir": "types", 9 | "allowSyntheticDefaultImports": true, 10 | "jsx": "react" 11 | }, 12 | "include": [ 13 | "src/**/*.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/qrcanvas/index.ts: -------------------------------------------------------------------------------- 1 | import QRCanvasRenderer from './renderer'; 2 | import { QRCanvasOptions } from '../types'; 3 | 4 | export function qrcanvas(options: QRCanvasOptions) { 5 | const { 6 | canvas, 7 | size, 8 | cellSize, 9 | ...rest 10 | } = options; 11 | const renderer = new QRCanvasRenderer(rest); 12 | return renderer.render(canvas, { size, cellSize }); 13 | } 14 | -------------------------------------------------------------------------------- /demo/examples/data/logo.js: -------------------------------------------------------------------------------- 1 | const image = new Image(); 2 | image.src = 'https://user-images.githubusercontent.com/3139113/38300650-ed2c25c4-382f-11e8-9792-d46987eb17d1.png'; 3 | image.onload = () => { 4 | const canvas = qrcanvas.qrcanvas({ 5 | cellSize: 8, 6 | correctLevel: 'H', 7 | data: 'hello, world', 8 | logo: { 9 | image, 10 | }, 11 | }); 12 | container.appendChild(canvas); 13 | }; 14 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | import helpers from './helpers'; 2 | 3 | export { helpers }; 4 | export { default as effects } from './effects'; 5 | 6 | export function setCanvasModule(canvasModule) { 7 | const { Canvas, Image, createCanvas } = canvasModule; 8 | const isCanvas = el => el instanceof Canvas; 9 | const isDrawable = el => isCanvas(el) || el instanceof Image; 10 | helpers.createCanvas = createCanvas; 11 | helpers.isCanvas = isCanvas; 12 | helpers.isDrawable = isDrawable; 13 | } 14 | -------------------------------------------------------------------------------- /demo/examples/data/gradient.js: -------------------------------------------------------------------------------- 1 | const fg = document.createElement('canvas'); 2 | fg.width = 100; 3 | fg.height = 100; 4 | const ctx = fg.getContext('2d'); 5 | const gradient = ctx.createLinearGradient(0, 0, fg.width, 0); 6 | gradient.addColorStop(0, '#f00'); 7 | gradient.addColorStop(1, '#0f0'); 8 | ctx.fillStyle = gradient; 9 | ctx.fillRect(0, 0, fg.width, fg.height); 10 | 11 | const canvas = qrcanvas.qrcanvas({ 12 | cellSize: 8, 13 | data: 'hello, world', 14 | foreground: fg, 15 | }); 16 | container.appendChild(canvas); 17 | -------------------------------------------------------------------------------- /demo/examples/style.css: -------------------------------------------------------------------------------- 1 | #root > .d-flex { 2 | height: 100vh; 3 | } 4 | #root h2 { 5 | margin-top: .5em; 6 | margin-left: .5em; 7 | } 8 | #root > .d-flex > :first-child { 9 | width: 12rem; 10 | background: #f8f9fa; 11 | } 12 | #content { 13 | flex: 1; 14 | width: 0; 15 | padding: 1em; 16 | overflow: auto; 17 | } 18 | #content canvas { 19 | border: 1px solid #eee; 20 | } 21 | 22 | .flex-column { 23 | flex-direction: column; 24 | } 25 | .flex-auto { 26 | flex: auto; 27 | } 28 | .flex-column > .flex-auto { 29 | height: 0; 30 | } 31 | -------------------------------------------------------------------------------- /demo/examples/data/colorful.js: -------------------------------------------------------------------------------- 1 | const colorFore = '#55a'; 2 | const colorOut = '#c33'; 3 | const colorIn = '#621'; 4 | const canvas = qrcanvas.qrcanvas({ 5 | cellSize: 8, 6 | correctLevel: 'H', 7 | data: 'hello, world', 8 | foreground: [ 9 | // foreground color 10 | { style: colorFore }, 11 | // outer squares of the positioner 12 | { row: 0, rows: 7, col: 0, cols: 7, style: colorOut }, 13 | { row: -7, rows: 7, col: 0, cols: 7, style: colorOut }, 14 | { row: 0, rows: 7, col: -7, cols: 7, style: colorOut }, 15 | // inner squares of the positioner 16 | { row: 2, rows: 3, col: 2, cols: 3, style: colorIn }, 17 | { row: -5, rows: 3, col: 2, cols: 3, style: colorIn }, 18 | { row: 2, rows: 3, col: -5, cols: 3, style: colorIn }, 19 | ], 20 | }); 21 | container.appendChild(canvas); 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gerald 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcanvas", 3 | "version": "3.1.2", 4 | "description": "Generate characteristic qrcodes with a canvas.", 5 | "author": "Gerald ", 6 | "license": "ISC", 7 | "scripts": { 8 | "prepare": "husky install", 9 | "dev": "rollup -wc rollup.conf.js", 10 | "build:types": "tsc", 11 | "build:js": "rollup -c rollup.conf.js", 12 | "build": "run-s ci clean build:types build:js", 13 | "lint": "eslint --ext .ts .", 14 | "build:docs": "typedoc --out demo/docs src/index.ts", 15 | "gh-pages": "gh-pages -fd demo", 16 | "deploy": "run-s build:docs gh-pages", 17 | "prepublishOnly": "npm run build", 18 | "clean": "del-cli lib types", 19 | "ci": "run-s lint" 20 | }, 21 | "title": "QRCanvas", 22 | "repository": { 23 | "type": "git", 24 | "url": "git@github.com:gera2ld/qrcanvas.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/gera2ld/qrcanvas/issues" 28 | }, 29 | "typings": "types/index.d.ts", 30 | "publishConfig": { 31 | "access": "public" 32 | }, 33 | "main": "lib/qrcanvas.common.js", 34 | "module": "lib/qrcanvas.esm.js", 35 | "unpkg": "lib/qrcanvas.min.js", 36 | "jsdelivr": "lib/qrcanvas.min.js", 37 | "files": [ 38 | "lib", 39 | "types" 40 | ], 41 | "devDependencies": { 42 | "@gera2ld/plaid": "~2.5.6", 43 | "@gera2ld/plaid-common-ts": "~2.5.1", 44 | "@gera2ld/plaid-rollup": "~2.5.6", 45 | "del-cli": "^5.0.0", 46 | "gh-pages": "^4.0.0", 47 | "husky": "^8.0.1", 48 | "typedoc": "^0.23.10" 49 | }, 50 | "dependencies": { 51 | "@babel/runtime": "^7.18.9", 52 | "qrcode-generator": "^1.4.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rollup.conf.js: -------------------------------------------------------------------------------- 1 | const { getRollupPlugins, getRollupExternal, defaultOptions, rollupMinify } = require('@gera2ld/plaid'); 2 | const pkg = require('./package.json'); 3 | 4 | const DIST = 'lib'; 5 | const FILENAME = 'qrcanvas'; 6 | const BANNER = `/*! ${pkg.name} v${pkg.version} | ${pkg.license} License */`; 7 | 8 | const external = getRollupExternal([ 9 | 'qrcode-generator', 10 | ]); 11 | const bundleOptions = { 12 | extend: true, 13 | esModule: false, 14 | }; 15 | const rollupConfig = [ 16 | { 17 | input: { 18 | input: 'src/index.ts', 19 | plugins: getRollupPlugins({ 20 | extensions: defaultOptions.extensions, 21 | }), 22 | external, 23 | }, 24 | output: { 25 | format: 'cjs', 26 | file: `${DIST}/${FILENAME}.common.js`, 27 | }, 28 | }, 29 | { 30 | input: { 31 | input: 'src/index.ts', 32 | plugins: getRollupPlugins({ 33 | esm: true, 34 | extensions: defaultOptions.extensions, 35 | }), 36 | external, 37 | }, 38 | output: { 39 | format: 'esm', 40 | file: `${DIST}/${FILENAME}.esm.js`, 41 | }, 42 | }, 43 | { 44 | input: { 45 | input: 'src/index.ts', 46 | plugins: getRollupPlugins({ 47 | esm: true, 48 | extensions: defaultOptions.extensions, 49 | }), 50 | }, 51 | output: { 52 | format: 'iife', 53 | file: `${DIST}/${FILENAME}.js`, 54 | name: 'qrcanvas', 55 | ...bundleOptions, 56 | }, 57 | minify: true, 58 | }, 59 | ]; 60 | // Generate minified versions 61 | rollupConfig.filter(({ minify }) => minify) 62 | .forEach(config => { 63 | rollupConfig.push(rollupMinify(config)); 64 | }); 65 | 66 | rollupConfig.forEach((item) => { 67 | item.output = { 68 | indent: false, 69 | // If set to false, circular dependencies and live bindings for external imports won't work 70 | externalLiveBindings: false, 71 | ...item.output, 72 | ...BANNER && { 73 | banner: BANNER, 74 | }, 75 | }; 76 | }); 77 | 78 | module.exports = rollupConfig.map(({ input, output }) => ({ 79 | ...input, 80 | output, 81 | })); 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QRCanvas 2 | 3 | ![NPM](https://img.shields.io/npm/v/qrcanvas.svg) 4 | ![License](https://img.shields.io/npm/l/qrcanvas.svg) 5 | ![Downloads](https://img.shields.io/npm/dt/qrcanvas.svg) 6 | 7 | This is a QRCode generator written in pure javascript. 8 | 9 | Based on [Kazuhiko Arase's QRCode](http://www.d-project.com/). 10 | 11 | The only requirement is that the browser works with a `canvas`, 12 | which is supported by most modern browsers. 13 | 14 | ## Usage 15 | 16 | ### With bundlers 17 | 18 | Install as a dependency: 19 | 20 | ```sh 21 | $ yarn add qrcanvas 22 | # or 23 | $ npm install qrcanvas -S 24 | ``` 25 | 26 | Use as a module: 27 | 28 | ``` js 29 | import { qrcanvas } from 'qrcanvas'; 30 | 31 | const canvas = qrcanvas({ 32 | data: 'hello, world' 33 | }); 34 | document.body.appendChild(canvas); 35 | ``` 36 | 37 | ### Browser 38 | 39 | Load from CDN: 40 | 41 | ``` html 42 |
43 | 44 | 45 | ``` 46 | 47 | The module is mounted to a global variable named `qrcanvas`: 48 | 49 | ``` js 50 | const canvas = qrcanvas.qrcanvas({ 51 | data: 'hello, world' 52 | }); 53 | document.getElementById('qrcode').appendChild(canvas); 54 | ``` 55 | 56 | ### Node.js 57 | 58 | [node-canvas](https://github.com/Automattic/node-canvas) is required in Node.js. 59 | 60 | Install dependencies: 61 | 62 | ```sh 63 | $ yarn add qrcanvas canvas 64 | ``` 65 | 66 | ``` js 67 | const fs = require('fs'); 68 | const { qrcanvas, setCanvasModule } = require('qrcanvas'); 69 | 70 | // Enable node-canvas 71 | setCanvasModule(require('canvas')); 72 | 73 | const canvas = qrcanvas({ 74 | data: 'hello, world' 75 | }); 76 | // canvas is an instance of `node-canvas` 77 | canvas.pngStream().pipe(fs.createWriteStream('qrcode.png')); 78 | ``` 79 | 80 | ## Documents 81 | 82 | - [Demo](https://gera2ld.github.io/qrcanvas/) 83 | - [Docs](https://gera2ld.github.io/qrcanvas/docs/) 84 | - [Examples](https://gera2ld.github.io/qrcanvas/examples/) 85 | 86 | ## Related 87 | 88 | - [qrcanvas-vue](https://github.com/gera2ld/qrcanvas-vue) 89 | - [qrcanvas-react](https://github.com/gera2ld/qrcanvas-react) 90 | 91 | ## Snapshots 92 | 93 | ![1](https://user-images.githubusercontent.com/3139113/39859468-8acec31a-546c-11e8-83b6-10e889423e88.png) 94 | 95 | ![2](https://user-images.githubusercontent.com/3139113/39859482-9b6c0d68-546c-11e8-83cd-d03a148c3e70.png) 96 | -------------------------------------------------------------------------------- /demo/examples/app.js: -------------------------------------------------------------------------------- 1 | const menu = $('#menu'); 2 | const content = $('#content'); 3 | const LOADER = '
'; 4 | const demos = [ 5 | { name: 'Simple', path: 'simple' }, 6 | { name: 'Text', path: 'text' }, 7 | { name: 'Logo', path: 'logo' }, 8 | { name: 'Pure color', path: 'pure-color' }, 9 | { name: 'Colorful', path: 'colorful' }, 10 | { name: 'Unicode', path: 'unicode' }, 11 | { name: 'Padding', path: 'padding' }, 12 | { name: 'Gradient', path: 'gradient' }, 13 | ]; 14 | let active; 15 | demos.forEach(item => { 16 | menu.append(createElement('li', { 17 | className: 'menu-item', 18 | }, [ 19 | item.el = createElement('a', { 20 | href: `#${item.path}`, 21 | textContent: item.name, 22 | }), 23 | ])); 24 | }); 25 | 26 | window.addEventListener('hashchange', handleHashChange, false); 27 | handleHashChange(); 28 | 29 | FallbackJs.ok(); 30 | 31 | function handleHashChange() { 32 | const path = window.location.hash.slice(1); 33 | const item = demos.find(item => item.path === path) || demos[0]; 34 | showDemo(item); 35 | } 36 | 37 | function showDemo(item) { 38 | if (active) active.el.classList.remove('active'); 39 | active = item; 40 | active.el.classList.add('active'); 41 | content.innerHTML = LOADER; 42 | loadResource(item) 43 | .then(item => { 44 | content.innerHTML = ''; 45 | let container; 46 | content.append( 47 | createElement('h3', { textContent: item.name }), 48 | container = createElement('div', { 49 | className: 'my-2 text-center', 50 | }), 51 | createElement('pre', { 52 | className: 'code', 53 | }, [ 54 | code = createElement('code', { 55 | innerHTML: Prism.highlight(item.code, Prism.languages.javascript), 56 | }), 57 | ]), 58 | ); 59 | const fn = new Function('container', item.code); 60 | container.innerHTML = LOADER; 61 | fn({ 62 | appendChild: canvas => { 63 | container.innerHTML = ''; 64 | container.append(canvas); 65 | }, 66 | }); 67 | }); 68 | } 69 | 70 | function loadResource(item) { 71 | if (item.code) return Promise.resolve(item); 72 | return fetch(`data/${item.path}.js`) 73 | .then(res => res.text()) 74 | .then(code => { 75 | item.code = code; 76 | return item; 77 | }); 78 | } 79 | 80 | function $(selector) { 81 | return document.querySelector(selector); 82 | } 83 | 84 | function createElement(tagName, props, children) { 85 | const el = document.createElement(tagName); 86 | if (props) { 87 | Object.keys(props).forEach(key => { 88 | const value = props[key]; 89 | if (key === 'on') { 90 | bindEvents(el, value); 91 | } else { 92 | el[key] = value; 93 | } 94 | }); 95 | } 96 | if (children) { 97 | children.forEach(child => { 98 | el.append(child); 99 | }); 100 | } 101 | return el; 102 | } 103 | 104 | function bindEvents(el, events) { 105 | if (events) { 106 | Object.keys(events).forEach(type => { 107 | const handle = events[type]; 108 | if (handle) el.addEventListener(type, handle); 109 | }); 110 | } 111 | return el; 112 | } 113 | -------------------------------------------------------------------------------- /demo/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QRCanvas Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Fork me on GitHub 15 |
16 |
17 |
18 | 19 |

QRCanvas

20 |
21 | 22 |
23 |

Designed with ♥ by Gerald

24 |
25 |
26 |
27 |
28 |
29 | 34 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /* global qrcanvas, Vue */ 2 | /* eslint-disable object-curly-newline */ 3 | 4 | const size = 256; 5 | 6 | const QrCanvas = { 7 | props: { 8 | options: Object, 9 | }, 10 | render: h => h('canvas', { 11 | attrs: { width: size, height: size }, 12 | }), 13 | methods: { 14 | update(options) { 15 | const qroptions = Object.assign({}, options, { 16 | canvas: this.$el, 17 | }); 18 | qrcanvas.qrcanvas(qroptions); 19 | }, 20 | }, 21 | watch: { 22 | options: 'update', 23 | }, 24 | mounted() { 25 | this.update(this.options); 26 | }, 27 | }; 28 | 29 | const themes = { 30 | classic: { 31 | colorFore: '#000000', 32 | colorBack: '#ffffff', 33 | colorOut: '#000000', 34 | colorIn: '#000000', 35 | }, 36 | light: { 37 | colorFore: '#0d86ff', 38 | colorBack: '#ffffff', 39 | colorOut: '#ff8080', 40 | colorIn: '#0059b3', 41 | }, 42 | dark: { 43 | colorFore: '#4169e1', 44 | colorBack: '#ffffff', 45 | colorOut: '#cd5c5c', 46 | colorIn: '#191970', 47 | }, 48 | }; 49 | 50 | const correctLevels = ['L', 'M', 'Q', 'H']; 51 | 52 | const data = { 53 | settings: Object.assign({ 54 | qrtext: 'https://gerald.win', 55 | cellSize: 6, 56 | padding: 0, 57 | effect: '', 58 | effectValue: 100, 59 | spotRatio: 25, 60 | logo: false, 61 | logoType: 'image', 62 | logoText: 'Gerald', 63 | logoClearEdges: 3, 64 | logoMargin: 0, 65 | logoColor: '#000000', 66 | correctLevel: 0, 67 | }, themes.classic), 68 | effects: [ 69 | { title: 'None', value: '' }, 70 | { title: 'Fusion', value: 'fusion' }, 71 | { title: 'Round', value: 'round' }, 72 | { title: 'Spot', value: 'spot' }, 73 | ], 74 | themes: Object.keys(themes), 75 | options: {}, 76 | }; 77 | 78 | new Vue({ 79 | components: { 80 | QrCanvas, 81 | }, 82 | data, 83 | watch: { 84 | settings: { 85 | deep: true, 86 | handler: 'update', 87 | }, 88 | }, 89 | methods: { 90 | update() { 91 | const { settings } = this; 92 | const { 93 | colorFore, colorBack, colorOut, colorIn, 94 | } = settings; 95 | const options = { 96 | cellSize: +settings.cellSize, 97 | padding: +settings.padding, 98 | foreground: [ 99 | // foreground color 100 | { style: colorFore }, 101 | // outer squares of the positioner 102 | { row: 0, rows: 7, col: 0, cols: 7, style: colorOut }, 103 | { row: -7, rows: 7, col: 0, cols: 7, style: colorOut }, 104 | { row: 0, rows: 7, col: -7, cols: 7, style: colorOut }, 105 | // inner squares of the positioner 106 | { row: 2, rows: 3, col: 2, cols: 3, style: colorIn }, 107 | { row: -5, rows: 3, col: 2, cols: 3, style: colorIn }, 108 | { row: 2, rows: 3, col: -5, cols: 3, style: colorIn }, 109 | ], 110 | background: colorBack, 111 | data: settings.qrtext, 112 | correctLevel: correctLevels[settings.correctLevel] || 'L', 113 | }; 114 | if (settings.logo) { 115 | if (settings.logoType === 'image') { 116 | options.logo = { 117 | image: this.$refs.logo, 118 | }; 119 | } else { 120 | options.logo = { 121 | text: settings.logoText, 122 | options: { 123 | fontStyle: [ 124 | settings.logoBold && 'bold', 125 | settings.logoItalic && 'italic', 126 | ].filter(Boolean).join(' '), 127 | fontFamily: settings.logoFont, 128 | color: settings.logoColor, 129 | }, 130 | }; 131 | } 132 | } 133 | if (settings.effect) { 134 | options.effect = { 135 | type: settings.effect, 136 | value: settings.effectValue / 100, 137 | spotRatio: settings.spotRatio / 100, 138 | }; 139 | if (settings.effect === 'spot') { 140 | options.background = [colorBack, this.$refs.effect]; 141 | } 142 | } 143 | this.options = options; 144 | }, 145 | loadImage(e, ref) { 146 | const file = e.target.files[0]; 147 | if (!file) return; 148 | const reader = new FileReader(); 149 | reader.onload = () => { 150 | this.$refs[ref].src = reader.result; 151 | this.options = Object.assign({}, this.options); 152 | }; 153 | reader.readAsDataURL(file); 154 | }, 155 | loadTheme(key) { 156 | Object.assign(this.settings, themes[key]); 157 | }, 158 | }, 159 | mounted() { 160 | this.update(); 161 | }, 162 | }).$mount('#app'); 163 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type TypeNumber = 2 | | 0 // Automatic type number 3 | | 1 4 | | 2 5 | | 3 6 | | 4 7 | | 5 8 | | 6 9 | | 7 10 | | 8 11 | | 9 12 | | 10 13 | | 11 14 | | 12 15 | | 13 16 | | 14 17 | | 15 18 | | 16 19 | | 17 20 | | 18 21 | | 19 22 | | 20 23 | | 21 24 | | 22 25 | | 23 26 | | 24 27 | | 25 28 | | 26 29 | | 27 30 | | 28 31 | | 29 32 | | 30 33 | | 31 34 | | 32 35 | | 33 36 | | 34 37 | | 35 38 | | 36 39 | | 37 40 | | 38 41 | | 39 42 | | 40; 43 | 44 | export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'; 45 | 46 | export interface QRCanvasOptions { 47 | /** 48 | * The type number of the QRCode. If too small to contain the data, the smallest valid type number will be used instead. 49 | * Default as `0`. 50 | */ 51 | typeNumber?: TypeNumber; 52 | /** 53 | * The correct level of QRCode. When `logo` is assigned, `correctLevel` will be set to `H`. 54 | */ 55 | correctLevel?: ErrorCorrectionLevel; 56 | /** 57 | * The data to be encoded in the QRCode, text will be encoded in UTF-8. Default as `''`. 58 | */ 59 | data?: string; 60 | /** 61 | * The final image will be painted to `canvas` if provided. 62 | */ 63 | canvas?: HTMLCanvasElement; 64 | /** 65 | * The pixel width or height of the entire image, ignored if cellSize is specified. Default as `undefined`. 66 | */ 67 | size?: number; 68 | /** 69 | * The pixel width or height of a cell. Default value is used only if neither `cellSize` nor `size` is provided. Default as `2`. 70 | */ 71 | cellSize?: number; 72 | /** 73 | * The background color or image of the QRCode. Default as `'white'`. 74 | */ 75 | background?: QRCanvasLayerValue; 76 | /** 77 | * The foreground color or image of the QRCode. Default as `'black'`. 78 | */ 79 | foreground?: QRCanvasLayerValue; 80 | /** 81 | * Padding space around the QRCode. Default as `0`. 82 | */ 83 | padding?: number; 84 | /** 85 | * Add a logo in the middle of the QRCode. 86 | */ 87 | logo?: QRCanvasLogo; 88 | /** 89 | * Whether to resize the canvas to size of QRCode on render 90 | */ 91 | resize?: boolean; 92 | effect?: QRCanvasEffect; 93 | } 94 | 95 | export interface QRCanvasRenderConfig { 96 | size?: number; 97 | cellSize?: number; 98 | } 99 | 100 | export interface QRCanvasEffect { 101 | /** 102 | * Built-in effects are `round`, `fusion` and `spot`. 103 | */ 104 | type?: string; 105 | /** 106 | * A ratio between 0..1. 107 | */ 108 | value?: number; 109 | /** 110 | * The foreground color for `spot` effect. 111 | */ 112 | foregroundLight?: string; 113 | /** 114 | * The percentage of a cell that should be dedicated to the QR code data. A ratio between 0.25..1. 115 | * 116 | * Defaults to 0.25 117 | */ 118 | spotRatio?: number; 119 | } 120 | 121 | export interface QRCanvasBaseLayer { 122 | /** 123 | * Width of the layer in pixels. Default as the width of QRCode. 124 | */ 125 | w?: number; 126 | /** 127 | * Height of the layer in pixels. Default as the height of QRCode. 128 | */ 129 | h?: number; 130 | /** 131 | * Width of the layer in columns. If not provided, {@link w} will be used instead. 132 | */ 133 | cols?: number; 134 | /** 135 | * Height of the layer in rows. If not provided, {@link h} will be used instead. 136 | */ 137 | rows?: number; 138 | /** 139 | * X of start position. Default as `0`. 140 | */ 141 | x?: number; 142 | /** 143 | * Y of start position. Default as `0`. 144 | */ 145 | y?: number; 146 | /** 147 | * Column index of the start position. If not provided, {@link x} will be used instead. 148 | */ 149 | col?: number; 150 | /** 151 | * Row index of the start position. If not provided, {@link y} will be used instead. 152 | */ 153 | row?: number; 154 | } 155 | 156 | export interface QRCanvasFillLayer extends QRCanvasBaseLayer { 157 | /** 158 | * CSS style to fill the area defined by other attributes. Default as `'black'`. 159 | */ 160 | style: string; 161 | } 162 | 163 | export interface QRCanvasTextLayer extends QRCanvasBaseLayer { 164 | text: string; 165 | options?: QRCanvasDrawTextOptions; 166 | } 167 | 168 | export interface QRCanvasImageLayer extends QRCanvasBaseLayer { 169 | image: CanvasImageSource; 170 | } 171 | 172 | /** 173 | * A layer defines what to paint to a canvas. 174 | */ 175 | export type QRCanvasLayer = QRCanvasFillLayer | QRCanvasImageLayer; 176 | 177 | /** 178 | * Examples: 179 | * 180 | * - text logo 181 | * 182 | * ```js 183 | * logo = 'hello, world'; 184 | * logo = { 185 | * text: 'hello, world', 186 | * options: { 187 | * color: 'green', 188 | * }, 189 | * }; 190 | * ``` 191 | * 192 | * - image logo 193 | * 194 | * ```js 195 | * logo = { image: loadedImageElementOrCanvas }; 196 | * ``` 197 | * 198 | * - another {@link QRCanvasLayer} 199 | * 200 | * ```js 201 | * logo = { style: 'red' }; 202 | * ``` 203 | */ 204 | export type QRCanvasLogo = 205 | | QRCanvasLayer 206 | | QRCanvasTextLayer 207 | | CanvasImageSource 208 | | string; 209 | 210 | export interface QRCanvasDrawTextOptions { 211 | fontSize?: number; 212 | fontStyle?: string; 213 | fontFamily?: string; 214 | color?: string; 215 | pad?: number; 216 | padColor?: string; 217 | mode?: number; 218 | } 219 | 220 | export type QRCanvasLayerValue = 221 | | string 222 | | CanvasImageSource 223 | | QRCanvasLayer 224 | | QRCanvasLayerValue[]; 225 | 226 | export interface DrawCanvasOptions { 227 | cellSize?: number; 228 | context?: CanvasRenderingContext2D; 229 | clear?: boolean; 230 | } 231 | -------------------------------------------------------------------------------- /src/qrcanvas/renderer.ts: -------------------------------------------------------------------------------- 1 | import qrcode from 'qrcode-generator'; 2 | import helpers from '../util/helpers'; 3 | import effects from '../util/effects'; 4 | import { 5 | QRCanvasOptions, QRCanvasRenderConfig, QRCanvasTextLayer, QRCanvasImageLayer, QRCanvasLayerValue, 6 | } from '../types'; 7 | 8 | // Enable UTF_8 support 9 | qrcode.stringToBytes = qrcode.stringToBytesFuncs['UTF-8']; 10 | 11 | const DEFAULTS: QRCanvasOptions = { 12 | background: 'white', 13 | foreground: 'black', 14 | typeNumber: 0, 15 | correctLevel: 'L', 16 | data: '', 17 | padding: 0, 18 | resize: true, 19 | }; 20 | 21 | interface QRCanvasRendererCache { 22 | qr?: ReturnType; 23 | count?: number; 24 | } 25 | 26 | export default class QRCanvasRenderer { 27 | private options: QRCanvasOptions = { ...DEFAULTS }; 28 | 29 | private cache: QRCanvasRendererCache = {}; 30 | 31 | private logo?: QRCanvasImageLayer; 32 | 33 | constructor(options: Partial) { 34 | this.setOptions(options); 35 | } 36 | 37 | public render(canvas: HTMLCanvasElement | undefined, config: QRCanvasRenderConfig = {}) { 38 | const { 39 | background, 40 | foreground, 41 | padding, 42 | effect, 43 | resize, 44 | } = this.options; 45 | const onRender = effects[effect.type] || effects.default; 46 | const { 47 | count, 48 | } = this.cache; 49 | const { drawCanvas } = helpers; 50 | let { size } = config; 51 | let canvasOut: HTMLCanvasElement; 52 | let canvasBg: HTMLCanvasElement; 53 | let canvasFg: HTMLCanvasElement; 54 | // Prepare output canvas, resize it if cellSize or size is provided. 55 | { 56 | let { cellSize } = config; 57 | if (!canvas && !cellSize && !size) cellSize = 6; 58 | if (cellSize) size = count * cellSize + padding + padding; 59 | if (size) { 60 | canvasOut = resize || !canvas ? helpers.updateCanvas(canvas, size) : canvas; 61 | } else { 62 | size = canvas.width; 63 | canvasOut = canvas; 64 | } 65 | } 66 | const contentSize = size - padding - padding; 67 | // Create foreground and background layers on canvas 68 | { 69 | const cellSize = Math.ceil(contentSize / count); 70 | const sketchSize = cellSize * count; 71 | canvasBg = helpers.getCanvas(cellSize * count); 72 | drawCanvas(canvasBg, background, { cellSize }); 73 | canvasFg = onRender({ 74 | foreground, 75 | cellSize, 76 | isDark: this.isDark, 77 | ...this.cache, 78 | }, this.options.effect); 79 | // draw logo 80 | if (this.logo) { 81 | const logo: QRCanvasImageLayer = { ...this.logo }; 82 | if (!logo.w && !logo.h && !logo.cols && !logo.rows) { 83 | const { width, height } = logo.image as { width: number; height: number }; 84 | const imageRatio = width / height; 85 | const posRatio = Math.min((count - 18) / count, 0.38); 86 | const h = Math.min( 87 | height, 88 | sketchSize * posRatio, 89 | sketchSize * posRatio / imageRatio, 90 | ); 91 | const w = h * imageRatio; 92 | const x = (sketchSize - w) / 2; 93 | const y = (sketchSize - h) / 2; 94 | logo.w = w; 95 | logo.h = h; 96 | logo.x = x; 97 | logo.y = y; 98 | } 99 | drawCanvas(canvasFg, logo, { clear: false }); 100 | } 101 | } 102 | // Combine the layers 103 | drawCanvas(canvasOut, [ 104 | { image: canvasBg }, 105 | { 106 | image: canvasFg, 107 | x: padding, 108 | y: padding, 109 | w: contentSize, 110 | h: contentSize, 111 | }, 112 | ]); 113 | return canvasOut; 114 | } 115 | 116 | private setOptions(options: Partial) { 117 | this.options = { 118 | ...this.options, 119 | ...options, 120 | }; 121 | this.normalizeEffect(); 122 | this.normalizeLogo(); 123 | const { typeNumber, data, logo } = this.options; 124 | // L / M / Q / H 125 | let { correctLevel } = this.options; 126 | if (logo && ['Q', 'H'].indexOf(correctLevel) < 0) correctLevel = 'H'; 127 | const qr = qrcode(typeNumber, correctLevel); 128 | qr.addData(data || ''); 129 | qr.make(); 130 | const count = qr.getModuleCount(); 131 | this.cache = { 132 | qr, 133 | count, 134 | }; 135 | } 136 | 137 | private normalizeEffect() { 138 | let { effect } = this.options; 139 | if (typeof effect === 'string') { 140 | effect = { type: effect }; 141 | } 142 | this.options.effect = effect || {}; 143 | } 144 | 145 | private normalizeLogo() { 146 | const { isDrawable, drawText } = helpers; 147 | const { logo } = this.options; 148 | if (logo) { 149 | if (isDrawable(logo as QRCanvasLayerValue)) { 150 | this.logo = { image: logo as CanvasImageSource }; 151 | } else if ((logo as QRCanvasImageLayer).image) { 152 | this.logo = logo as QRCanvasImageLayer; 153 | } else { 154 | let textLogo: QRCanvasTextLayer; 155 | if (typeof logo === 'string') { 156 | textLogo = { text: logo }; 157 | } else if ((logo as QRCanvasTextLayer).text) { 158 | textLogo = logo as QRCanvasTextLayer; 159 | } 160 | if (textLogo?.text) { 161 | this.logo = { image: drawText(textLogo.text, textLogo.options) }; 162 | } else { 163 | this.logo = null; 164 | } 165 | } 166 | } 167 | } 168 | 169 | private isDark = (i: number, j: number) => { 170 | const { qr, count } = this.cache; 171 | if (i < 0 || i >= count || j < 0 || j >= count) return false; 172 | return qr.isDark(i, j); 173 | }; 174 | } 175 | -------------------------------------------------------------------------------- /src/util/helpers.ts: -------------------------------------------------------------------------------- 1 | import { COLOR_BLACK, COLOR_WHITE } from './consts'; 2 | import { 3 | QRCanvasLayerValue, 4 | QRCanvasDrawTextOptions, 5 | QRCanvasLayer, 6 | QRCanvasImageLayer, 7 | QRCanvasFillLayer, 8 | DrawCanvasOptions, 9 | } from '../types'; 10 | 11 | const helpers = { 12 | createCanvas, 13 | isCanvas, 14 | isDrawable, 15 | getCanvas, 16 | updateCanvas, 17 | drawCanvas, 18 | drawText, 19 | }; 20 | export type QRCanvasHelpers = typeof helpers; 21 | export default helpers; 22 | 23 | function createCanvas(width: number, height: number): HTMLCanvasElement { 24 | const canvas = document.createElement('canvas'); 25 | canvas.width = width; 26 | canvas.height = height; 27 | return canvas; 28 | } 29 | 30 | function isCanvas(el: QRCanvasLayerValue): boolean { 31 | return el instanceof HTMLCanvasElement; 32 | } 33 | 34 | function isDrawable(el: QRCanvasLayerValue): boolean { 35 | return isCanvas(el) || el instanceof HTMLImageElement; 36 | } 37 | 38 | function getCanvas(width: number, height?: number): HTMLCanvasElement { 39 | return helpers.createCanvas(width, height == null ? width : height); 40 | } 41 | 42 | function updateCanvas( 43 | canvas: HTMLCanvasElement, 44 | width: number, 45 | height?: number 46 | ): HTMLCanvasElement { 47 | if (canvas) { 48 | canvas.width = width; 49 | canvas.height = height == null ? width : height; 50 | return canvas; 51 | } 52 | return getCanvas(width, height); 53 | } 54 | 55 | /** 56 | * Paint to a canvas with given image or colors. 57 | */ 58 | function drawCanvas( 59 | canvas: HTMLCanvasElement, 60 | data: QRCanvasLayerValue, 61 | options: DrawCanvasOptions = {} 62 | ): HTMLCanvasElement { 63 | const { cellSize, context, clear = true } = options; 64 | const { width, height } = canvas; 65 | let queue: QRCanvasLayerValue[] = [data]; 66 | const ctx = context || canvas.getContext('2d'); 67 | if (clear) ctx.clearRect(0, 0, width, height); 68 | ctx.globalCompositeOperation = 'source-over'; 69 | while (queue.length) { 70 | const item = queue.shift(); 71 | if (Array.isArray(item)) { 72 | queue = item.concat(queue); 73 | } else if (item) { 74 | let obj: QRCanvasLayer; 75 | if (helpers.isDrawable(item)) { 76 | obj = { image: item as CanvasImageSource }; 77 | } else if (typeof item === 'string') { 78 | obj = { style: item }; 79 | } else { 80 | obj = item as QRCanvasLayer; 81 | } 82 | let x = (obj.col == null ? obj.x : obj.col * cellSize) || 0; 83 | let y = (obj.row == null ? obj.y : obj.row * cellSize) || 0; 84 | if (x < 0) x += width; 85 | if (y < 0) y += width; 86 | const w = ('cols' in obj ? obj.cols * cellSize : obj.w) || width; 87 | const h = ('rows' in obj ? obj.rows * cellSize : obj.h) || width; 88 | if ((obj as QRCanvasImageLayer).image) { 89 | ctx.drawImage((obj as QRCanvasImageLayer).image, x, y, w, h); 90 | } else if ((obj as QRCanvasFillLayer).style) { 91 | ctx.fillStyle = (obj as QRCanvasFillLayer).style || 'black'; 92 | ctx.fillRect(x, y, w, h); 93 | } 94 | } 95 | } 96 | return canvas; 97 | } 98 | 99 | function drawText( 100 | text: string, 101 | options?: QRCanvasDrawTextOptions 102 | ): HTMLCanvasElement { 103 | const { 104 | fontSize = 64, 105 | fontStyle = '', // italic bold 106 | fontFamily = 'Cursive', 107 | color = null, 108 | pad = 8, 109 | padColor = COLOR_WHITE, 110 | // mode = 0: add outline with padColor to pixels 111 | // mode = 1: make a rect with padColor as background 112 | mode = 1, 113 | } = options || {}; 114 | const canvas = getCanvas(1); 115 | const ctx = canvas.getContext('2d'); 116 | let padColorArr: Uint8ClampedArray; 117 | if (padColor) { 118 | ctx.fillStyle = padColor; 119 | ctx.fillRect(0, 0, 1, 1); 120 | ({ data: padColorArr } = ctx.getImageData(0, 0, 1, 1)); 121 | if (!padColorArr[3]) padColorArr = null; 122 | } 123 | const height = fontSize + 2 * pad; 124 | const font = [fontStyle, `${fontSize}px`, fontFamily] 125 | .filter(Boolean) 126 | .join(' '); 127 | const resetContext = (): void => { 128 | ctx.textAlign = 'center'; 129 | ctx.textBaseline = 'middle'; 130 | ctx.font = font; 131 | }; 132 | resetContext(); 133 | const width = Math.ceil(ctx.measureText(text).width) + 2 * pad; 134 | canvas.width = width; 135 | canvas.height = height; 136 | resetContext(); 137 | const fillText = (): void => { 138 | ctx.fillStyle = color || COLOR_BLACK; 139 | ctx.fillText(text, width / 2, height / 2); 140 | }; 141 | if (mode === 1) { 142 | ctx.fillStyle = padColor; 143 | ctx.fillRect(0, 0, width, height); 144 | fillText(); 145 | } else { 146 | fillText(); 147 | if (padColorArr) { 148 | const imageData = ctx.getImageData(0, 0, width, height); 149 | const { data } = imageData; 150 | const total = width * height; 151 | const padded = []; 152 | let offset = 0; 153 | for (let loop = 0; loop < pad; loop += 1) { 154 | const current = []; 155 | const unique = {}; 156 | padded[offset] = current; 157 | offset = 1 - offset; 158 | let last = padded[offset]; 159 | if (!last) { 160 | last = []; 161 | for (let i = 0; i < total; i += 1) last.push(i); 162 | } 163 | last.forEach((i) => { 164 | if (data[4 * i + 3]) { 165 | [ 166 | i % width ? i - 1 : -1, 167 | (i + 1) % width ? i + 1 : -1, 168 | i - width, 169 | i + width, 170 | ].forEach((j) => { 171 | const k = 4 * j; 172 | if (k >= 0 && k <= data.length && !unique[j]) { 173 | unique[j] = 1; 174 | current.push(j); 175 | } 176 | }); 177 | } 178 | }); 179 | current.forEach((i) => { 180 | const j = 4 * i; 181 | if (!data[j + 3]) { 182 | for (let k = 0; k < 4; k += 1) { 183 | data[j + k] = padColorArr[k]; 184 | } 185 | } 186 | }); 187 | } 188 | ctx.putImageData(imageData, 0, 0); 189 | } 190 | } 191 | return canvas; 192 | } 193 | -------------------------------------------------------------------------------- /src/util/effects.ts: -------------------------------------------------------------------------------- 1 | import helpers from './helpers'; 2 | import { COLOR_BLACK, COLOR_WHITE } from './consts'; 3 | import { QRCanvasEffect } from '../types'; 4 | 5 | export default { 6 | default: renderDefault, 7 | round: renderRound, 8 | fusion: renderFusion, 9 | spot: renderSpot, 10 | }; 11 | 12 | function renderDefault({ 13 | foreground, 14 | cellSize, 15 | isDark, 16 | count, 17 | }) { 18 | const { getCanvas, drawCanvas } = helpers; 19 | const width = cellSize * count; 20 | const canvasMask = getCanvas(width); 21 | const context = canvasMask.getContext('2d'); 22 | context.fillStyle = COLOR_BLACK; 23 | drawCells({ cellSize, count }, ({ 24 | i, j, x, y, 25 | }) => { 26 | if (isDark(i, j)) { 27 | context.fillRect(x, y, cellSize, cellSize); 28 | } 29 | }); 30 | const canvasFg = getCanvas(width); 31 | drawCanvas(canvasFg, foreground, { cellSize }); 32 | const ctx = canvasFg.getContext('2d'); 33 | ctx.globalCompositeOperation = 'destination-in'; 34 | ctx.drawImage(canvasMask, 0, 0); 35 | return canvasFg; 36 | } 37 | 38 | function renderRound({ 39 | foreground, 40 | cellSize, 41 | isDark, 42 | count, 43 | }, maskOptions: QRCanvasEffect) { 44 | const { getCanvas, drawCanvas } = helpers; 45 | const width = cellSize * count; 46 | const canvasMask = getCanvas(width); 47 | const { 48 | value = 1, 49 | } = maskOptions; 50 | const radius = value * cellSize / 2; 51 | const context = canvasMask.getContext('2d'); 52 | context.fillStyle = COLOR_BLACK; 53 | drawCells({ cellSize, count }, ({ 54 | i, j, x, y, 55 | }) => { 56 | if (isDark(i, j)) { 57 | context.beginPath(); 58 | context.moveTo(x + 0.5 * cellSize, y); 59 | drawCorner(context, x + cellSize, y, x + cellSize, y + 0.5 * cellSize, radius); 60 | drawCorner(context, x + cellSize, y + cellSize, x + 0.5 * cellSize, y + cellSize, radius); 61 | drawCorner(context, x, y + cellSize, x, y + 0.5 * cellSize, radius); 62 | drawCorner(context, x, y, x + 0.5 * cellSize, y, radius); 63 | // context.closePath(); 64 | context.fill(); 65 | } 66 | }); 67 | const canvasFg = getCanvas(width); 68 | drawCanvas(canvasFg, foreground, { cellSize }); 69 | const ctx = canvasFg.getContext('2d'); 70 | ctx.globalCompositeOperation = 'destination-in'; 71 | ctx.drawImage(canvasMask, 0, 0); 72 | return canvasFg; 73 | } 74 | 75 | function renderFusion({ 76 | foreground, 77 | cellSize, 78 | isDark, 79 | count, 80 | }, maskOptions: QRCanvasEffect) { 81 | const { getCanvas, drawCanvas } = helpers; 82 | const width = cellSize * count; 83 | const canvasMask = getCanvas(width); 84 | const context = canvasMask.getContext('2d'); 85 | context.fillStyle = COLOR_BLACK; 86 | const { 87 | value = 1, 88 | } = maskOptions; 89 | const radius = value * cellSize / 2; 90 | drawCells({ cellSize, count }, ({ 91 | i, j, x, y, 92 | }) => { 93 | const corners = [0, 0, 0, 0]; // NW, NE, SE, SW 94 | if (isDark(i - 1, j)) { 95 | corners[0] += 1; 96 | corners[1] += 1; 97 | } 98 | if (isDark(i + 1, j)) { 99 | corners[2] += 1; 100 | corners[3] += 1; 101 | } 102 | if (isDark(i, j - 1)) { 103 | corners[0] += 1; 104 | corners[3] += 1; 105 | } 106 | if (isDark(i, j + 1)) { 107 | corners[1] += 1; 108 | corners[2] += 1; 109 | } 110 | if (isDark(i, j)) { 111 | if (isDark(i - 1, j - 1)) corners[0] += 1; 112 | if (isDark(i - 1, j + 1)) corners[1] += 1; 113 | if (isDark(i + 1, j + 1)) corners[2] += 1; 114 | if (isDark(i + 1, j - 1)) corners[3] += 1; 115 | context.beginPath(); 116 | context.moveTo(x + 0.5 * cellSize, y); 117 | drawCorner( 118 | context, 119 | x + cellSize, 120 | y, 121 | x + cellSize, 122 | y + 0.5 * cellSize, 123 | corners[1] ? 0 : radius, 124 | ); 125 | drawCorner( 126 | context, 127 | x + cellSize, 128 | y + cellSize, 129 | x + 0.5 * cellSize, 130 | y + cellSize, 131 | corners[2] ? 0 : radius, 132 | ); 133 | drawCorner(context, x, y + cellSize, x, y + 0.5 * cellSize, corners[3] ? 0 : radius); 134 | drawCorner(context, x, y, x + 0.5 * cellSize, y, corners[0] ? 0 : radius); 135 | // context.closePath(); 136 | context.fill(); 137 | } else { 138 | if (corners[0] === 2) { 139 | fillCorner(context, x, y + 0.5 * cellSize, x, y, x + 0.5 * cellSize, y, radius); 140 | } 141 | if (corners[1] === 2) { 142 | fillCorner( 143 | context, 144 | x + 0.5 * cellSize, 145 | y, 146 | x + cellSize, 147 | y, 148 | x + cellSize, 149 | y + 0.5 * cellSize, 150 | radius, 151 | ); 152 | } 153 | if (corners[2] === 2) { 154 | fillCorner( 155 | context, 156 | x + cellSize, 157 | y + 0.5 * cellSize, 158 | x + cellSize, 159 | y + cellSize, 160 | x + 0.5 * cellSize, 161 | y + cellSize, 162 | radius, 163 | ); 164 | } 165 | if (corners[3] === 2) { 166 | fillCorner( 167 | context, 168 | x + 0.5 * cellSize, 169 | y + cellSize, 170 | x, 171 | y + cellSize, 172 | x, 173 | y + 0.5 * cellSize, 174 | radius, 175 | ); 176 | } 177 | } 178 | }); 179 | const canvasFg = getCanvas(width); 180 | drawCanvas(canvasFg, foreground, { cellSize }); 181 | const ctx = canvasFg.getContext('2d'); 182 | ctx.globalCompositeOperation = 'destination-in'; 183 | ctx.drawImage(canvasMask, 0, 0); 184 | return canvasFg; 185 | } 186 | 187 | function renderSpot({ 188 | foreground, 189 | cellSize, 190 | isDark, 191 | count, 192 | }, maskOptions: QRCanvasEffect) { 193 | const { getCanvas, drawCanvas } = helpers; 194 | const width = cellSize * count; 195 | const canvasMask = getCanvas(width); 196 | const { 197 | value, 198 | foregroundLight = COLOR_WHITE, 199 | spotRatio = 0.25, 200 | } = maskOptions; 201 | const context = canvasMask.getContext('2d'); 202 | const canvasLayer = getCanvas(width); 203 | const canvasFg = getCanvas(width); 204 | const ctxLayer = canvasLayer.getContext('2d'); 205 | [ 206 | { dark: true, foreground }, 207 | { dark: false, foreground: foregroundLight }, 208 | ].forEach(item => { 209 | context.fillStyle = COLOR_BLACK; 210 | context.clearRect(0, 0, width, width); 211 | drawCells({ cellSize, count }, ({ 212 | i, j, x, y, 213 | }) => { 214 | if (isDark(i, j) ^ +!item.dark) { 215 | let fillSize: number; 216 | if ( 217 | i <= 7 && j <= 7 218 | || i <= 7 && count - j - 1 <= 7 219 | || count - i - 1 <= 7 && j <= 7 220 | || i + 5 <= count && i + 9 >= count && j + 5 <= count && j + 9 >= count 221 | || i === 7 222 | || j === 7 223 | ) { 224 | fillSize = 1 - 0.1 * value; 225 | } else { 226 | fillSize = Math.min(1, Math.max(0.25, spotRatio)); 227 | } 228 | const offset = (1 - fillSize) / 2; 229 | context.fillRect( 230 | x + offset * cellSize, 231 | y + offset * cellSize, 232 | fillSize * cellSize, 233 | fillSize * cellSize, 234 | ); 235 | } 236 | }); 237 | drawCanvas(canvasLayer, item.foreground, { cellSize, context: ctxLayer }); 238 | ctxLayer.globalCompositeOperation = 'destination-in'; 239 | ctxLayer.drawImage(canvasMask, 0, 0); 240 | drawCanvas(canvasFg, canvasLayer, { cellSize, clear: false }); 241 | }); 242 | return canvasFg; 243 | } 244 | 245 | function drawCells({ cellSize, count }, drawEach) { 246 | for (let i = 0; i < count; i += 1) { 247 | for (let j = 0; j < count; j += 1) { 248 | const x = j * cellSize; 249 | const y = i * cellSize; 250 | drawEach({ 251 | i, j, x, y, 252 | }); 253 | } 254 | } 255 | } 256 | 257 | function drawCorner(ctx, cornerX, cornerY, x, y, r) { 258 | if (r) { 259 | ctx.arcTo(cornerX, cornerY, x, y, r); 260 | } else { 261 | ctx.lineTo(cornerX, cornerY); 262 | ctx.lineTo(x, y); 263 | } 264 | } 265 | 266 | function fillCorner(context, startX, startY, cornerX, cornerY, destX, destY, radius) { 267 | context.beginPath(); 268 | context.moveTo(startX, startY); 269 | drawCorner(context, cornerX, cornerY, destX, destY, radius); 270 | context.lineTo(cornerX, cornerY); 271 | context.lineTo(startX, startY); 272 | // context.closePath(); 273 | context.fill(); 274 | } 275 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QRCanvas 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Fork me on GitHub 14 | 191 | 192 | 193 | 194 | --------------------------------------------------------------------------------