UNPKG

@bitjson/qr-code

Version:

QR Code Web Component

255 lines (254 loc) 10.9 kB
import { addPlugin, animate } from 'just-animate'; import { waapiPlugin } from 'just-animate/lib.es2015/web'; addPlugin(waapiPlugin); // Comment below and line 149 build for production: // import { player } from 'just-animate/lib.es2015/tools'; import qrcode from 'qrcode-generator'; import { getAnimationPreset, QRCodeEntity, } from './animations'; export class BpQRCode { constructor() { this.contents = ''; this.protocol = ''; this.moduleColor = '#000'; this.positionRingColor = '#000'; this.positionCenterColor = '#000'; this.maskXToYRatio = 1; this.squares = false; } /** * The first update must run after load to query the created shadowRoot for * slotted nodes. */ componentDidLoad() { this.updateQR(); } componentDidUpdate() { this.codeRendered.emit(); } updateQR() { /** * E.g. Firefox, as of Firefox 61 */ const isUsingWebComponentPolyfill = this.qrCodeElement === this.qrCodeElement.shadowRoot; const realSlot = this.qrCodeElement.shadowRoot.querySelector('slot'); const hasSlot = isUsingWebComponentPolyfill ? this.qrCodeElement.querySelector('[slot]') ? true : false : realSlot ? realSlot.assignedNodes().length > 0 : false; this.data = this.generateQRCodeSVG(this.contents, hasSlot); } animateQRCode(animation) { this.executeAnimation(typeof animation === 'string' ? getAnimationPreset(animation) : animation); } getModuleCount() { return this.moduleCount; } executeAnimation(animation) { const modules = Array.from(this.qrCodeElement.shadowRoot.querySelectorAll('.module')); const rings = Array.from(this.qrCodeElement.shadowRoot.querySelectorAll('.position-ring')); const centers = Array.from(this.qrCodeElement.shadowRoot.querySelectorAll('.position-center')); const icons = Array.from(this.qrCodeElement.shadowRoot.querySelectorAll('#icon-wrapper')); const setEntityType = (array, entity) => { return array.map((element) => { return { element, entityType: entity, }; }); }; const animationAdditions = [ ...setEntityType(modules, QRCodeEntity.Module), ...setEntityType(rings, QRCodeEntity.PositionRing), ...setEntityType(centers, QRCodeEntity.PositionCenter), ...setEntityType(icons, QRCodeEntity.Icon), ] .map(({ element, entityType }) => { return { element, // SVGElement.dataset is part of the SVG 2.0 draft // TODO: requires a polyfill for Edge: // https://developer.mozilla.org/en-US/docs/Web/API/SVGElement/dataset positionX: parseInt(element.dataset.column, 10), positionY: parseInt(element.dataset.row, 10), entityType: entityType, }; }) .map((entityInfo) => animation(entityInfo.element, entityInfo.positionX, entityInfo.positionY, this.moduleCount, entityInfo.entityType)); const timeline = animate(animationAdditions); // Comment out below to build for production: // player(timeline); timeline.play(); } generateQRCodeSVG(contents, maskCenter) { const qr = qrcode( /* Auto-detect QR Code version to use */ 0, /* Highest error correction level */ 'H'); qr.addData(contents); qr.make(); const margin = 4; this.moduleCount = qr.getModuleCount(); const pixelSize = this.moduleCount + margin * 2; const coordinateShift = pixelSize / 2; return ` <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="${0 - coordinateShift} ${0 - coordinateShift} ${pixelSize} ${pixelSize}" preserveAspectRatio="xMinYMin meet"> <rect width="100%" height="100%" fill="white" fill-opacity="0" cx="${-coordinateShift}" cy="${-coordinateShift}"/> ${this.squares ? void 0 : renderQRPositionDetectionPatterns(this.moduleCount, margin, this.positionRingColor, this.positionCenterColor, coordinateShift)} ${renderQRModulesSVG(qr, this.moduleCount, margin, maskCenter, this.maskXToYRatio, this.squares, this.moduleColor, coordinateShift)} </svg>`; function renderQRPositionDetectionPatterns(count, margin, ringFill, centerFill, coordinateShift) { return ` ${renderQRPositionDetectionPattern(margin, margin, margin, ringFill, centerFill, coordinateShift)} ${renderQRPositionDetectionPattern(count - 7 + margin, margin, margin, ringFill, centerFill, coordinateShift)} ${renderQRPositionDetectionPattern(margin, count - 7 + margin, margin, ringFill, centerFill, coordinateShift)} `; } function renderQRPositionDetectionPattern(x, y, margin, ringFill, centerFill, coordinateShift) { return ` <path class="position-ring" fill="${ringFill}" data-column="${x - margin}" data-row="${y - margin}" d="M${x - coordinateShift} ${y - 0.5 - coordinateShift}h6s.5 0 .5 .5v6s0 .5-.5 .5h-6s-.5 0-.5-.5v-6s0-.5 .5-.5zm.75 1s-.25 0-.25 .25v4.5s0 .25 .25 .25h4.5s.25 0 .25-.25v-4.5s0-.25 -.25 -.25h-4.5z"/> <path class="position-center" fill="${centerFill}" data-column="${x - margin + 2}" data-row="${y - margin + 2}" d="M${x + 2 - coordinateShift} ${y + 1.5 - coordinateShift}h2s.5 0 .5 .5v2s0 .5-.5 .5h-2s-.5 0-.5-.5v-2s0-.5 .5-.5z"/> `; } function renderQRModulesSVG(qr, count, margin, maskCenter, maskXToYRatio, squares, moduleFill, coordinateShift) { let svg = ''; for (let column = 0; column < count; column += 1) { const positionX = column + margin; for (let row = 0; row < count; row += 1) { if (qr.isDark(column, row) && (squares || (!isPositioningElement(row, column, count) && !isRemovableCenter(row, column, count, maskCenter, maskXToYRatio)))) { const positionY = row + margin; svg += squares ? ` <rect x="${positionX - 0.5 - coordinateShift}" y="${positionY - 0.5 - coordinateShift}" width="1" height="1" /> ` : ` <circle class="module" fill="${moduleFill}" cx="${positionX - coordinateShift}" cy="${positionY - coordinateShift}" data-column="${column}" data-row="${row}" r="0.5"/>`; } } } return svg; } function isPositioningElement(row, column, count) { const elemWidth = 7; return row <= elemWidth ? column <= elemWidth || column >= count - elemWidth : column <= elemWidth ? row >= count - elemWidth : false; } /** * For ErrorCorrectionLevel 'H', up to 30% of the code can be corrected. To * be safe, we limit damage to 10%. */ function isRemovableCenter(row, column, count, maskCenter, maskXToYRatio) { if (!maskCenter) return false; const center = count / 2; const safelyRemovableHalf = Math.floor((count * Math.sqrt(0.1)) / 2); const safelyRemovableHalfX = safelyRemovableHalf * maskXToYRatio; const safelyRemovableHalfY = safelyRemovableHalf / maskXToYRatio; const safelyRemovableStartX = center - safelyRemovableHalfX; const safelyRemovableEndX = center + safelyRemovableHalfX; const safelyRemovableStartY = center - safelyRemovableHalfY; const safelyRemovableEndY = center + safelyRemovableHalfY; return (row >= safelyRemovableStartY && row <= safelyRemovableEndY && column >= safelyRemovableStartX && column <= safelyRemovableEndX); } } render() { return (h("div", { id: "qr-container" }, h("div", { id: "icon-container", style: this.squares ? { display: 'none', visibility: 'hidden' } : {} }, h("div", { id: "icon-wrapper", style: { width: `${18 * this.maskXToYRatio}%` }, "data-column": this.moduleCount / 2, "data-row": this.moduleCount / 2 }, h("slot", { name: "icon" }))), h("div", { innerHTML: this.data }))); } static get is() { return "qr-code"; } static get encapsulation() { return "shadow"; } static get properties() { return { "animateQRCode": { "method": true }, "contents": { "type": String, "attr": "contents", "watchCallbacks": ["updateQR"] }, "data": { "state": true }, "getModuleCount": { "method": true }, "maskXToYRatio": { "type": Number, "attr": "mask-x-to-y-ratio", "watchCallbacks": ["updateQR"] }, "moduleColor": { "type": String, "attr": "module-color", "watchCallbacks": ["updateQR"] }, "moduleCount": { "state": true }, "positionCenterColor": { "type": String, "attr": "position-center-color", "watchCallbacks": ["updateQR"] }, "positionRingColor": { "type": String, "attr": "position-ring-color", "watchCallbacks": ["updateQR"] }, "protocol": { "type": String, "attr": "protocol", "watchCallbacks": ["updateQR"] }, "qrCodeElement": { "elementRef": true }, "squares": { "type": Boolean, "attr": "squares", "watchCallbacks": ["updateQR"] } }; } static get events() { return [{ "name": "codeRendered", "method": "codeRendered", "bubbles": true, "cancelable": true, "composed": true }]; } static get style() { return "/**style-placeholder:qr-code:**/"; } }